# 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

```sh
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:

```sh
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`:

```sh
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 `--structural` and 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 timeline` or the
  merge ancestry of old branches. Timeline, leaves, branches, tags and
  merge ancestry derive exactly as on a dense clone. `zit annotate`
  still credits a line no further back than the content held.

## When to use a dense clone

```sh
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 FULLHASH` to 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 `descendants` horizon is not held and has
  no name in the clone. Clone with `--horizon-rule=leaves` to 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:

```sh
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](RELEASE-NOTES-0.7.0.md) have the measurements.

## A CI job

```sh
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](../www/reference.html) for every flag.
