zit Command Reference

Every verb and flag of zit — a Fossil version-control system in Zig. One self-contained repository file (.zit), no staging area, immutable history.

Conventions shared by every command

  • -h, --help — every verb prints its own help and exits.
  • Help topics — zit help TOPIC covers cross-cutting subjects: names (every accepted check-in-name form, timestamps included), sparse (the cone model), repo (-R/--no-cache), exit-codes (the contract and its departures). A typo'd topic refuses with exit 2 instead of falling back to the command list.
  • Exit status — 0 success, 1 operational failure (not a repo, missing file, refused action), 2 usage error (unknown option, wrong operands). Every verb's help now states its contract; frames departs (3 = nothing new, 4 = cursor invalid) and zit help exit-codes lists the departures.
  • -R, --repo <path> — read-only inspectors (log, timeline, leaves, branch, tag, info, ls, cat, artifact, annotate, frames, verify, tarball, zip) accept a repository path directly; no working tree required. <path> is the directory holding .zit or the .zit file itself.
  • --no-cache — derived-state readers (leaves, branch, tag, info, ls, cat) can bypass every sidecar (cache and rebuild checkpoint) and re-derive from the full log, re-verifying whole-log integrity.
  • Name resolution — wherever a check-in name is accepted: a full 40/64-char artifact ID, a unique prefix, a branch or tag name, reserved forms tip, rid:, tag:, root:, start:, merge-in:, or a timestamp — 2026-08-01, 2026-08-01T14:30:00Z, date:… — resolving to the youngest check-in at or before that moment, and the compound BRANCH:TIMESTAMP (trunk:2026-08-01) scoping it to one branch. zit help names is the authoritative statement.
  • Autosync — with the default autosync on and a saved remote, commit pulls before and pushes after, and update/merge pull first. Failures warn and the local operation proceeds; --no-sync skips it once; no saved remote means silence, not a nag.
  • Sparse cones — clone/open/update accept --cone=PATH (repeatable; files or directories), --cone-not=PATH exclusions, and --cone-file=FILE. The most specific member decides coverage — zit cone test PATH reports the verdict and names the member that produced it. A sparse checkout materializes and tracks only the cone; a sparse store holds only what the cone needs, the rest deliberately elided and refetchable by widening. Losing sparse state heals toward dense — bandwidth, never data.
  • Timestamps — every artifact-minting verb writes its D card with millisecond precision (YYYY-MM-DDTHH:MM:SS.SSS, fossil's canonical form), so same-second writes keep real-time order for latest-wins readers; SOURCE_DATE_EPOCH pins .000 for reproducible builds.
  • Authenticated sync — an authenticated exchange carries a fresh credential on every round: a body login until the peer's version gate is known, then a per-round x-f-l-c cookie whose nonce covers that round's own body; zit serve verifies both channels and refuses tampered, replayed, or duplicate logins.
  • Signatures — zit commit --sign clearsigns the manifest and stores the wrapped text as the artifact, so its name covers the signature and every reader strips the wrapper to reach the cards; a signed check-in therefore survives pack, verify, and sync byte-exact. The signer follows fossil's contract — invoked as COMMAND OUTFILE INFILE, an ssh-family basename taking the detached path with zit assembling the armor, off disabling — and is chosen widest-scope-last: $ZIT_PGP_COMMAND, else the repository's pgp-command setting, else gpg --clearsign -o . Setting clearsign on signs every commit without --sign; both settings are deliberately local and never propagate (see settings). A signer that fails or alters the manifest refuses the commit rather than storing it unsigned. zit artifact reports signature presence; validity stays your own gpg --verify over artifact --raw.
  • Worktrees — zit open --store=PATH opens additional working trees of one repository. All trees share the store and its cache; per-tree state (branch, cone, undo, bisect) stays independent, and concurrent commits from different trees serialize on the store's lock.
  • No staging area — add/rm record which files are tracked; contents are read from the working tree at commit time.

Getting started

zit init

Create an empty zit repository.

zit init [-q|--quiet] [--] [directory]

Writes .zit (the repository — a single, self-contained content log) and .zit-checkout (the working-state record) into directory (default: current dir; created if missing). The default branch trunk is unborn until the first commit. Artifacts are named with SHA3-256.

-q, --quietdo not print the confirmation line

zit clone

Copy a repository from a URL into a new directory.

zit clone URL DIR [--once] [--cone=PATH]... [--cone-not=PATH]...
          [--cone-file=FILE]

Creates DIR and its .zit, syncs the peer's artifacts down to convergence over HTTP. The origin's shun table is adopted with it (see shun), so a clone can never come up serving content its origin was ordered to remove; the adopted count is reported. URL is the peer's sync endpoint, used verbatim. Anonymous — no login. The URL is saved as DIR's default remote (canonical form — a user:password@host URL is stored as user@host, password structurally excluded), so pull/push/sync work with no URL. On a terminal, per-round progress shows on stderr with a closing rounds/bytes accounting line. Follow with zit open inside DIR to fill the working tree.

--oncedo not save URL as the default remote
--cone=PATHclone sparse: fetch only the artifacts PATH's subtree needs; everything else stays a deliberately-elided phantom (repeatable — files and directories both work). A cone-aware server filters what it sends; against any other server the clone filters locally, so it degrades to bandwidth, never to error.
--cone-not=PATHexclude PATH's subtree from an enclosing --cone member ("all of src/ except src/vendor/"); the most specific member decides coverage, and a deeper re-inclusion under an exclusion works
--cone-file=FILEread cone members from FILE, one per line — # comments and blank lines skipped, a ! prefix marks an exclusion; combines with the flags

zit open

Fill a freshly cloned directory with its files.

zit open [CHECKIN] [--store=PATH] [--manifest[=r,u,t]] [--cone=PATH]...
         [--cone-not=PATH]... [--cone-file=FILE]

Materializes a check-in in the working tree — the second half of clone. Without CHECKIN, the tip of the main branch; with one (resolved like info — ID, prefix, branch, tag), that check-in directly, so a fresh clone opens straight at a release tag without materializing the tip first. An already-open tree is left untouched (use update to move it); a repo with no check-in is already correct and reported as such — though an explicit CHECKIN must resolve, so there it fails as not found.

Worktrees: --store=PATH in an empty directory opens a detached working tree of the repository at PATH (the checkout holding .zit, or the .zit file itself). The new tree keeps its own checkout state — branch, cone, undo, bisect — but reads and writes the one shared store, so a commit in either tree is immediately visible to the other; concurrent commits from different trees serialize on the store's own lock (writers queue, readers never block).

Re-open: on a tree that is already open, --store=NEWPATH re-points it instead — the rescue for a store that moved out from under its worktrees, uncommitted work intact. The new path must hold the same repository (one containing this tree's current check-in); re-pointing at an unrelated store is refused.

--store=PATHcreate a detached working tree of the repository at PATH; on an already-open tree, re-point it at the (same) repository's new location
--manifest[=r,u,t]write build-stamp files: manifest (r), manifest.uuid (u), manifest.tags (t); bare flag = ru. Remembered — every later update rewrites them. Never replaces a tracked file of the same name.
--cone=PATHopen sparse: materialize and track only PATH's subtree (repeatable). Content the store lacks is demand-fetched through the saved remote, one bounded retry.
--cone-not=PATHexclude PATH's subtree from an enclosing --cone member; the most specific member decides
--cone-file=FILEread cone members from FILE (# comments, ! prefix = exclusion)

Working tree

zit add

Mark files to include in your next check-in.

zit add [-n|--dry-run] [-q|--quiet] [--] <path>...

Records only which files to track — contents are read at commit time. Naming a directory adds every untracked file beneath it recursively; zit add . adds the whole tree. Control files are never added; re-adding a tracked file is a no-op.

-n, --dry-runshow what would be added; change nothing
-q, --quietdo not print the added files

zit rm

Stop tracking files (undo add).

zit rm [-n|--dry-run] [--hard] [-q|--quiet] [--] <path>...

Removes paths from the managed set so the next check-in records their deletion. By default the working copy is left on disk (unlike git). Removing an untracked path is an error; naming a directory forgets every tracked file beneath it.

-n, --dry-runshow what would be removed; change nothing
--hardalso delete the working-tree file from disk
-q, --quietdo not print the removed files

zit mv

Rename a tracked file.

zit mv OLD NEW

Moves the working file, swaps the tracked-name entry, and records the rename for the next commit. OLD must be tracked and present; NEW must not be tracked or exist on disk — collisions are refused, no --force. Exactly two operands; never a glob, never a directory rename. Not undone by zit undo; reverse with zit mv NEW OLD.

zit status

Show the working-checkout state.

zit status

Reports the branch, the current check-in, and each tracked file's standing: added (new, will enter the next check-in), edited (bytes or mode differ), missing (not in the working tree). Unchanged files are not listed. Doesn't report untracked files (extras) or deleted-and-forgotten files (diff).

zit diff

Compare the working tree with the current check-in, or two check-ins.

zit diff [--brief] [-w] [-c N] [--from CHECKIN --to CHECKIN]

Default output is unified hunks (git-compatible headers, three context lines, binary files noted). With --from/--to, compares two check-ins directly.

--briefname-status only: A added, M content/mode change, D removed; sorted by pathname
-w, --ignore-whitespacelines differing only in whitespace compare equal, so a reindent or a CRLF/LF change produces no hunks; a file whose every difference is whitespace-only leaves the output entirely, --brief included
-c, --context Nhunk context lines (default 3)
--from CHECKINolder operand of a check-in comparison
--to CHECKINnewer operand; required with --from

zit extras

List files zit isn't tracking.

zit extras

Every on-disk file under the checkout root that is neither tracked nor a control file — relative paths, one per line, sorted. Symlinks listed but never followed; directories themselves are not listed.

zit clean

Delete untracked files.

zit clean [--dry-run] [--force]

Deletes the set extras lists. Destructive and not undoable. By default it refuses and only reports the count. Never prompts; tracked and control files are never touched; empty directories are not pruned. If both flags are given, --dry-run wins.

--dry-runlist what would be deleted; delete nothing
--forcedelete the untracked files

zit addremove

Track new files and forget deleted ones in one pass.

zit addremove [--dry-run]

Adds everything extras lists and removes tracked files missing from disk (git spells it add -A). Never edits a working file; reports ADDED/REMOVED per action. Modified files are membership-unchanged and left alone.

--dry-runreport what would change; change nothing

zit revert

Restore managed files to their checked-in contents.

zit revert [PATH...]

Discards working-tree edits — all managed files, or just the named ones. Changes contents only, never tracking membership; a managed file the check-in doesn't contain is reported and left alone. Recoverable with zit undo.

zit stash

Set working-tree changes aside and restore them later.

zit stash [save [-m MSG] | list | show [ID] | apply [ID] | pop [ID] | drop [ID]]

Entries live in the local .zit-stash sidecar: durable user data, never synced, never an artifact — a reserved name, so it cannot be tracked. With no action, save runs. apply is fail-closed: the checkout must sit at the entry's own check-in and every target path must be clean — nothing is merged or overwritten.

savecapture every added/modified/deleted file vs. the current check-in, then restore the check-in's state
listone line per entry: id, check-in, file count, message
show [ID]an entry's files as A/M/D lines (newest without ID)
apply [ID]restore an entry's changes into the working tree (fail-closed)
pop [ID]apply, then drop
drop [ID]delete an entry (ids never reused)
-m, --message MSGlabel the saved entry

zit landing

Propose a branch to land on trunk, as a record that can be reviewed, accepted or rejected.

zit landing open --branch NAME [--to TARGET] -m MSG | ls [--all] | show ID | comment ID -m MSG | accept ID | reject ID

A landing is a ceremony, not a new artifact kind. The record is an ordinary forum thread, every comment and state change is a reply in that thread, and the outcome is a raw tag on the check-in that landed. A stock fossil clone reads all of it — the artifacts are ordinary; only the ceremony is zit’s. Nothing here mints a card that zit forum and zit tag do not already mint.

The record names a branch, not a frozen range. Check-ins added after the landing opens belong to it, which is what merging the target back into the branch while work continues requires. TARGET defaults to the repository’s main branch, and a branch may hold only one open landing at a time.

open’s message is split the way a commit message is: the first line becomes the record’s title — the thread’s H card, which is one token — and anything after it becomes the prose beneath. A one-line message is a title with no prose, and the record never carries the same text twice.

show also lists the check-ins on the branch that are not yet on the target — newest first, in topological order, so two made in the same second never invert — and the thread beneath them. A state change renders as an event (“dan rejected this”) rather than a remark, because that is what it is. Both lists are bounded, and say so when they truncate.

accept merges, commits, tags the outcome and records the transition, in that order: a failure part-way leaves merged work under a still-open landing, which one command settles, rather than an accepted record over an unmerged branch. In a checkout it requires that checkout to already sit on the target branch — moving it for you would be this verb rewriting a working tree it was not asked to touch.

With -R it needs no checkout of yours at all. It creates an ephemeral working area beside the store, materializing only the files both sides changed — every other path is resolved into the record without being written to disk — merges there, and removes it. The check-in is minted by the same code a checkout commit uses, so its F, P and R cards match what merging from a checkout would have produced. A landing that does not merge cleanly is refused with exit status 5, naming each conflicting path, and nothing is written: resolving a conflict is the author's job, from their own checkout. A server never holds a half-finished merge and never commits conflict markers.

show also answers would this land cleanly? — computed from the store alone, writing nothing, so a forge can ask before anyone accepts. The merges line reads clean, up-to-date, unrelated, conflict (N), or unknown when the branch or target no longer resolves; a conflicted answer is followed by one conflict PATH line per unresolvable path. The verdict comes from the same three-way classifier the working-tree merge runs, so it agrees with what an actual merge would do — a second, disagreeing conflict detector would be worse than none.

openmint the record for a branch (needs --branch and -m)
lsopen landings, one per line; --all includes settled ones
show IDthe record, its mergeability, the check-ins being proposed, and the thread
comment IDadd a reply without changing the state
accept IDmerge the branch into its target, then record that it landed; with -R, without any checkout of yours
reject IDsettle the record without merging anything
--branch NAMEthe branch being proposed (open)
--to TARGETthe branch it should land on (default: main)
-m, --message Mthe message (required on open and comment); on open, its first line is the title and the rest is the prose
--allls: include accepted and rejected landings
--user NAMEauthor to record (default: $USER); empty is refused
--mimetype Tthe body's mimetype (N card), e.g. text/x-markdown; omitted, the reader applies its own default

zit precedent alias: zit rerere

Reuse a conflict resolution you already authored.

zit precedent [list | show ID | forget ID | forget --all]

When a merge produces a conflict whose bytes match one remembered here, the remembered resolution is used instead of writing markers into the working tree. Precedents live in the local .zit-precedent sidecar: never synced, never an artifact, invisible to fossil. Losing the file costs one hand resolution and nothing else, so damage degrades to “no precedents” rather than failing the merge — reported, never silent.

A precedent is keyed by the file path and the conflicted bytes together, which is exact: the conflict envelope carries all three sides. It is also whole-file, so a file whose other conflict changed will not replay. That under-fires on purpose — a resolution spliced into a neighbourhood that moved would be a plausible wrong answer written silently.

To decline replay for a single merge without forgetting anything, use zit merge --no-precedent.

listone line per precedent: id, path, sizes (the default)
show IDthe conflict a precedent fires on, and its resolution
forget IDdrop one; the next such merge conflicts normally again
forget --alldrop every precedent

zit undo

Undo the last update or revert.

zit undo

Restores every affected file and the checkout record to their state before the last update or revert. Single level — a second undo in a row has nothing to undo. Does not recover clean, mv, or shun.

zit redo

Reapply an undone change.

zit redo

Available only immediately after an undo; a new update or revert clears the redo slot.

Check-ins & branching

zit commit

Record a check-in from your tracked files.

zit commit -m <message> [--user <login>] [--branch <name>] [--close]
           [--allow-fork] [--no-sync] [--sign] [-q|--quiet]
           [--no-submanifest-advice] [--] [<path>...]

Reads each tracked file straight from the working tree — nothing to stage. With no paths, everything tracked is captured; named paths commit just those (the rest carries forward). The first commit creates the root check-in and starts trunk. History is immutable: no amend of content, no --force.

Fork guard: a commit whose parent is no longer its branch's leaf is refused, naming the newer tip — zit update to join it, or --allow-fork to fork deliberately. With autosync on, commit pulls first (so the guard sees the freshest DAG) and pushes the new check-in after.

With sub-manifests enabled and pinned, a commit that drifts >15% from the analyzed optimum prints one advisory line (and interactively offers to retune) — never affects whether the commit succeeds.

-m, --message <msg>the check-in comment (required)
--user <login>author to record (default: $USER)
--branch <name>start a new branch at this check-in; must not name an open branch, needs a parent to fork from
--closemark this check-in a closed leaf
--allow-forkcommit even though the parent is no longer the branch tip — a deliberate fork
--no-syncskip autosync (the pull before and push after) for this one commit
--signclearsign the manifest before storing it — see Signatures in the conventions above
-q, --quietdo not print the check-in line
--no-submanifest-advicesuppress the sub-manifest drift advisory

zit update

Materialize a check-in in the working tree.

zit update [--no-sync] [--cone=PATH]... [--cone-not=PATH]...
           [--cone-file=FILE] [--dense] CHECKIN

CHECKIN resolves like info — full or abbreviated ID, branch, tag, or timestamp. With autosync on and a saved remote, pulls first so CHECKIN resolves against the freshest history. Recoverable with zit undo.

Sparse cones: --cone replaces the checkout's stored cone (widening fetches newly covered content, demand-fetching through the saved remote when the store lacks it; narrowing dematerializes files without deleting stored content), --dense drops it and rematerializes everything. Absent flags inherit the stored cone.

--cone=PATHmaterialize and track only PATH's subtree (repeatable); replaces the stored cone
--cone-not=PATHexclude PATH's subtree from an enclosing --cone member; most specific member decides
--cone-file=FILEread cone members from FILE (# comments, ! prefix = exclusion)
--densedrop the stored cone and rematerialize the full tree
--no-syncskip the autosync pull for this invocation

zit cone

Show, amend, or query the sparse cone.

zit cone
zit cone add PATH...
zit cone remove PATH...
zit cone test [--store] [--quiet] PATH...

With no arguments, prints the worktree cone's members one per line — exclusions in the cone-file grammar (!member) — or dense. add/remove compose the stored member set with the change and drive the same materialization path as update --cone at the current check-in, so an add demand-fetches missing content and a remove dematerializes. remove of an exclusion member re-widens. Removing every member is refused — return to dense with zit update --dense.

test answers "would this cone keep this path, and why" without materializing anything. Coverage is decided by the longest member covering the path, across both polarities — so the deciding member is often neither the first listed nor the one a gitignore last-match-wins habit would predict, and naming it is what makes the rule legible:

$ zit cone test src/a.zig src/vendor/big.bin src/vendor/keep/gem.txt README
covered src/a.zig                src
elided  src/vendor/big.bin       src/vendor
covered src/vendor/keep/gem.txt  src/vendor/keep
elided  README                   (no member covers)

The member prints bare, so it can be handed straight to zit cone remove; a named member under elided is necessarily an exclusion. A dense checkout reports covered … (dense), which keeps the exit status honest — the file really is there. Paths are validated as worktree paths first, so one that could never be tracked is a usage error rather than a silent elided. Exits 0 when every path is covered and 1 when any is elided, making it usable as a predicate: zit cone test build.zig && make.

add PATH...union PATHs into the cone and rematerialize at the current check-in
remove PATH...narrow the cone; files leave the tree, the store keeps their bytes
test PATH...classify PATHs and name the deciding member; - reads paths from stdin, one per line
--storetest only: query the store cone instead of the worktree cone. The two need not agree, and a path present in the checkout but absent from the store is a distinct condition.
--quiettest only: suppress the per-path lines, leaving only the exit status

zit worktrees

List the working trees this repository serves.

zit worktrees

Every tree registered against this store: the invoking tree (starred), every detached tree (open --store), and every co-located checkout that has opened or updated against it — each with its branch and check-in prefix. The registry is advisory and self-healing: trees stamp themselves at open and update, and the listing lazily prunes entries whose path is gone or whose checkout re-pointed at another store (membership is verified by the store file's inode, so a same-named impostor never counts). With -R it answers from anywhere with no working tree — "which trees does this store serve?". Move-safety warnings and cross-repo sweeps build on this data.

zit merge

Merge another check-in into the working tree.

zit merge [-X ours|theirs] [--no-sync] [--no-precedent] NAME
zit merge --cherrypick NAME
zit merge --backout NAME | --probe NAME [--into TARGET]

Three-way merge, recorded as a pending merge parent — run commit to record it. Conflicts get markers in the files and a list on stderr; resolve and commit, or undo to abandon. With autosync on, pulls first.

-X ours / -X theirs resolve every content conflict in favour of one side instead of writing markers — the mechanical cases, like a regenerated lockfile or a vendored tree taken whole from upstream, where resolving by hand is where mistakes enter. A region only one side changed is still that side's either way: -X answers only what the merge could not, so it is not “discard their work”, it is “stop asking me”.

-X ours is not --backout. --backout reverses NAME's change; -X ours keeps your side where the two collide and reverses nothing. They read alike and do different things.

One case -X deliberately declines: a file one side modified and the other deleted. That is a disagreement about whether the file should exist, not about its contents, and either reading silently destroys work — so it stays a conflict with or without -X. A strategy also leaves nothing for precedent to replay or record, since no conflict survives it.

Across a sparse cone: out-of-cone paths resolve at hash level with no blob reads — theirs-only changes are applied to the record (reported as "applied to the record"), ours-only carry forward, and only a path changed on both sides refuses, naming the path and the remedy (zit cone add … to widen).

--probe answers would this merge cleanly? and writes nothing — no merge, no working tree touched, no autosync. It prints merges followed by clean, up-to-date, unrelated or conflict (N), then one conflict PATH line per unresolvable path. Exit status is 0 in every case: “has conflicts” is an answer, not a failure, so a caller reads the line rather than the status.

Being a read it takes -R and needs no checkout — but then there is no current check-in to merge into, so --into TARGET is required. In a checkout it defaults to the check-in you are on. A real merge refuses -R, because it writes a working tree.

--probereport mergeability and write nothing
--into TARGETprobe against TARGET (required with -R)
-X, --strategy ours|theirsresolve content conflicts in favour of one side rather than writing markers
--cherrypick NAMEapply only NAME's own change onto the working tree
--backout NAMEreverse NAME's change on the working tree
--no-syncskip the autosync pull for this invocation
--no-precedentdo not replay remembered conflict resolutions for this merge (see precedent); nothing is forgotten, only declined

zit tag

List tags, name a check-in, or attach a property to one.

zit tag list [--raw] [CHECKIN] [--no-cache]
zit tag add [--raw] [--propagate] NAME CHECKIN [VALUE]
zit tag cancel [--raw] NAME CHECKIN

A tag is one of two things, and --raw picks between them.

Without --raw, NAME is a symbolic name for one check-in (stored sym--prefixed): it resolves to that check-in everywhere a check-in name is accepted, and unlike a branch name later check-ins do not inherit it. Right for release names. Two check-ins carrying one name make that name ambiguous.

With --raw, NAME is written verbatim as a property, outside name resolution entirely — right for a fact many check-ins may carry, such as a CI verdict, optionally with a VALUE. Because raw tags are not names, plain list omits them; list --raw shows every tag with its prefix intact.

cancel retracts a tag, taking the same --raw choice as the add that created it. Names beginning wiki-, tkt-, or event- are refused — fossil crosslinks wiki pages, tickets, and technotes through them. ls aliases list; --no-cache and -R are accepted, and the write paths work against a bare store.

--rawNAME is a raw tag name, not a symbolic check-in name; on list, show every tag with its prefix intact
--propagateadd only: descendants inherit the tag (mints * rather than +). Hazardous without --raw — an inherited symbolic name drifts to the newest descendant carrying it.
VALUEadd only: the tag's value payload; omit it for a valueless tag (an empty VALUE has no spelling on the wire and is refused)

zit amend

Attach display overrides to a check-in.

zit amend NAME [--comment TEXT] [--user NAME] [--date ISO] [--branch NAME]

Mints one control artifact; history is never rewritten and the original check-in is untouched. At least one override flag is required.

--comment TEXToverride the displayed comment
--user NAMEoverride the displayed author
--date ISOoverride the displayed date (YYYY-MM-DDTHH:MM:SS)
--branch NAMEmove the check-in (and its descendants) to branch NAME

zit bisect

Binary-search the check-in DAG for a good/bad boundary.

zit bisect bad NAME | good NAME | next | status | reset

Mark a known-bad and known-good check-in; zit narrows the candidate span and moves the working tree to the midpoint for you to test, until one check-in remains. Moves go through the update engine — refused over a dirty tree, recoverable with undo. State lives in .zit-checkout, never in the repository.

bad NAMEmark NAME as containing the defect
good NAMEmark NAME as free of the defect
nextmove to (or re-announce) the current probe check-in
statusshow the marks, the current probe, and the remaining count
resetend the bisection and restore the starting check-in

History & inspection

zit log

Show a check-in and its reachable parents.

zit log [--long] [-n N] [-R <path>] CHECKIN [PATH]

Depth-first P-card order from CHECKIN, resolved like info — full or abbreviated ID, branch, tag, or reserved form. Each check-in shown once; a missing referenced parent shows once as a phantom. With PATH (exact repo-relative path, not a pattern), shows only check-ins where that file's effective content changes — including add and delete.

--long adds each check-in's own facts after its id, so a file-history page is one invocation rather than a second call per row:

$ zit log --long tip readme.md
checkin 2b268f57…  2026-08-17T13:00:46.515  dmestas  third
checkin e35bda20…  2026-08-17T13:00:46.462  dmestas  second
checkin 347f4e8e…  2026-08-17T12:58:56.727  dmestas  seed

The comment is last because it is the only field that may contain spaces; a check-in with no comment prints an empty trailing field, so the column count never varies. An absent timestamp or user reads -. Works with and without PATH.

The default listing reads no artifact content. It walks the derived parent adjacency — the same relation Fossil keeps in its plink table and traverses by indexed query — so it costs the same on a 300 MB repository as on a small one. --long is different in kind: the timestamp, user and comment live only in each check-in's manifest, so that form reconstructs and parses every one. On a repository whose manifests inline the whole file set the gap is large — seconds against milliseconds — which is worth knowing before scripting --long over deep history.

Not an integrity check. log trusts what the derive already validated and does not re-hash the artifacts it names, so it will happily list a check-in whose stored bytes have since been damaged. verify is the verb that re-reads and re-hashes the store; Fossil draws the same line between traversal and test-integrity.

The --long record grammar — checkin <id> <timestamp> <user> <comment> — is a contract: consumers parse it positionally, and no display option, present or future, will change it.

--longadd timestamp, user and comment to each entry
-n, --limit Nshow only the N most recent entries; 0 means all
--weekdayshow the UTC day of week beside each date — Ben Joffe’s branchless day-of-week computation (benjoffe.com/fast-day-of-week)
-R, --repo <path>read this repository instead; no working tree required

zit timeline

List every check-in, most recent first.

zit timeline [-n N] [--weekday]

Per line: short hash, ISO timestamp, branch, first comment line. Merges are marked M. Order is event time descending with hash tiebreak — byte-identical across runs.

-n, --limit Nshow only the N most recent entries; 0 means all
-R, --repo <path>read this repository instead; no working tree required

zit leaves

List the open leaves of the check-in DAG.

zit leaves [--all] [--no-cache]

Check-ins with no same-branch child and no active closed tag; full ID plus branch, most-recent first, current checkout marked *.

--allalso list closed leaves, each marked (closed)
--no-cachebypass every derived sidecar and re-derive from the full log
-R, --repo <path>read this repository instead; no working tree required

zit branch

List the repository's branches.

zit branch [--no-cache]

One per line, by name, derived from active branch tags. Current checkout's branch marked *; a branch whose newest check-in is closed is marked (closed). Accepts --no-cache and -R.

zit info

Resolve a name to a check-in and describe it.

zit info NAME [--no-cache]

NAME: full ID, unique prefix, branch/tag, or reserved form (tip, rid:, tag:, root:, start:, merge-in:). Resolution follows recency, not open-leaf status. Accepts --no-cache and -R.

zit ls

List the files of a check-in.

zit ls [VERSION] [--long|--size] [--under=PATH] [--no-cache]

<blob-id> <filename> per line in canonical order. VERSION resolves like info; omitted = current checkout (unborn is an error). Shows the effective set: delta check-ins are merged with their baseline, deletions omitted. Accepts --no-cache and -R.

--long adds three columns before the filename, which stays last so a name containing spaces is still unambiguous:

$ zit ls --long
f04f3b9b…  11  -  text    readme.md
980e8347…  4   -  binary  src/blob.bin
59df8a6e…  18  x  text    tools/run.sh

perm is the F card's own permission token — x executable, l symlink, - when the card carries none. It is the one fact no other verb can supply: it lives in the manifest, and the blob it names does not know it is executable, so without this a listing cannot mark executables and mis-renders a symlink (whose blob content is its target path). l reaches a repository only through import-fossil — zit commit refuses a symlink, matching fossil's allow-symlinks default.

class is text, binary (the content holds a NUL), or absent when the store cannot produce the bytes — a sparse elision, a shunned artifact, or a phantom. There size reads 0 because nothing was measured, not because the file is empty.

--size is the cheap half — <blob-id> <size> <perm> <filename>, no class column — and the split is exactly where the cost is. A size is read out of the frame header: a zlib frame states its uncompressed length outright, a delta carries its target size in its own header, so --size costs a header read per file and never resolves a delta chain. The binary verdict cannot be had that way, because it is "a NUL anywhere in the content" and the NUL may be the last byte. So a listing that needs names and sizes should ask for --size; one that needs the text/binary answer pays for --long. Under --size an unavailable blob prints absent in the size column, since there is no size to report.

--long reads every listed blob, so pair it with --under to pay only for what is displayed. The default two-field output reads no content and is unchanged.

--longadd size, permission and text/binary columns
--sizeadd size and permission only; reads no blob content
--under=PATHlist only entries at or beneath PATH, matched on segment boundaries so src never matches src-other; PATH may name a single file

zit cat

Emit a file's contents at a check-in.

zit cat FILE [VERSION] [--no-cache]

Verbatim to stdout — no trailing newline added, binary passes through. Resolved against the effective file set (a delta check-in's own entry wins over the baseline; deletions read as not found). Emitted bytes are verified against the recorded hash. Accepts --no-cache and -R.

zit artifact

Print or describe a raw artifact by its hash.

zit artifact HASH [--raw]

HASH is a full or ≥4-char hex prefix, resolved against every stored artifact including file blobs — branch/tag names not accepted (use info). Default is a summary: hash, size, type, per-card lines, Z self-checksum status. Accepts -R.

--rawwrite the exact stored bytes to stdout — hashing them reproduces HASH

zit frames

Stream raw artifacts appended since a cursor (indexer feed).

zit frames [--since=CURSOR] [--verify=TOKEN] [--structural]

The incremental read seam for an external consumer that only speaks through the zit binary. Each artifact appended after CURSOR is emitted as a header line artifact HASH SIZE followed by exactly SIZE raw bytes (whole artifacts — delta storage is resolved and every payload is verified against its name; byte-identical to artifact HASH --raw), then one trailer line cursor CURSOR TOKEN. Pass both trailer values back on the next poll; they are opaque. Without --since, the full census streams (the bootstrap read).

--verify proves the already-consumed prefix is still byte-identical: appends (commits, sync arrivals) keep a cursor valid; a store rewrite (pack, a shun reclaim) fails validation — the cursor is dead and the consumer re-reads from zero (derived indexes rebuild; loss is detectable by design, never silent). Shun-masked artifacts and phantoms are skipped with the cursor still advancing.

--structural emits only artifacts that parse as structural artifacts — the same gate the repository’s own crosslink applies — and skips content blobs, which dominate a full stream’s bytes. Skipped frames are neither hashed nor emitted; the cursor still advances over them, so the flag never changes what a later poll sees and filtered and unfiltered polls compose against one cursor. A poll whose new frames are all content emits nothing and exits 3, trailer included.

Threads: this is the one verb that uses more than one. Reconstruction stays single-threaded — it memoises delta bases, and consecutive artifacts routinely share one — while the per-artifact hash check runs on up to min(cores, 8) workers over a bounded batch. Emission order, the payload bytes, and the exit codes are identical at any worker count; the only thing that varies is how many cores a full-history read will occupy.

--since=CURSORemit only artifacts appended after CURSOR
--verify=TOKENvalidate the consumed prefix before emitting
--structuralemit only structural artifacts; skip content blobs (cursor semantics unchanged)

Exit status departs from the global convention: 0 artifacts emitted, 3 cursor current with nothing new (trailer still printed), 4 cursor invalid — re-read from --since=0. 1/2 keep their usual meanings. Accepts -R — a bare indexer is the primary consumer.

zit annotate

Show which check-in introduced each line of a file.

zit annotate PATH [VERSION] [--full]

Per line: 10-char ID prefix of the introducing check-in, its UTC ISO date, then the line. A line is attributed to the oldest check-in carrying it; a merged line is credited to its origin branch, never the merge itself. Only committed content is annotated; binary files are refused. VERSION resolves like cat's (omitted = the current checkout), so with -R and a VERSION no working tree is required — a bare store annotates at any check-in.

--fullemit the full artifact ID per line instead of the 10-char prefix — stable as an index key

zit grep

Search history for a literal string, or find when it appeared and went.

zit grep [-n N] [--no-cache] [-R <path>] PATTERN PATH...
zit grep -S|-G [-n N] [--no-cache] [-R <path>] PATTERN [PATH...]

For each PATH the file's stored versions are walked newest-first, and every version whose content contains PATTERN is reported one line per matching line: <checkin12> <path>:<lineno>: <line>. A run of check-ins that did not touch PATH shares one blob and is reported once, not once per check-in.

This searches history, not the working tree — content that is not on disk, because it lives as deltas inside .zit. For the files in front of you, a dedicated tool such as ripgrep is faster than zit would be.

PATTERN is a literal string, not a regular expression: zit carries no regex engine. git log -S is literal for the same reason and is the closer analogue; matching fossil grep's POSIX expressions is later work.

A version whose bytes the store cannot produce — elided by a sparse store, shunned, or a phantom — is counted and reported on stderr rather than skipped silently: a search that quietly examined less than it claimed looks exactly like one that found nothing. Binary versions (any NUL byte) are not searched.

The pickaxes ask a different question: not "which versions contain this" but "when did it appear, and when did it go". They report one line per check-in, newest first — <checkin12> <±N|~N> <path>. A string added once and deleted later is exactly two lines, where plain grep reports every version that held it in between. PATH becomes optional and narrows the walk instead of driving it.

-S fires when the occurrence count changed; -G when a changed line mentions the pattern. Renaming x = SECRET to y = SECRET leaves the count at one, so -S is silent and -G is not — git draws the same line between its two pickaxes, and it is why both exist. Give one or the other, never both.

These are git log -S/-G. zit takes the string as the ordinary PATTERN operand rather than git's attached -S<string>, so there is one way to say it. Both read only the blobs that actually changed — a path whose content hash matches its parent's cannot have moved either answer — so the walk is far cheaper than a diff per revision. Merges compare against the primary parent only, as git does; counting against every parent would report one appearance twice.

-n, --limit Nstop after N matching versions; 0 means all
-S, --pickaxereport check-ins where the occurrence count changed
-G, --diff-matchreport check-ins where a changed line mentions PATTERN (runs the diff engine per changed path, so it costs more than -S)

Exit status departs from the convention: 0 at least one version matched, 1 nothing matched or the repository could not be read, 2 usage error.

zit tarball

Write a check-in's files as a gzip-compressed tar archive.

zit tarball VERSION [OUTPUTFILE] [--name DIR] [--include GLOBS]
            [--exclude GLOBS] [--list] [--no-cache]

VERSION resolves like info. No working tree is involved and none is created — the file set is read straight from the repository, so -R works and a bare mirror can cut a release. Control files are never archived.

Modes travel. An F card's permission rides with the file: an executable stays executable and a symbolic link stays a link. A checkout-then-tar workaround routinely loses this, and the result looks perfectly correct — right files, right bytes — until someone unpacks it and ./configure will not run.

The bytes are reproducible. Members appear in the check-in's own canonical order, carry the check-in's own timestamp rather than the clock, and record no uid, gid, user name, or build host. Two runs of one VERSION on different machines produce identical files, which is what makes the output usable in a release pipeline.

Every member sits under one top-level directory. --name sets it; otherwise it is derived from OUTPUTFILE's basename with the archive suffix stripped (release-1.2.tar.gz gives release-1.2), falling back to the check-in's short id when there is no usable output name.

An OUTPUTFILE of /dev/null or the empty string writes nothing; with --list that is how you preview an archive. A file the store does not hold — a sparse store elides out-of-cone content — is refused by name before any bytes are written, never omitted, which would look exactly like success.

--name DIRtop-level directory inside the archive
--include GLOBSarchive only files matching this glob list
--exclude GLOBSarchive no file matching this glob list; a file matching both lists is excluded
--listname the members instead of writing an archive
--no-cachebypass the derived-state cache and re-derive

GLOBS is a comma- or newline-separated list in the same dialect as ignore-glob. Unlike fossil, a pattern may not be quoted, so a glob cannot contain a comma.

zit zip

Write a check-in's files as a zip archive.

zit zip VERSION [OUTPUTFILE] [--name DIR] [--include GLOBS]
        [--exclude GLOBS] [--list] [--no-cache]

Identical to tarball in every respect but the container: same selection, same top-level directory rules, same reproducibility guarantee, and the same refusal to write a partial archive.

Modes travel in the central directory's external attributes — where a reader looks, and where an executable silently becomes 0644 if a writer gets it wrong. A member is deflated only when that makes it smaller, so an already-compressed file is stored rather than grown.

Members are limited to 4 GiB: zip64 is not emitted, and a file needing it is refused by name rather than written with a truncated size field. Use tarball for such a check-in.

--name DIRtop-level directory inside the archive
--include GLOBSarchive only files matching this glob list
--exclude GLOBSarchive no file matching this glob list; exclude wins
--listname the members instead of writing an archive
--no-cachebypass the derived-state cache and re-derive

Sync & serving

zit serve

Serve this repository over HTTP for sync.

zit serve [--port N]

Answers POST /xfer with an application/x-fossil body per the sync protocol; one request per connection, single-threaded. Binds 127.0.0.1. Stop with Ctrl-C. A running server picks up a concurrent shun or pack on its next request, no restart needed. A cone-scoped requester is served filtered; a sparse store refuses to be a clone origin for cones it does not cover (dense clone requests included) with one error card and no content — the requester's clone fails quoting the refusal.

--port Nlisten on port N (default 8080)

zit pull

Fetch new artifacts from a peer.

zit pull [URL]

Announces what this repo holds, requests everything it lacks, converges toward the peer's content. Anonymous. With URL omitted the saved remote is used; neither is an error.

zit push

Send this repository's artifacts to a peer.

zit push [URL] [--user U [--password P]]

Uploads every artifact the peer lacks until both converge; nothing on this side changes. With URL omitted the saved remote is used. Without --user the push is anonymous; with it, a shared secret derived from the password signs the login card. After one successful authenticated push the derived secret (never the password) is saved at 0600, so later pushes need only --user.

--user Uauthenticate as login U (sync §6)
--password PU's password; else ZIT_PASSWORD, else a no-echo prompt

zit sync

Reconcile with a peer in both directions.

zit sync [URL] [--user U [--password P]]

One session, not pull-then-push: uploads what the peer lacks and receives what this side lacks until convergence. History transfers both ways and artifacts are never discarded — there is no force-push. Authentication works exactly like push.

--user Uauthenticate as login U (sync §6)
--password PU's password; else ZIT_PASSWORD, else a no-echo prompt

zit remote

Show, set, or clear the default sync remote.

zit remote [URL | --unset]

A single default remote, not a named registry. No argument prints it (including what clone saved); URL saves it verbatim. Never put a credential in the URL — authenticate with --user on push/sync instead.

--unsetclear the saved remote

zit settings

Show or change this repository's local settings.

Versionable settings also read Fossil’s own spelling, .fossil-settings/<name>, which wins over .zit-settings/<name> when both exist — a repository mirrored from a Fossil server keeps its ignore, CRLF, and binary policy without renaming anything (#117).

zit settings [NAME [VALUE]]

Bare lists every setting and its value; NAME prints one; NAME VALUE sets it. Every setting this verb edits is local to this repository and never crosses the wire — a clone comes up with defaults for all of them.

That is a deliberate line, not an omission. pgp-command is a command line this machine executes, so a value a peer could set would be remote code execution; fossil classifies it sensitive and propagates it nowhere, and clearsign sits in no configuration group at all. Settings that legitimately travel do so by other mechanisms — tracked .zit-settings/ files that clone like any other source, and the sync configuration groups.

--globalread or write the operator-scope tier instead of this repository's
autosync on|offpull from the saved remote before commit/update/merge and push after a successful commit (default on; failures warn and the local operation proceeds)
ignore-glob LISTcomma- or newline-separated globs that extras, addremove, clean, and add's directory expansion leave alone (* matches /, so a directory is zig-out/*); empty clears it. Two tracked files override it and clone with the repository: .zit-settings/ignore-glob (this same fossil dialect) and a root .zitignore (gitignore dialect). Naming an ignored file explicitly still adds it.
clearsign on|offclearsign every commit, as though --sign had been passed (default off) — fossil's setting of the same name
pgp-command CMDthe clearsign command, invoked as CMD OUTFILE INFILE (default gpg --clearsign -o , trailing space included; an ssh-family basename takes the detached form CMD INFILE and writes INFILE.sig; a false-ish value — off/no/0/false — disables signing). $ZIT_PGP_COMMAND overrides it, so one invocation can be redirected without editing the repository.

Store & maintenance

zit stat

Report the store's size and artifact census.

zit stat [--artifacts] [--type=full|delta]

One field value line per fact, read from the frames in .zit — nothing estimated: store/header/content/frame-overhead/config/shun/user bytes, artifact counts, and the full-form vs. delta partition of content bytes.

user-frames counts what the log physically holds — rows plus any supersede markers a pack has not yet reclaimed — so it can exceed the number of rows zit users prints.

--artifactslist stored artifact hashes instead, one per line, ascending
--type=KINDwith --artifacts, only full or delta artifacts

zit pack

Repack the store into a compact new generation.

zit pack [--drop-elided]

Re-encodes full-form artifacts as deltas against their lineage and drops superseded config records; content and names are preserved exactly. Also where shunned content is reclaimed — reclamation is not reversible — and where a superseded user-table row goes away. Written to a temp file and swapped in atomically; transiently needs room for old + new store.

An artifact already stored as a profitable delta is copied verbatim, not re-encoded and compared. Re-beating every delta by a few bytes meant pack never reached a fixpoint: each run rewrote the whole store again for scraps. A delta that fails the profitability bar — a peer's poorly chosen base — is still re-based, so the verb keeps doing what its name promises.

A repeat pack over a store nothing has appended to answers immediately and rewrites nothing. Any commit, pull, or shun row re-arms it. A pack that answers this way streams no bytes and therefore checks no integrity — when a run does rewrite, it verifies every frame checksum and every reconstructed artifact hash along the way, but zit verify is the pass that always reads everything.

--drop-elidedon a sparse store, also drop artifacts that are out-of-cone and unreferenced by any spine — the sparse analog of shun reclamation, refetchable by widening (unlike a shun). Refused on a store whose worktree cone is wider than its store cone.

zit verify

Check the store for damage (zit's fsck).

zit verify [-R <path>]

Two passes: every frame's whole-frame checksum (catches on-disk bitflips), then every artifact reconstructed through its delta chain and re-hashed against its name (catches valid checksums over wrong bytes). Read-only, lockless, safe against a live serve. An incomplete final frame from a crash is reported but is not a fault. Unreclaimed shunned artifacts are verified and labelled. On a sparse store the summary counts the cone's members and the deliberately elided artifacts, and verification abstains from checksums it cannot reconstruct without out-of-cone bytes — elision is never reported as damage.

zit config

Fetch a configuration area from a peer, or apply one from a file.

zit config pull shun|user [URL] [--overwrite] [--user=NAME] [--password=SECRET]
zit config import FILE
zit config merge FILE

Fossil's fossil configuration pull|import|merge. Configuration otherwise travels at clone time only, and that is deliberate: a shun table that refreshed on every sync would let any peer suppress content in this repository on its own say-so. This verb is how you ask for it anyway — explicitly, naming the area and the peer.

The shun area carries the peer's takedown records. Rows are added, never removed, so pulling cannot un-shun anything here. URL defaults to the saved remote; only configuration moves, no artifacts.

The user area carries the peer's logins, capabilities, and password hashes. Fossil serves it to an administrator only, so the pull is credentialed — --user=NAME with --password=SECRET or $ZIT_PASSWORD. Without them the peer answers with no rows and this verb says so rather than reporting an empty success. A pull merges (a row wins only if its mtime is newer), which is fossil's default; --overwrite replaces the local table instead, for a mirror tracking an upstream authoritatively. A local-only login survives a merge and does not survive an --overwrite.

import and merge read a file written by fossil configuration export AREA FILE, whose cards are byte-identical to what the sync protocol carries — so a fossil repository's user table reaches zit with no SQLite reader, no network, and no running server. import replaces the local table, merge folds rows in with the newest mtime winning; both are fossil's own two modes. Cards for areas zit does not model are counted and ignored. Read the result with zit users.

zit users

Print the repository's user table.

zit users [--raw] [-R <path>]

One row per line, TAB-separated and sorted by login, so it reads by machine: LOGIN CAPS MTIME INFO, and with --raw the password hash after CAPS. The four category rows — nobody, anonymous, reader, developer — are ordinary rows of this table and are listed with the rest, exactly as fossil holds them.

The hash is withheld unless asked for. It is the sync protocol's shared secret, and fossil serves the whole user area only to an administrator, so a listing that printed it by default would make it easier to lift here than from the repository this one mirrors.

CAPS is fossil's capability string, stored and printed verbatim. zit never interprets it: the vocabulary is fossil's to define, and a reader that parsed it would need changing every time fossil adds a letter. A repository that has never had a user table imported prints nothing and exits 0 — an empty table is a fact, not a failure.

--rawinclude the password hash column

zit trust

Accept or withdraw consent to run the project's checks.

zit trust [--yes] [--revoke]

A project can ship its own pre-commit checks as a tracked file, .zit-settings/before-commit, so they clone with the code and are reviewable in a diff. That file is inert on its own: zit never runs it until this checkout says so. Cloning a repository therefore executes nothing, and neither does pulling an edit to a check you already accepted — consent is pinned to the exact text you read, so any change revokes it and asks again.

Bare, it prints the command and its status and grants nothing: accepting something you have not been shown is not consent. Run it again with --yes to accept. A before-commit you set yourself with zit settings needs no trust record — writing the command is the consent.

--yesaccept the declaration as shown; commits then run it
--revokewithdraw consent; commits are refused until re-accepted

zit purge

Remove leaf check-ins, recoverably, and reclaim their bytes.

zit purge checkins [--dry-run] [--explain] [--reason TEXT] [--force] CHECKIN...
zit purge list
zit purge undo ID
zit purge obliterate [--force] ID...

The tool for the ordinary accident — an add . that swept in a build tree, noticed before anyone else has the check-in. Removed artifacts go to a local graveyard that purge undo restores and purge obliterate discards for good.

Not a takedown. A purge is local, so a peer that already holds the content keeps it and a later sync fetches it straight back. Use shun when the content must never exist here again. This is the trap the two verbs exist to keep apart.

A blob is removed only when no retained check-in still references it. A file the purged check-ins share with surviving history stays exactly where it is — removing it would not free anything, it would turn held content into a phantom the next update fails on. Tags naming a purged check-in go with it.

Only leaves can be purged, judged over the whole named set: zit purge checkins A B where B is A's child is accepted, because afterwards nothing points at either. Naming A alone is refused rather than orphaning B. The check-in your working tree is on cannot be purged at all — the one refusal with no override.

--dry-runreport the plan and exit 0 without changing anything
--explainlist every artifact the plan removes, not only the counts
--reason TEXTrecord why, shown by purge list
--forceactually remove (refuses by default; never prompts)

Reclaiming rewrites the store into a new generation and swaps it in atomically, exactly as pack does; a crash before the swap leaves the original untouched. The graveyard is local and disposable like the undo journal — never an artifact, never synced, not carried by a clone. Losing it costs recoverability, not data.

No tombstone is recorded, and that is precisely what makes a purge recoverable — and precisely why it is local. shun is the verb that also stops the content coming back.

zit shun

Permanently remove artifacts and record them as never-fetch.

zit shun [--reason TEXT] [--defer] [--force] HASH...
zit shun --list

Irreversible — undo does not recover it and the content cannot be re-fetched. By default it refuses and reports what would be destroyed; never prompts. Every sync direction honours the shun table (never requested, announced, served; arriving copies discarded). A hash not held here may still be shunned (given in full — a prefix can't be completed). The current checkout's check-in cannot be shunned. Removal rewrites the store atomically like pack; concurrent rewrites, or a store that changed mid-examination, refuse cleanly.

--reason TEXTrecord a single-line note (shown by --list)
--deferrecord the shun; leave the bytes until zit pack. Reads answer identically, but the bytes stay reconstructible on disk until the rewrite. Still needs --force.
--forceactually record and remove
--listlist this repository's shun table and exit

The table travels to clones. A clone of this repository adopts its shun table, and a clone from a Fossil server adopts that server's — over the same /shun configuration group Fossil uses, so the carriage is bidirectional with stock Fossil. This matters because shunning is a takedown mechanism: a clone that arrived without the table would, on its next sync with any third party, re-accept exactly the content an operator was ordered to remove.

The carriage is one-way and clone-time only. An ordinary pull or sync never changes this table, so a peer can never shun an artifact here on its own say-so.

zit checkpoint

Show, build, verify, or drop the local rebuild checkpoint.

zit checkpoint [show | build [CHECKIN] | verify | drop]

A local, disposable accelerator letting from-scratch derivation skip the log's permanently flat prefix. Sidecar only: never writes the repository, never synced, never history; deleting it changes no answer and --no-cache bypasses it. Planted automatically by the first full derivation and advanced by the ordinary open path.

showprint the stored claim and whether it still validates (default)
build [CHECKIN]build/rebuild, anchored at CHECKIN (full ID) or the newest check-in
verifyre-derive everything claimed from real history and compare byte-for-byte
dropdelete the checkpoint (missing = success)

zit submanifest

Show or change this repository's sub-manifest settings.

zit submanifest [show | enable [--k N] | disable | retune --k N | analyze [--names PATH]]

A sub-manifest lets a check-in carry its file set as a tree of shared, content-addressed slices instead of one flat F-card list. Emission is OFF by default; turning it on means peers below Fossil 2.29 are refused every S-bearing artifact, and it cannot retire artifacts already written. The specification is provisional; a repository containing one is a disposable test fixture. Settings are local and do not propagate to clones.

showprint the settings and what commits currently emit (default)
enable [--k N]turn emission on, pinning k the first time (chosen by replaying recent history; --k overrides)
disablestop emitting new tree manifests (existing ones are permanent)
retune --k Nre-pin k — always manual. Next commit rewrites every leaf, once; no history changes
analyze [--names PATH]measure the objective for k = 1..12, report the optimum, replay recent history through the real tree builder and delta encoder. Changes nothing. --names analyzes a sorted name list from PATH instead
--k Nthe partition parameter, 1..12
--replay-allanalyze only: replay the WHOLE history instead of the recent window — the full-history evidence a conversion decision wants
-R <path>analyze only: measure a bare store with no checkout (the tip check-in's file set) — the pre-conversion gate for a cloned fossil history; settings actions refuse it

Git & Fossil interop

zit import-git

Import a Git repository into a new zit repository.

zit import-git SOURCE DEST [--submanifest-k K | --no-submanifests]
               [--attribution committer|author] [--default-branch NAME]
               [--refuse-signed] [--provenance]
               [--max-export-payload-bytes BYTES] [--quiet]

Transactional: DEST appears only after export, artifact creation, and ref publication all succeed. Branches are reconstructed from the commit graph, not the exporting refs: the main branch claims its whole first-parent chain to the root; other branches claim shallowest-tip first (name tiebreak) down first parents until reaching a claimed commit — so merges stay on the branch merged into, and history below a fork stays with the branch continuing through it.

--submanifest-k Kpin K (1–12) instead of analyzing; use to match an existing repository's pin so two repos share sub-manifests
--no-submanifestsimport with flat manifests and default settings; mutually exclusive with --submanifest-k
--attribution committer|authorrecord committer (default) or author identities
--default-branch NAMEmake NAME the main branch (default: refs/heads/main if present, else the first head). Also decides which branch keeps the history below every fork. NAME must exist in the source
--refuse-signedrefuse a source with signed commits/tags. Default: signatures are STRIPPED (Fossil has no signature card) and the import proceeds
--provenancekeep .zit-import-map.jsonl (per-object provenance) and refs in DEST; by default both are verified in staging then discarded. The repository is identical either way
--max-export-payload-bytes BYTEScumulative Git payload bound (default 1 TiB); bounds total history, not memory — raise on ExportPayloadLimitExceeded
--quietsuppress progress diagnostics

zit export-git

Mirror a zit repository into a Git repository.

zit export-git SOURCE DEST [--email-domain DOMAIN] [--rebuild] [--quiet]

Mirrors the canonical check-in graph through git fast-import — flat, sub-manifest, and delta-manifest history alike. Incremental and resumable: a local-only mark map in DEST/.zit-export-state makes re-runs export only new check-ins; deleting it costs a full re-export, never a wrong answer. A source holding S-card artifacts is a provisional-format fixture, and the exported mirror is disposable (noted once per run).

--email-domain DOMAINsynthesized identity domain (default zit.invalid)
--rebuildignore prior state; DEST must carry no refs
--quietsuppress the provisional-fixture notice

exporting to fossil

There is no export-fossil verb — and the asymmetry with import-fossil is deliberate.

zit serve --port 8080 &
fossil clone http://127.0.0.1:8080/ out.fossil

zit speaks fossil's sync protocol, so a stock fossil clones straight from zit serve — no converter, no verb. That is the export path, and it is verified on every CI run: scripts/roundtrip-fossil.sh takes a repository fossil → zit → fossil and compares the artifact sets, fossil's own test-integrity verdict, the event counts, and the shun table.

export-git exists because git speaks a different protocol entirely and needs fast-import. Fossil needs nothing.

What this route does not give you is a single .fossil file from one command — it needs a free port, a background process, a readiness wait, and a second binary. That gap is tracked separately.

zit import-fossil

Bring a fossil repository's history into zit — the compatibility consequence is chosen at import time.

zit import-fossil SOURCE DIR (--compatible | --submanifest)
                  [--k N] [--force]

The mode is the decision and it is permanent, so there is no default. SOURCE is a sync URL (for --compatible) or a local store already cloned from the fossil origin (for --submanifest); DIR is created.

--compatible preserves the artifact corpus byte-exact: same hashes, same project code, flat manifests. The result is a first-class member of the origin's sync world — it pulls, pushes, and syncs with the fossil repository it came from. Fossil's sync protocol is the import path; no SQLite is ever read. This is the prerequisite for the eventual fossil → zit → fossil round trip (#73): a later export reproduces the exact fossil, server settings aside.

--submanifest is a rebirth: every check-in re-mints as a sub-manifest tree under a NEW project code — uniform tree history from the first commit, at the price of a permanent break (never syncs with the origin; PGP signatures cannot survive rehashing and are stripped and counted; clusters drop). Before converting, the WHOLE history replays through the real tree builder and a conversion the evidence prices worse than flat is refused without --force. Every (old, new) check-in pair is proven file-set-equivalent before anything is published, counts are reconciled against a full corpus census, publication is staged with an atomic rename as the only commit point (a failed run leaves a sweepable .zit-tmp-*, never a half-repo), and the old→new provenance map ships as a deterministic technote inside the result, so external hash citations stay resolvable by lookup.

--compatibleverbatim corpus; stays in the origin's sync world
--submanifestre-mint as sub-manifest trees; new project code, gated by whole-history replay
--k Nsubmanifest only: override the gate's partition parameter (1..12)
--forcesubmanifest only: convert even when the replay gate refuses

Miscellaneous

zit version

Print the version.

zit version

zit help

Show help — global, or for one command.

zit help [COMMAND | TOPIC]

Machine verbs (plumbing)

Verbs for other programs (quarry's write plane) — wiki, ticket, forum, technote — deliberately absent from the human zit help command list — unlisted, never undocumented: each answers its own --help and zit help <verb>. Machine contract: stdout carries exactly the result line, diagnostics go to stderr, exit codes are exact (0 recorded, 1 user error, 2 usage). All operate on a bare store via -R and serialize on the store's write lock, so concurrent callers queue instead of tearing the log.

zit wiki

Record a wiki artifact (machine verb).

zit wiki commit <title> [-R <repo>] [--user NAME] [--mimetype TYPE] < body

Records a wiki artifact for title with the body read from stdin to EOF. Prints the new artifact hash — nothing else — on stdout. Cards per the artifact-format RFC: D (writer clock), L (title), N (only with --mimetype; readers assume Fossil wiki markup without it), U, W, Z. An empty title or author is refused.

--user NAMEauthor to record (default: $USER); empty refused
--mimetype TYPErecord an N card, e.g. text/x-markdown

zit ticket

Record a ticket-change artifact (machine verb).

zit ticket new [-R <repo>] [--user NAME] field=value ...
zit ticket set <ticket-id> [-R <repo>] [--user NAME] field=value ...

Records a ticket-change artifact: one J card per field=value, sorted ascending (a repeated field is refused, not resolved by position), plus K, D, U, Z. new mints a fresh 40-hex ticket id and prints <ticket-id> <artifact-hash>; set requires an id some stored ticket artifact already carries — a typo'd id exits 1 instead of silently opening a second ticket — and prints the artifact hash.

--user NAMEauthor to record (default: $USER); empty refused

zit forum

Record forum-thread artifacts (machine verb).

zit forum post [-R <repo>] [--user NAME] [--title TITLE]
               [--mimetype TYPE] < body
zit forum reply <post-hash> [-R <repo>] [--user NAME]
                [--mimetype TYPE] < body

post opens a new thread — --title is the thread's H card, the stdin body its W card. reply answers post-hash inside its thread, recording G (the thread root, resolved transitively — replying to a reply still names the root) and I (in-reply-to). An unknown post-hash exits 1; --title on a reply is refused (a forum artifact carries H or I, never both). Prints the new artifact hash on stdout.

Each post declares its own --mimetype (the N card); nothing propagates the thread root's to its replies, and omitting it leaves the card absent so the reader applies its own default.

--user NAMEauthor to record (default: $USER); empty refused
--title TITLEthe thread title (post only; a reply inherits it)
--mimetype TYPEthe body's mimetype (N card), e.g. text/x-markdown; same spelling as wiki commit

zit technote

Record a technote (event) artifact (machine verb).

zit technote add [-R <repo>] [--user NAME] [--comment TEXT]
                 [--tag NAME]... [--mimetype TYPE]
                 [--event EVENT-ID] [--time TIMESTAMP] < body

Body from stdin. --event names an existing technote to record a new edit of (latest wins at read time); omitted, a fresh 40-hex event id is minted. --time sets the E card's own event timestamp (any form the artifact format accepts; default now); the D card is always the writer clock. Prints <event-id> <artifact-hash> on stdout.

--comment is the note's title (the C card), and it is what a timeline renders as the note's name. No later verb can attach it — a note recorded without one shows its event id forever — so give it at the mint or not at all. --tag adds a symbolic tag the note is found by, written T +sym-NAME * exactly as fossil writes it; repeatable, sorted into the ascending run the format requires. Names carrying whitespace, the same name twice, and an empty comment are all refused.

--user NAMEauthor to record (default: $USER); empty refused
--comment TEXTthe note's title (C card); unattachable afterwards, empty refused
--tag NAMEa symbolic tag the note is found by (T +sym-NAME *); repeatable, at most 64
--mimetype TYPEthe body's mimetype (N card)
--event EVENT-IDthe technote to add an edit to
--time TIMESTAMPthe event's own timestamp (E card)

No commands match that filter.