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 and the shortest spelling that reaches each (zit ti → timeline, ticket; try tim or tic). Aliases count as candidates, so zit ch stops at changes/checkout/checkpoint instead of guessing; several spellings of one verb are not an ambiguity (zit remov is rm). An exact verb always wins over the prefix rule, so stat stays the store census. Each verb's help ends with its aliases.
  • 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), options (how options and operands are told apart). A typo'd topic refuses with exit 2 instead of falling back to the command list.
  • End of options — a bare -- ends option parsing for every verb: each argument after it is an operand, even one beginning with a dash. It is the only way to name a path, branch or tag whose name starts with -, since such a token is otherwise read as an option and a ./ prefix is matched literally against the check-in.
  • 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, merge-tree, uv) accept a repository path directly; no working tree required. <path> is the directory holding .zit or the .zit file 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 compound BRANCH:TIMESTAMP (trunk:2026-08-01) scoping it to one branch. zit help names is 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 inside commit and update; a no-op is always already in sync; clone prints 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 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] [--user U [--password P]] [--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.

DIRdefaults to the URL's last path segment (.fossil dropped); the clone then opens a working tree there, as zit open would
--user Uauthenticate as login U from the second round on (sync §6): how a private repository is read. A user@ in URL counts too. The login the peer accepted is stored in DIR, so a later pull or push authenticates unasked
--password PU's password; else ZIT_PASSWORD, else a no-echo prompt on a terminal
--no-openclone only; leave the bare store unopened
--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 | 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=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] [-f|--force] [--dotfiles] [--] <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
-f, --forcea directory expansion also takes files the ignore globs would skip
--dotfilesa directory expansion also takes names beginning with a dot (a dotfile named directly always adds)

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 [-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... DIRwhen 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-runprint would rename OLD -> NEW per file; change nothing
--softrecord the renames without touching the working tree
--hardmove 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 in Fossil's words and column: ADDED (new, will enter the next check-in), EDITED (bytes or mode differ), MISSING (not in the working tree), CONFLICT under a pending merge, which is named as MERGED_WITH. Unchanged files are not listed. Doesn't report untracked files (extras) or deleted-and-forgotten files (diff).

(header)the report leads with Fossil's checkout block — repository, local-root, checkout, parent, tags, comment — with twelve-character names and the weekday in the times; zit changes is the same report without it
-v, --verboseaccepted; the block is printed either way
--classifyaccepted and changes nothing; Fossil's words are the default
--added, --edited, --missing, --conflict, --extraonly 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|--numstat|--by] [-w|-Z] [-c N] [--invert] [-N]
         [--from CHECKIN --to CHECKIN | --checkin V | --branch B] [-R PATH] [PATH...]

Default output is Fossil's: an Index: banner per file, then unified hunks (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
PATH...files or directories that narrow the report
--numstatper-file added and removed line counts, tab-separated (- for binary)
--checkin Vcompare check-in V with its primary parent
--branch Bcompare B's tip with the check-in it forked from
--invertswap the two sides
-Z, --ignore-all-spacethe same as -w
-N, --new-fileaccepted for Fossil parity; an added file's hunks already carry its whole content
--byrender the diff side by side as an HTML page under $TMPDIR, print its path, and open it in the browser when stdout is a terminal

zit extras

List files zit isn't tracking.

zit extras [--dotfiles]

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. Names beginning with a dot are hidden unless asked for.

--dotfilesalso list names beginning with a dot (also settable with zit settings dotfiles on)

zit clean

Delete untracked files.

zit clean [-n|--dry-run] [-f|--force] [--emptydirs] [--dotfiles]

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-runlist what would be deleted; delete nothing
-f, --forcedelete the untracked files
--emptydirsalso remove the directories the deletions leave empty
--dotfilesalso consider names beginning with a dot (also settable with zit settings dotfiles on)

zit addremove

Track new files and forget deleted ones in one pass.

zit addremove [--dry-run] [--dotfiles]

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
--dotfilesalso track names beginning with a dot (also settable with zit settings dotfiles on)

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. Names each file it changes and, after a change, reminds you that zit undo can take you back.

-r, --revision VERSIONrestore from VERSION instead of the current check-in; the checkout stays put and the files show as edited
-n, --dry-runprint would revert PATH per file; write nothing

zit stash

Set working-tree changes aside and restore them later.

