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.- Aliases and prefixes —
ci/checkin→commit,co/checkout→update,changes→status,delete/forget/remove→rm,rename→mv,blame/praise→annotate,new→init. Any unambiguous prefix of a command works (zit up,zit tim); an ambiguous one lists its candidates (zit ti→ timeline, ticket). An exact verb always wins over the prefix rule, sostatstays the store census. - Help topics —
zit help TOPICcovers 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 —
0success,1operational failure (not a repo, missing file, refused action),2usage error (unknown option, wrong operands). Every verb's help now states its contract;framesdeparts (3 = nothing new, 4 = cursor invalid) andzit help exit-codeslists 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.zitor the.zitfile itself. The sync family (pull,push,sync,remote) takes it too: a bare mirror needs no working tree to talk to its peer.--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,current/ckout,prev/previous,next(the last three relative to the working tree's check-in; not found on a bare store),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 compoundBRANCH:TIMESTAMP(trunk:2026-08-01) scoping it to one branch.zit help namesis the authoritative statement. - Network narration — every sync-family line names the peer (
pull from URL: …,push to URL: …,sync with URL: …,clone from URL: …), including the autosync insidecommitandupdate; a no-op is alwaysalready in sync;cloneprints the project code; an unreachable peer is named in the error. On a terminal, stderr shows one \r-overwritten progress line per round during a transfer. - Autosync — with the default
autosync onand a saved remote,commitpulls before and pushes after, andupdate/mergepull first. Failures warn and the local operation proceeds;--no-syncskips it once; no saved remote means silence, not a nag. - Sparse cones —
clone/open/updateaccept--cone=PATH(repeatable; files or directories),--cone-not=PATHexclusions, and--cone-file=FILE. The most specific member decides coverage —zit cone test PATHreports 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_EPOCHpins.000for 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-ccookie whose nonce covers that round's own body;zit serveverifies both channels and refuses tampered, replayed, or duplicate logins. - Signatures —
zit commit --signclearsigns 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 asCOMMAND OUTFILE INFILE, anssh-family basename taking the detached path with zit assembling the armor,offdisabling — and is chosen widest-scope-last:$ZIT_PGP_COMMAND, else the repository'spgp-commandsetting, elsegpg --clearsign -o. Settingclearsign onsigns every commit without--sign; both settings are deliberately local and never propagate (seesettings). A signer that fails or alters the manifest refuses the commit rather than storing it unsigned.zit artifactreports signature presence; validity stays your owngpg --verifyoverartifact --raw. - Worktrees —
zit open --store=PATHopens 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/rmrecord which files are tracked; contents are read from the working tree atcommittime.
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, --quiet | do not print the confirmation line |
zit clone
Copy a repository from a URL into a new directory.
zit clone URL [DIR] [--no-open] [--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.
| DIR | defaults to the URL's last path segment (.fossil dropped); the clone then opens a working tree there, as zit open would |
| --no-open | clone only; leave the bare store unopened |
| --once | do not save URL as the default remote |
| --cone=PATH | clone 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=PATH | exclude 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=FILE | read 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 | URL] [--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.
Given a URL instead of a check-in, this is zit clone URL: cloned into a directory named from the URL and opened there.
| --store=PATH | create 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=PATH | open 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=PATH | exclude PATH's subtree from an enclosing --cone member; the most specific member decides |
| --cone-file=FILE | read 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] [-f|--force] [--] <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-run | show what would be added; change nothing |
| -q, --quiet | do not print the added files |
| -f, --force | a directory expansion also takes files the ignore globs would skip |
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-run | show what would be removed; change nothing |
| --hard | also delete the working-tree file from disk |
| -q, --quiet | do not print the removed files |
zit mv
Rename a tracked file.
zit mv [-n] [--soft|--hard] OLD... NEW|DIR
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.
| OLD... DIR | when the last operand is a directory (on disk, a tracked prefix, or .) or there are more than two operands, each OLD moves into it keeping its base name; a tracked directory moves as a unit |
| -n, --dry-run | print would rename OLD -> NEW per file; change nothing |
| --soft | record the renames without touching the working tree |
| --hard | move the files too (the default) |
zit status
Show the working-checkout state.
zit status [-v] [--classify] [--added] [--edited] [--missing] [--conflict] [--extra] [--differ]
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).
| -v, --verbose | the header block: repository, checkout, parent, tags, comment |
| --classify | Fossil's words — ADDED EDITED MISSING CONFLICT EXTRA MERGED_WITH — in an aligned column |
| --added, --edited, --missing, --conflict, --extra | only rows of those kinds; under a pending merge an edited file still carrying conflict markers is conflict; --extra lists the untracked files extras would |
| --differ | --edited plus --extra |
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.
| --brief | name-status only: A added, M content/mode change, D removed; sorted by pathname |
| -w, --ignore-whitespace | lines 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 N | hunk context lines (default 3) |
| --from CHECKIN | older operand of a check-in comparison |
| --to CHECKIN | newer 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 [-n|--dry-run] [-f|--force] [--emptydirs]
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.
| -n, --dry-run | list what would be deleted; delete nothing |
| -f, --force | delete the untracked files |
| --emptydirs | also remove the directories the deletions leave empty |
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-run | report what would change; change nothing |
zit revert
Restore managed files to their checked-in contents.
zit revert [-r VERSION] [-n] [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.
| -r, --revision VERSION | restore from VERSION instead of the current check-in; the checkout stays put and the files show as edited |
| -n, --dry-run | print would revert PATH per file; write nothing |
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.
| save | capture every added/modified/deleted file vs. the current check-in, then restore the check-in's state |
| list | one 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 MSG | label 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 | retarget ID --to TARGET | update 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.
| open | mint the record for a branch (needs --branch and -m) |
| ls | open landings, one per line; --all includes settled ones |
| show ID | the record, its mergeability, the check-ins being proposed, and the thread |
| comment ID | add a reply without changing the state |
| accept ID | merge the branch into its target, then record that it landed; with -R, without any checkout of yours |
| reject ID | settle the record without merging anything |
| retarget ID --to T | say the landing should land on T instead; accept does this for the stack it unblocks |
| update ID | merge the target into the proposed branch on the store, recording the refresh in the thread; never touches your checkout |
| --branch NAME | the branch being proposed (open) |
| --to TARGET | the branch it should land on (default: main) |
| -m, --message M | the message (required on open and comment); on open, its first line is the title and the rest is the prose |
| --all | ls: include accepted and rejected landings |
| --user NAME | author to record (default: $USER); empty is refused |
| --mimetype T | the 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.
| list | one line per precedent: id, path, sizes (the default) |
| show ID | the conflict a precedent fires on, and its resolution |
| forget ID | drop one; the next such merge conflicts normally again |
| forget --all | drop every precedent |
zit undo
Undo the last update or revert.
zit undo [-n|--dry-run]
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.
| -n, --dry-run | name the files (and the checkout record) a undo would restore; change nothing |
zit redo
Reapply an undone change.
zit redo [-n|--dry-run]
Available only immediately after an undo; a new update or revert clears the redo slot.
| -n, --dry-run | name the files (and the checkout record) a redo would restore; change nothing |
Check-ins & branching
zit commit
Record a check-in from your tracked files.
zit commit [-m <message> | -M <file> | --editor <prog>] [-n|--dry-run] [--allow-empty]
[--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 comment comes from -m, from -M FILE, or from an editor: with no -m on a terminal, commit opens $VISUAL (else $EDITOR, else --editor PROG) on a template whose # lines name the branch, the parent and every file this commit captures; those lines are stripped and an empty comment aborts. The template lives in .zit-ci-comment in the checkout root and stays there when the commit fails after the editor, so commit -M .zit-ci-comment retries without retyping. Off a terminal a missing -m is still a usage error.
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 |
| -M, --message-file <file> | read the comment from a file |
| --editor <prog> | compose the comment in prog (default $VISUAL, then $EDITOR, on a terminal) |
| -n, --dry-run | list what would be recorded (ADDED/EDITED/DELETED); write nothing |
| --allow-empty | record a check-in whose file set equals the parent's |
| --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 |
| --close | mark this check-in a closed leaf |
| --allow-fork | commit even though the parent is no longer the branch tip — a deliberate fork |
| --no-sync | skip autosync (the pull before and push after) for this one commit |
| --no-checks, --no-warnings | waive the binary-glob/crlf-glob content gate for this one commit — Fossil's spelling accepted. The gate is opt-in: an unset glob is "no opinion", never "refuse everything" |
| --sign | clearsign the manifest before storing it — see Signatures in the conventions above |
| -q, --quiet | do not print the check-in line |
| --no-submanifest-advice | suppress the sub-manifest drift advisory |
zit update
Materialize a check-in in the working tree.
zit update [-n|--dry-run] [-v] [--no-sync] [--cone=PATH]... [--cone-not=PATH]...
[--cone-file=FILE] [--dense] (CHECKIN | --latest)
CHECKIN resolves like info — full or abbreviated ID, branch, tag, or timestamp; --latest names the current branch's most recent check-in instead. With autosync on and a saved remote, pulls first so CHECKIN resolves against the freshest history. Recoverable with zit undo.
The receipt goes to stderr, so stdout stays clean for scripts: one line per file that moves (UPDATE, ADD, REMOVE; MERGE for a carried local edit, CONFLICT where the merge could not decide), then updated-from/updated-to with their dates, the target's branch and comment, a count of changes, and the reminder that zit undo can take you back. -n prints the same receipt and changes nothing; -v lists unchanged files too.
Uncommitted work is carried across the move, not refused: every managed file is three-way merged (the check-in you are on, your working file, CHECKIN). So when a shared branch tip advances under you, update joins it with your edits on top, and the commit that follows is an ordinary child of that tip rather than a fork. Conflicts get markers in the files and a list on stderr, and update exits 1 so a script notices. A file you changed that CHECKIN deletes, or whose type changed, cannot be merged either way — update names it and refuses, changing nothing. To discard local changes instead of carrying them, run revert first.
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=PATH | materialize and track only PATH's subtree (repeatable); replaces the stored cone |
| --cone-not=PATH | exclude PATH's subtree from an enclosing --cone member; most specific member decides |
| --cone-file=FILE | read cone members from FILE (# comments, ! prefix = exclusion) |
| --dense | drop the stored cone and rematerialize the full tree |
| --no-sync | skip the autosync pull for this invocation |
| -n, --dry-run | print the receipt; write nothing |
| --latest | update to the current branch's most recent check-in |
| -v, --verbose | list unchanged files in the receipt as well |
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 |
| --store | test 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. |
| --quiet | test 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.
update is the reverse merge: the target brought into the proposed branch, on the store, as an ordinary merge check-in on that branch — GitHub’s Update branch, for a reviewer with no clone. It always merges in an ephemeral working area, checkout or not, so your own tree is never touched. A branch already carrying its target is left alone (exit 0); a conflict is refused with exit status 5, naming each path, and nothing is written — resolving it stays a clone’s job. The refresh is recorded as a reply in the record’s thread, so a clone sees it as part of the record. Refreshing a rung is what lets the rungs stacked above it probe and test against the combined state before anything lands.
Stacks (strata): a landing may target another branch that has its own open landing; that pair is a stack, read from the targets rather than stored anywhere. A stacked landing reviews only its own check-ins, because its diff is computed against the branch below it. Stacks land bottom-up: accept refuses a landing whose target has an open landing, was rejected, or already landed — in the last two the merge would succeed and file the work where trunk never sees it — and accepting one retargets whatever was stacked on it, which is the entire restack, since no check-in needs adjusting. Note that rejecting a landing rejects the proposal, not the code beneath a segment stacked on it: that branch descends from the rejected one and still contains its check-ins.
Uncommitted work does not block it. Your working file is the merge's “current” side, so a region only you changed stays yours and a region only NAME changed is applied around it. Where the two collide you get markers, exactly as for any other conflict, and undo restores the uncommitted bytes. A local change that cannot be merged either way — a symlink or directory where a file's bytes are needed — is named and refused before anything is written.
-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.
| --probe | report mergeability and write nothing |
| --into TARGET | probe against TARGET (required with -R) |
| -X, --strategy ours|theirs | resolve content conflicts in favour of one side rather than writing markers |
| --cherrypick NAME | apply only NAME's own change onto the working tree |
| --backout NAME | reverse NAME's change on the working tree |
| --no-sync | skip the autosync pull for this invocation |
| --no-precedent | do 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.
| --raw | NAME is a raw tag name, not a symbolic check-in name; on list, show every tag with its prefix intact |
| --propagate | add only: descendants inherit the tag (mints * rather than +). Hazardous without --raw — an inherited symbolic name drifts to the newest descendant carrying it. |
| VALUE | add 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 TEXT | override the displayed comment |
| --user NAME | override the displayed author |
| --date ISO | override the displayed date (YYYY-MM-DDTHH:MM:SS) |
| --branch NAME | move 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 NAME | mark NAME as containing the defect |
| good NAME | mark NAME as free of the defect |
| next | move to (or re-announce) the current probe check-in |
| status | show the marks, the current probe, and the remaining count |
| reset | end 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.
| --long | add timestamp, user and comment to each entry |
| -n, --limit N | show only the N most recent entries; 0 means all |
| --weekday | show 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] [--offset N] [-b BRANCH | -c] [-p PATH] [-u LOGIN] [-r] [-v]
[-F oneline|medium|full] [--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.
Filters narrow the listing: -b BRANCH, -c (the checkout's branch), -p PATH (check-ins that change PATH against their primary parent), -u LOGIN; -r lists oldest first and --offset N skips the first N matches before -n counts. -F medium prints Fossil's shape — a date header once per day, then time, hash, *CURRENT* on the checkout's check-in, the comment, and (user: … tags: …); -F full adds each check-in's changed files, which -v adds to any format.
| -n, --limit N | show only the N most recent entries; 0 means all |
| --offset N | skip the first N matching entries |
| -b, --branch B | only check-ins on branch B |
| -c, --current-branch | only check-ins on the checkout's branch |
| -p, --path P | only check-ins that change P |
| -u, --for-user U | only check-ins by U |
| -r, --reverse | oldest first |
| -v, --verbose | list each check-in's changed files |
| -F, --format F | oneline (default), medium, or full; --oneline/--medium/--full are the same |
| --weekday | show the UTC day of week beside each date |
| -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 *.
| --all | also list closed leaves, each marked (closed) |
| --no-cache | bypass 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 [list] [-c] [-t] [-r] [--no-cache] zit branch new NAME [BASIS] zit branch (close | reopen | hide | unhide) NAME zit branch current zit branch info NAME
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.
| new NAME [BASIS] | record an empty check-in that starts NAME at the checkout's check-in (BASIS must be that check-in — update first) |
| close / reopen NAME | add / cancel the closed tag on NAME's tip |
| hide / unhide NAME | add / cancel the hidden tag on NAME's tip |
| current | print the checkout's branch |
| info NAME | tip, check-in count and root of NAME |
| -c, --closed | list closed branches only |
| -t | most recently changed first |
| -r | reverse the order |
| --long | add size, permission and text/binary columns |
| --size | add size and permission only; reads no blob content |
| --under=PATH | list 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.
| --raw | write 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=CURSOR | emit only artifacts appended after CURSOR |
| --verify=TOKEN | validate the consumed prefix before emitting |
| --structural | emit 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 crosslink
Stream derived per-check-in records (indexer feed).
zit crosslink [--no-cache]
Every check-in’s derived record, in persisted order: identity, timestamp, author, comment, tag operations, and the files changed relative to the primary parent — the answers an indexer otherwise re-derives by parsing and expanding every manifest itself. The first line names the stream format (crosslink 1); each record is a checkin header, an optional user line, a length-framed comment, tag and file (A|M|D) lines, and end. Free-text tokens are card-escaped exactly as an artifact carries them, so every line is newline-safe. A rename reads as a delete plus an add.
A record may say files unknown instead: a tree (sub-manifest) check-in, or a parent the store does not hold — the sqlite corpus carries exactly one, a 2009 check-in whose parent is genuinely absent. Unknown is deliberate; an empty change set is a claim, and the verb never claims what it did not compute.
Records are served from the derived cache when it provably covers the whole log (the same key-and-shun-fingerprint discipline every read verb follows) and derived from the log when it does not; --no-cache forces the derivation, and the two outputs are byte-identical — pinned by test.
| --no-cache | derive from the log, bypassing the cache |
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.
| --full | emit 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 N | stop after N matching versions; 0 means all |
| -S, --pickaxe | report check-ins where the occurrence count changed |
| -G, --diff-match | report 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 DIR | top-level directory inside the archive |
| --include GLOBS | archive only files matching this glob list |
| --exclude GLOBS | archive no file matching this glob list; a file matching both lists is excluded |
| --list | name the members instead of writing an archive |
| --no-cache | bypass 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 DIR | top-level directory inside the archive |
| --include GLOBS | archive only files matching this glob list |
| --exclude GLOBS | archive no file matching this glob list; exclude wins |
| --list | name the members instead of writing an archive |
| --no-cache | bypass the derived-state cache and re-derive |
Sync & serving
zit serve
Serve this repository over HTTP for sync.
zit serve [--port N] [--fronted]
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. With --fronted, a trusted proxy on this machine is the only client, and its per-request X-Zit-Protect header — a glob list of branches whose membership that push may not change — is honoured: a pushed check-in that would join one, or a tag that would move a held check-in in or out of one, refuses the whole round with the branch named, on the same error card a content-policy refusal uses. The server never learns who pushed; the front decides that and sends only the rule. Without --fronted the header is ignored and nothing about the server changes.
| --port N | listen on port N (default 8080) |
| --fronted | honour a trusted front's X-Zit-Protect header (zit#151) |
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 U | authenticate as login U (sync §6) |
| --password P | U'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 U | authenticate as login U (sync §6) |
| --password P | U'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.
| --unset | clear 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.
| --global | read or write the operator-scope tier instead of this repository's |
| autosync on|off | pull 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 LIST | comma- 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|off | clearsign every commit, as though --sign had been passed (default off) — fossil's setting of the same name |
| pgp-command CMD | the 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.
| --artifacts | list stored artifact hashes instead, one per line, ascending |
| --type=KIND | with --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-elided | on 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.
| --raw | include 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.
| --yes | accept the declaration as shown; commits then run it |
| --revoke | withdraw 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-run | report the plan and exit 0 without changing anything |
| --explain | list every artifact the plan removes, not only the counts |
| --reason TEXT | record why, shown by purge list |
| --force | actually 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 TEXT | record a single-line note (shown by --list) |
| --defer | record the shun; leave the bytes until zit pack. Reads answer identically, but the bytes stay reconstructible on disk until the rewrite. Still needs --force. |
| --force | actually record and remove |
| --list | list 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.
| show | print the stored claim and whether it still validates (default) |
| build [CHECKIN] | build/rebuild, anchored at CHECKIN (full ID) or the newest check-in |
| verify | re-derive everything claimed from real history and compare byte-for-byte |
| drop | delete 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] | convert SRC DEST [--k N]]
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.
| show | print the settings and what commits currently emit (default) |
| enable [--k N] | turn emission on for new check-ins, pinning k the first time (chosen by replaying recent history; --k overrides). Existing check-ins stay flat, and from the next commit peers below Fossil 2.29 — every released fossil — receive nothing new |
| disable | stop emitting new tree manifests (existing ones are permanent) |
| retune --k N | re-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 |
| convert SRC DEST [--k N] | re-mint the whole history of the flat zit store SRC as sub-manifest trees in a new repository DEST under a new project code — import-fossil SRC DEST --submanifest by its proper name. SRC is never modified; DEST cannot sync with SRC's peers; every pair is proven file-set-equivalent before DEST is published; run zit pack in DEST afterwards |
| --k N | the partition parameter, 1..12 |
| --replay-all | analyze 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 K | pin K (1–12) instead of analyzing; use to match an existing repository's pin so two repos share sub-manifests |
| --no-submanifests | import with flat manifests and default settings; mutually exclusive with --submanifest-k |
| --attribution committer|author | record committer (default) or author identities |
| --default-branch NAME | make 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-signed | refuse a source with signed commits/tags. Default: signatures are STRIPPED (Fossil has no signature card) and the import proceeds |
| --provenance | keep .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 BYTES | cumulative Git payload bound (default 1 TiB); bounds total history, not memory — raise on ExportPayloadLimitExceeded |
| --quiet | suppress 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 DOMAIN | synthesized identity domain (default zit.invalid) |
| --rebuild | ignore prior state; DEST must carry no refs |
| --quiet | suppress 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]
The mode is the decision and it is permanent, so there is no default. SOURCE is the fossil's sync URL; --submanifest also accepts a flat zit store already cloned from it (the same operation as zit submanifest convert). 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). The partition parameter comes from --k, else from the §6.3 sweep over the source tip's names; each check-in's tree is then built from its parent's. 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.
The converted store is written full-form, with no deltas: run zit pack inside DIR afterwards to delta-encode it. zit submanifest enable on the flat repository is not a substitute — it turns emission on for new check-ins only and never re-mints existing history. Each phase reports its wall time on stderr as it completes, so a long conversion can be watched from outside. There is no replay gate: the source is never modified, so the evidence that matters is the converted store, whose size the summary reports against the flat source's; zit submanifest analyze --replay-all models it in advance for anyone who wants that first.
| --compatible | verbatim corpus; stays in the origin's sync world |
| --submanifest | re-mint as sub-manifest trees; new project code |
| --k N | submanifest only: pin the partition parameter (1..12) instead of sweeping for it |
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 NAME | author to record (default: $USER); empty refused |
| --mimetype TYPE | record 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 NAME | author 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 NAME | author to record (default: $USER); empty refused |
| --title TITLE | the thread title (post only; a reply inherits it) |
| --mimetype TYPE | the 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 NAME | author to record (default: $USER); empty refused |
| --comment TEXT | the note's title (C card); unattachable afterwards, empty refused |
| --tag NAME | a symbolic tag the note is found by (T +sym-NAME *); repeatable, at most 64 |
| --mimetype TYPE | the body's mimetype (N card) |
| --event EVENT-ID | the technote to add an edit to |
| --time TIMESTAMP | the event's own timestamp (E card) |
No commands match that filter.