Artifact c7d8ee1cddaf73902944200deafec2981dbba9f96119424d9a91b3768891cfc4:
- File docs/fast-clones.md — part of check-in [d2b3e6c376] at 2026-10-23 21:21:58 on branch trunk — Docs: the command reference covers horizons and cones; llms.txt and a fast-clone guide for CI and agents (user: dmestas, size: 7116) [annotate] [blame] [check-ins using]
Fast clones for CI and agents
A CI job or an agent needs one check-in's files, a build, and maybe a commit and a push. It rarely reads old history. A clone of a reasonably sized repository for that job should take about a second.
zit gets there with a horizon: the clone holds history from one check-in forward and knows what lies below it by name. Add a path cone and it holds only the part of the tree the job touches. Every artifact it holds is the origin's, under the same name, so the job opens, builds, commits and pushes as it would in a dense clone.
The default: a structural horizon at the commit being built
zit clone "$URL" work --horizon="$REV" --structural --no-open
cd work
zit open "$REV"
$REV is the full hash of the check-in being built (64 hex digits for
SHA3, 40 for SHA-1); a prefix is refused. Your CI trigger usually has it.
--structural leaves out the check-in manifests below $REV, and the
origin sends a seed carrying the branch and tag state at $REV, so
branches and tags above it still derive exactly. The default rule,
descendants, holds $REV and every check-in descended from it.
When the job touches one area, add a cone, and name it again at open:
zit clone "$URL" work --horizon="$REV" --structural --cone=src --no-open
cd work
zit open "$REV" --cone=src
--cone repeats, takes files or directories, and pairs with
--cone-not=PATH and --cone-file=FILE. A structural or cone clone at
the tip moves only the tip's tree plus the structure from $REV forward.
The clone's last line says what the origin served:
cloned 9 artifacts into work in 2 rounds (horizon clone; origin served: structural; nothing elided locally)
--no-open and zit open "$REV" put the tree at the check-in being
built. A plain clone opens the tip of the main branch instead, which is
right only when that is what you build.
When to use a content horizon
Drop --structural:
zit clone "$URL" work --horizon="$REV" --no-open
A content horizon keeps every check-in manifest and control artifact of
every age, and file content only from $REV forward. Use it when:
- the origin is stock Fossil. Fossil ignores the horizon and serves
densely; the clone keeps what a content horizon keeps and drops the
rest locally. Ask for
--structuraland it falls back and says so:the origin did not serve the structural horizon with a seed; keeping the content horizon instead. - the job needs real ancestry, such as a full
zit timelineor the merge ancestry of old branches. Timeline, leaves, branches, tags and merge ancestry derive exactly as on a dense clone.zit annotatestill credits a line no further back than the content held.
When to use a dense clone
zit clone "$URL" work
Use a dense clone when the job mirrors to Git (zit export-git refuses a
horizon store), when other clones will be taken from this one, when it
bisects or annotates across old history, or when the repository is small
enough that the horizon saves nothing.
Deepening when a job needs history
Nothing below the horizon is fetched until something asks for it.
zit update FULLHASHto a check-in below the horizon moves the horizon back to it, keeps the depth and rule, pulls what the new held set adds from the saved remote, and updates there.zit update "$REV"goes back.zit merge NAME, when the merge base lies below the horizon, asks the origin for the base, deepens to it, and merges over real artifacts.- A branch that forked below a
descendantshorizon is not held and has no name in the clone. Clone with--horizon-rule=leavesto hold every open leaf's ancestry back to where it forked from$REV's line. That costs a good deal more on a repository with old open branches.
zit pull and zit sync carry the horizon on every request. Pass no
flag.
What fails loudly
Each failure exits 1 (2 for a usage error) and says what to do.
| You see | Do |
|---|---|
--horizon needs a full artifact hash |
pass the full hash |
--horizon-rule and --structural need --horizon=HASH |
add --horizon |
--horizon names …, which the origin does not hold as a check-in |
check $REV; nothing was cloned |
this store holds history from its horizon … forward (from cat, diff, update) |
zit update FULLHASH to deepen, or re-clone with an earlier --horizon |
content for '…' is elided by this store's horizon |
the same |
a branch below it has no name here (from merge) |
re-clone with --horizon-rule=leaves |
a Git mirror needs all of it (from export-git) |
export from a dense clone |
sparse origin does not cover the requested clone cone |
clone from the origin, not from a horizon or cone clone |
To change the cone, clone again with the new --cone; a fresh horizon
clone is cheap. zit verify counts elided artifacts on their own line,
never as faults:
sparse store: cone of 1 member and horizon at 9cd75f917cfbb709438fc28b633caaf8c88a6b4725c10442b5519c1bb1567edf (descendants); 4 artifacts deliberately elided
verified: 11 frames, 9 artifacts, 1832 bytes, 0 faults
Releases older than 0.7.0 refuse a horizon store rather than read it as dense.
Pushing from a horizon clone
A check-in made in a horizon clone is an ordinary check-in. Its manifest names its parent and its files by real hash, and nothing in it reveals the horizon, so a stock Fossil server stores it byte for byte.
Commit with --no-sync and push on its own line. Autosync failures only
warn, and a CI job wants the push to fail the step:
zit commit --no-sync -m "CI: stamp $REV"
ZIT_PASSWORD="$CI_PASSWORD" zit push --user ci
If a check-in starts a branch and the clone's seed was stale, the origin
refuses it with stale seed: … re-sync, then 'zit amend --branch …' and
push again. Do that; the same push then lands.
Keeping the origin warm
To serve a horizon or cone clone, the origin classifies the request's
cone once and keeps the classification, a cone index and a structural
graph beside the store as .zit-cache sidecars. The first request for a
cone pays for that work; later ones answer from it. A warm origin
answers in about a second for a cone of a large tree.
So run one long-lived zit serve (or a forge in front of one) rather
than spawning a server per request, and leave .zit-cache in place.
Deleting it costs time, never data. The
0.7.0 release notes have the measurements.
A CI job
set -eu
# URL and REV come from the CI trigger: the origin and the full hash to build.
zit clone "$URL" work --horizon="$REV" --structural --cone=src --no-open
cd work
zit open "$REV" --cone=src
./ci/build.sh # your build hook
zit add src/generated.txt # only if the job commits
zit commit --no-sync -m "CI: build $REV"
ZIT_PASSWORD="$CI_PASSWORD" zit push --user ci
Leave out --cone lines for a job that needs the whole tree, and
--structural against a stock Fossil origin. See the
command reference for every flag.