zit stash [save [-m MSG] | snapshot [-m MSG] | list | show [ID] | diff [ID]
          | apply [--merge] [ID] | pop [--merge] [ID] | goto [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, pop, and apply name the files they moved.

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
snapshotsave the entry and leave the working tree as it is
diff [ID]the entry's changes as unified hunks against its check-in
goto [ID]update to the entry's check-in, then apply it
--mergeapply/pop: three-way merge into modified files instead of refusing; conflicts get markers and exit 5

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.

accept reads the branch tip once and merges exactly that check-in, so a push arriving mid-accept is not swept into the merge. A caller that checked a tip — CI, a preflight — names it with --expect-tip HASH; if the branch has moved since, the accept is refused with exit status 6, nothing is written, and the message names the tip the branch is at now. The target is read once the same way, and merged onto by hash. A caller that judged the landing against a particular target names it with --expect-target HASH; if the target has moved, the accept is refused with exit status 7 and nothing is written.

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
retarget ID --to Tsay the landing should land on T instead; accept does this for the stack it unblocks
update IDmerge the target into the proposed branch on the store, recording the refresh in the thread; never touches your checkout
--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
--expect-tip HASHaccept: refuse (exit 6) and write nothing unless the branch is at HASH — the tip the caller checked
--expect-target HASHaccept: refuse (exit 7) and write nothing unless the target is at HASH — the target the caller judged the landing against
--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 [-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. Names each restored file on its own UNDO line (REDO for redo) before the confirmation.

-n, --dry-runname 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-runname 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). On success stdout carries check-in HASH on BRANCH; stderr carries the receipt — what went in (a.txt edited, or 3 files: 2 added, 1 edited) and a note: for every binary or CRLF file no glob declared, the thing Fossil would have prompted about. -q silences both.

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.

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
-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-runlist what would be recorded (ADDED/EDITED/DELETED); write nothing
--allow-emptyrecord 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
--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
--no-checks, --no-warningswaive 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"
--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 [-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 twelve-character hashes and weekday-dated times, 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.

CHECKIN omittedthe same as --latest: the current branch's newest check-in, as Fossil's bare update
--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
-n, --dry-runprint the receipt; write nothing
--latestupdate to the current branch's most recent check-in
-v, --verboselist 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
--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] [--baseline B] [--integrate] [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.

Given several NAMEs, --probe answers a stack: each NAME against TARGET with the ones before it already landed, merged in memory, in order. One merges NAME VERDICT line per NAME, conflict PATH lines under the one that stops, and not-reached for every NAME after it. This is not the same as probing each NAME against the one below it. That says nothing about what TARGET changed meanwhile, so it can call a rung clean that stops the landing.

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
NAME omittedthe one other open leaf is merged; several is an error that names them
-n, --dry-runthe same as --probe
--baseline Buse B as the common ancestor instead of the computed merge base
--integrateclose NAME's leaf when the merge is committed

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] [--offset N] [-b BRANCH | -c] [-p PATH] [-u LOGIN] [-r] [-v]
             [-F oneline|medium|full|html] [--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.

(default)Fossil's day-grouped shape (-F medium) with the weekday in the day header and twelve-character hashes; -F oneline is the one-line-per-check-in form for scripts
-n, --limit Nshow only the N most recent entries; 0 means all
--offset Nskip the first N matching entries
-b, --branch Bonly check-ins on branch B
-c, --current-branchonly check-ins on the checkout's branch
-p, --path Ponly check-ins that change P
-u, --for-user Uonly check-ins by U
-r, --reverseoldest first
-v, --verboselist each check-in's changed files
-F, --format Foneline (default), medium, or full; --oneline/--medium/--full are the same
--weekdayshow 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 *.

(line)Fossil's (N) time [hash] comment (user: … tags: …), with the weekday and a twelve-character hash; the checkout's leaf carries *CURRENT*
--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 [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.

zit info

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

zit info [NAME...] [-F text|json] [--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.

Several NAMEs share one repository open, so describing hundreds of check-ins costs one process, not one per name; their text blocks are separated by a blank line. A NAME of - reads names from stdin, one per line. A name that does not resolve is reported on stderr and the rest are still described; the exit status is then 1.

-v, --verboseeach branch with its tip and time
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 NAMEadd / cancel the closed tag on NAME's tip
hide / unhide NAMEadd / cancel the hidden tag on NAME's tip
currentprint the checkout's branch
info NAMEtip, check-in count and root of NAME
-c, --closedlist closed branches only
-tmost recently changed first
-rreverse the order
NAME omittedthe check-in the working tree is on, as Fossil's bare info
-F, --format <f>text (default) or json: one object per line, per name in input order, with name, hash, branch, leaf, parent, merge, cherrypick, backout, plus comment/user/date when an amend overrides them; a name that fails is {"name":…,"error":"not found"|"ambiguous"}

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.

(default)one name per line, as Fossil lists them; --long adds the blob-id, size, perm and class
--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] [-n N] [-l]

Per line, Fossil's columns: the 12-character ID of the introducing check-in, its UTC ISO date, the line number (annotate) or the committing user (blame, praise), 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.

(line)hash date N: text — Fossil's line-number column, twelve-character hash
--fullemit the full artifact ID per line instead of the 10-char prefix — stable as an index key
-n, --limit Nlook back at most N check-ins
-l, --loglist the check-ins consulted before the lines

zit grep

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

zit grep [-n N] [-i] [-c|-l] [-v] [--once] [--no-cache] 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)
-i, --ignore-casematch letters regardless of case
-c, --countprint CHECKIN PATH:COUNT per file instead of the lines
-l, --files-with-matchesprint CHECKIN PATH per file with a match, no lines
-v, --invert-matchselect the lines that do not match
--oncestop at the first matching line of each file

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 merge-tree

Write the tree a merge would produce, without a checkout.

zit merge-tree BRANCH [--into TARGET] (--out DIR | --tar | -F json)
zit merge-tree BRANCH --into TARGET --commit [--resolved DIR] [--delete PATH]...
               -m MESSAGE [--user NAME] --expect-current HASH --expect-other HASH

Merges BRANCH into TARGET in memory, from the repository alone, and writes the result as plain files (--out DIR, which must be empty or absent) or as a gzip-compressed tar on stdout (--tar, no top-level directory). Nothing is written to the repository and no checkout is created, so a CI job can test a branch as merged without a clone, and without anything that could commit or push. It works with -R on a bare store; TARGET then has to be named.

One merge. This is the merge merge --probe judges and landing relies on: the files, contents and modes are what zit merge BRANCH leaves in a checkout of TARGET.

Conflicts are an answer. A pair that does not merge cleanly writes nothing, lists the conflicting paths on stdout one per line, and exits 5, the status landing accept gives the same finding. BRANCH already in TARGET writes TARGET's tree and says so on stderr; two unrelated check-ins are refused.

The tar depends only on the pair: canonical member order, the later of the two check-ins' times, no owner or host. A forge can cache it by the pair of tips.

The merge as JSON. -F json prints one document with the same exit status: the verdict (clean, up-to-date, conflict, unrelated), the base, current (TARGET) and other (BRANCH) check-ins, and every conflicting file. A file has a kind (text, binary, add-add, delete-modify, modify-delete, unreadable), each side's blob hash and mode, null where the path is absent, and for text the regions the merge found: same, current, other, both or conflict, each with half-open line ranges in base, current and other. The regions cover each file in order, so a page shows the merge region by region without merging again, and fetches the bytes with zit artifact. Which side is which is the caller's choice: to show a branch as "ours", name it as TARGET.

Recording the merge. --commit records the merge of BRANCH into TARGET on TARGET's branch, with an answer for each conflicting path: its resolved content at the same path under --resolved DIR, or --delete PATH. A clean merge needs no answers and takes none, so a forge can merge from the web without a checkout. Every other path is the merge's own. It writes nothing when the answers are not exactly the conflicting paths, when one still holds a conflict marker, or when TARGET or BRANCH is no longer the check-in named by --expect-current or --expect-other (exit 5, so a page knows to redraw). The check-in is the one zit merge and zit commit in a checkout would make with the same answers.

--into TARGETthe check-in to merge into (default: the checkout's)
--out DIRwrite the merged files into DIR
--tarwrite them to stdout as a gzip-compressed tar
-F, --format jsonprint the merge as one JSON document
--commitrecord the merge with the given resolutions
--resolved DIRthe conflicting files, resolved, at their paths under DIR
--delete PATHresolve a conflicting path by removing it; repeatable
-m, --message MSGthe check-in comment
--user NAMEthe check-in's author
--expect-current HASHthe TARGET check-in the answers were made against
--expect-other HASHthe BRANCH check-in the answers were made against

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] [--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 front decides who may change which branch and sends only the rule.

The front may also send X-Zit-Divert-Login with the login it authenticated. A push that would join a protected branch is then refused once, saying that the next push keeps the work on branch attempt/LOGIN/HASH; pushed again, it lands there instead, moved by a control artifact the server writes as zit-serve in the same commit, so the protected branch never shows it. The reply carries a message and a pragma zit-diverted, which a stock fossil prints as Server says and zit acts on by pulling the move, and an X-Zit-Diverted response header tells the front which check-in went where. Refusals are remembered for a week in .zit-cache. Without --fronted both headers are ignored and nothing about the server changes.

--port Nlisten on port N (default 8080)
--frontedhonour a trusted front's X-Zit-Protect and X-Zit-Divert-Login headers

zit pull

Fetch new artifacts from a peer.

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

Announces what this repo holds, requests everything it lacks, converges toward the peer's content. With URL omitted the saved remote is used; neither is an error. A private repository is read the way it is written: with --user, a user@ in the URL, or the login a previous authenticated push or pull stored here, the pull authenticates as that login (sync §6); without any of the three it is anonymous.

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

zit uv

List, fetch, send or delete a server's unversioned files.

zit uv list URL [-l]
zit uv pull URL NAME... [-o DIR]
zit uv pull URL --all [-o DIR]
zit uv push URL FILE... [--as NAME] [--mtime SECONDS]
zit uv rm URL NAME... [--mtime SECONDS]

Unversioned files are what a Fossil repository keeps without history: releases, downloads. These commands move them between the server at URL and plain files, with Fossil's own protocol, so the server can be quarry or a stock fossil server. Nothing is stored in this repository: quarry keeps them in its own storage, and sync, push, pull and clone leave them alone.

The newest time wins, as in Fossil. A push or rm no newer than the server's copy is refused with a message rather than silently ignored, and every change is confirmed against the server's catalog afterwards. Reading takes the o capability on the server, changing takes y; a server that shares nothing with this account says so. The login is the repository's, as for push, so run it inside the repository or name it with -R.

A name has no spaces, no . or .. parts and no leading /, and is at most 500 bytes; releases/1.2.3/x.tar.gz is one name.

-llist: each name with its time (seconds since 1970), hash and size; deleted names too, as NAME TIME deleted 0
--allpull: every name the server holds
-o DIRpull: write under DIR (default the current directory)
--as NAMEpush: the name for the one FILE (default its path)
--mtime SECONDSpush, rm: the time to record (default now)
--user U, --password Plog in as U, as for push

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, except that when the server keeps a pushed check-in on an attempt branch (see zit serve) push pulls the move so this repository agrees. Anything the server says is printed as server: .... 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.

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.
--changedlist only settings whose value in effect is not the default
--valueprint the bare value, without the name
autosync pullonlypull before commit/update/merge as usual, never push after a commit
editor CMDthe program commit opens for a comment: after --editor, before $VISUAL/$EDITOR; executable tier, never versioned
main-branch NAMEthe repository's main line (default trunk): the branch a check-in with no branch tag belongs to, and the one a forge treats as main. This repository only — never versioned, no --global — and, as in Fossil, never synced, so a clone reports trunk until it is set there; --changed lists it only when set to something else. An empty NAME resets it; a name no open branch has yet is accepted with a note.

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, apply one from a file, or write one to a file.

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

Fossil's fossil configuration pull|import|merge|export. 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.

Shun rows in a file are added in both modes. Replacing the shun table would un-shun whatever the file does not name, and a shun is never undone here.

export writes an area in that same format, to FILE or to stdout when FILE is absent or - — so a table can be backed up before an import replaces it, and restored by importing the backup. The output carries no timestamp, so two exports of an unchanged table are the same bytes. The user area includes password hashes; treat the file as you would the repository.

zit users

Print the repository's user table.

zit users [--raw] [-R <path>]
zit users contact LOGIN [INFO] [-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.

zit users contact LOGIN prints a user’s contact, and with INFO sets it, as fossil user contact does. It is free text; zit git export takes the email address in it — the part in <…>, or the first word with an @ — as that user’s Git identity.

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] | 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.

showprint 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
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
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 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, or add its new commits to one imported earlier.

zit import-git SOURCE DEST [--into]
               [--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.

--intoadd to DEST instead of creating it: only the upstream commits DEST does not hold are imported, as the check-ins a full import would write. DEST must hold the commit map (.zit-import-map) a first import wrote, from the same source; that import's k, attribution, signature policy and main branch are reused and a contradicting flag is refused. Check-ins already in DEST keep their branch; a head that moved away or vanished upstream is reported and the check-ins it left stay; a tag on a commit DEST already holds is imported as its name alone. A destination committed to since its import, or whose first import met a Fossil mirror's metadata, is refused. On failure DEST is unchanged
--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

zit git

Keep a Git mirror of the repository, and push it.

zit git export [MIRROR] [--autopush URL | --no-autopush] [--push-branch NAME] [--email-domain DOMAIN] [--quiet]
zit git status

zit git export MIRROR exports new check-ins into the Git repository at MIRROR, the same incremental export as export-git, and remembers MIRROR in the repository’s config. A later bare zit git export then does the same from any checkout, a hook, a cron job, or a forge on a bare store with -R, as fossil git export does. With an autopush URL it pushes the main branch there afterwards, fast-forward only, to the remote branch main unless --push-branch names another. Everything given is remembered, and none of it is ever synced to peers.

An author’s address is the email in their contact in the user table (zit users contact), so a Git host links the commit to their account; without one it is login@DOMAIN. An autopush URL may not carry a password, since it is stored: use a Git credential helper or an SSH remote.

zit git status shows the mirror, the autopush URL, how many check-ins have been exported and when, and whether the main branch is current.

--autopush URLpush to URL after each export (remembered)
--no-autopushstop pushing, and forget the URL
--push-branch NAMEthe remote branch the main branch is pushed to (default main)
--email-domain DOMAINaddress for authors with no contact (default zit.invalid)
--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]

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: 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 delta-encoded as it is written — each blob against the base the flat source chose, each manifest against its parent — which keeps the peak on disk low. Run zit pack inside DIR next: the source's own chains are rarely the best bases, and on a large history pack shrinks the store several-fold. 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.

--compatibleverbatim corpus; stays in the origin's sync world
--submanifestre-mint as sub-manifest trees; new project code
--k Nsubmanifest only: pin the partition parameter (1..12) instead of sweeping for it

Miscellaneous

zit ui

Open this repository in a browser (quarry's local mode).

zit ui [QUARRY-ARGS...]

The browser view — timeline graph, side-by-side diffs, file browser, annotate, branches — is quarry's single-repository local mode (quarry RFC 0010). This verb runs quarry ui <checkout root> QUARRY-ARGS... with the terminal inherited and returns its exit status; the binary is $ZIT_QUARRY when set, else quarry on PATH. When it cannot be started the verb says so and exits 1. Meanwhile zit diff --by renders a side-by-side page and zit timeline -F html renders the timeline.

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, attach — 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)

zit attach

Attach a file to a ticket, technote, forum post, or wiki page; list or end attachments (machine verb).

zit attach add  TARGET FILE [--name NAME] [--comment TEXT]
                [--mimetype TYPE] [--user NAME] [-R repo]
zit attach rm   TARGET NAME [--user NAME] [-R repo]
zit attach list [TARGET] [-R repo]

TARGET is a ticket, technote, or forum post's hash (full or abbreviated) or a wiki page's name; it is checked for shape only, as the format specifies — an unresolved target is recorded as a generic reference. add stores FILE as a blob, then an attachment artifact carrying one A NAME TARGET SOURCE card plus C, D, N, U, Z, and prints the artifact's hash, then the blob's, on two lines. FILE may be - for stdin, which needs --name. NAME defaults to FILE's final path component; a directory prefix is always discarded. An empty file is refused, as fossil refuses it.

rm writes an A card with no source for (TARGET, NAME): the association ends under the format's latest-wins rule; nothing leaves the store. list prints the live attachments — the newest A card per name, a sourceless one meaning gone — as NAME SIZE MIMETYPE BLOB DATE USER, with the target as a first column when no TARGET is given. A quarry attachment stub (a small text blob naming external bytes by SHA-256) is listed as external:sha256:… with the size and type it points at, and zit artifact describes such a blob as an attachment-stub.

The setting attachment-size-limit caps what add stores, in bytes, checked before the blob is written; 0 means unlimited. Fossil's default is unlimited; zit's, when the setting is unset, is 1 MiB — a blob in the DAG travels to every clone for ever. zit settings attachment-size-limit N raises it.

--name NAMEthe stored attachment name (default: FILE's final path component); required for stdin
--comment TEXTa comment (C card)
--mimetype TYPEthe attachment's mimetype (N card)
--user NAMEauthor to record (default: $USER); empty refused

No commands match that filter.