Artifact fee01a0404b06de352f63ec4fb1da9ca62e1e7916f3b27eea58f0a4079a7160b:
- File
src/main.zig
— part of check-in
[0910bc00f5]
at
2026-09-17 13:39:56
on branch trunk
— log: open once, on the zero-parse image
log reached past the open funnel and called Repository.open with the default options, which is the full record replay meant for write verbs, and did so twice for a symbolic name. On pkgsrc that was 0.94s before the first row; on the image path it is 0.05-0.10s, output byte-identical on every route. The open mode has no default any more: every direct caller declares it, so the next verb written past the funnel cannot inherit the expensive one silently. An e2e runs every log route under the derive-footprint seam and fails if any of them replays a record. (user: dmestas, size: 262368) [annotate] [blame] [check-ins using]
//! zit — command-line entry point. //! //! The heavy lifting lives in the `zit` library module (src/root.zig and the //! per-RFC / storage modules it re-exports); this file is only the CLI surface. //! `main` wires I/O and flushes; `run` is a pure-ish dispatcher over an //! injected `std.Io` that returns an exit code, so every verb is exercisable //! without a subprocess (design-seams rule). No `std.http` appears here. const std = @import("std"); const Io = std.Io; const File = Io.File; const zit = @import("zit"); const cli = @import("zit").cli; const merge3 = @import("merge3.zig"); const extras = @import("extras.zig"); // The store layers and the repository facade are all part of the one `zit` // library module (root.zig re-exports them), so the CLU imports a single // dependency. `Repository` is the deep read-path facade; `zit.repoview` is its // derived-view collaborator, reached directly only by the pack verify test. const logstore = @import("zit").logstore; const repoview = @import("zit").repoview; const storage = @import("zit").storage; const update = @import("zit").update; const checkout = @import("zit").checkout; const bisect = @import("zit").bisect; const Repository = @import("zit").Repository; const serve_mod = @import("serve.zig"); // The clone verb's two CLI-layer seams: `transport` is the sole HTTP/IO module // on the client side (sync §4), `sync_driver` the sans-IO round loop that pumps // `sync.Client` to §3.7 convergence over it. `sync.zig` itself stays sans-IO. const transport = @import("transport.zig"); const sync_driver = @import("sync_driver.zig"); const cmd_sync = @import("cmd/sync.zig"); const cmd_tracking = @import("cmd/tracking.zig"); const cmd_history = @import("cmd/history.zig"); const cmd_purge = @import("cmd/purge.zig"); const cmd_checkout = @import("cmd/checkout.zig"); const cmd_stash = @import("cmd/stash.zig"); const cmd_precedent = @import("cmd/precedent.zig"); const cmd_verify = @import("cmd/verify.zig"); const cmd_records = @import("cmd/records.zig"); const cmd_landing = @import("cmd/landing.zig"); const cmd_archive = @import("cmd/archive.zig"); const repostate = zit.repostate; const managed = zit.managed; // The artifact-format vocabulary `commit` assembles a check-in from: the // `Manifest`/card structs it populates, `create` (serializes + computes the Z // self-check), `parse` (reads the parent F-set), and `artifactName` (names each // blob and the manifest). Reached through the same `zit` module cli.zig uses, so // the `FCard` values flow between them without a type mismatch. const artifact = zit.artifact; const git_import_cli = @import("git/git_import_cli.zig"); const fossil_import_cli = @import("fossil/fossil_import_cli.zig"); const git_export_cli = @import("git/git_export_cli.zig"); pub fn main(init: std.process.Init) !void { const gpa = init.gpa; const arena = init.arena.allocator(); const io = init.io; // `toSlice` yields sentinel-terminated argv; widen to plain `[]const u8` // slices so the verb runner never depends on NUL termination (a slice of // `[:0]const u8` does not coerce to a slice of `[]const u8`). const argv_z = try init.minimal.args.toSlice(arena); const args = try arena.alloc([]const u8, argv_z.len); for (argv_z, args) |src, *dst| dst.* = src; // Streaming (not positional) writers: `Io.File.Writer.init` defaults to // `.positional`, which writes via `pwrite` from an internal `pos` that // starts at 0 — ignoring the OS file offset zit inherited on stdout/stderr. // That silently clobbers `zit … >> logfile` (O_APPEND) and grouped/shared-fd // redirections (`{ echo a; zit …; } > log`), overwriting bytes another // writer already put there. Streaming mode uses plain `write`/`writev`, // honoring the inherited offset and O_APPEND — the pipe-safe, composable // behavior a process's standard streams must have (contracts/rm.md B-rm-14). var out_buf: [4096]u8 = undefined; var out_writer: Io.File.Writer = .initStreaming(.stdout(), io, &out_buf); var err_buf: [4096]u8 = undefined; var err_writer: Io.File.Writer = .initStreaming(.stderr(), io, &err_buf); const streams: cli.Streams = .{ .out = &out_writer.interface, .err = &err_writer.interface }; // The ambient clock/environment values the runners need, read here — the // single place time and environment enter the CLI (design-seams rule) — and // threaded as plain values so every verb runner stays deterministic under // test. `$ZIT_PASSWORD` (sync §6.3) is a no-persistence password source for // `push`, read at this one seam like `$USER`. const amb: cli.Ambient = .{ .now_epoch = commitEpoch(io, init.environ_map), .now_ms = commitEpochMs(io, init.environ_map), .user_env = init.environ_map.get("USER") orelse init.environ_map.get("LOGNAME"), .push_password = init.environ_map.get("ZIT_PASSWORD"), .environ = init.minimal.environ, }; // zit#188 — the file-history index is an accelerator; this turns it off so // the same query is answered by reconstruction, which is how the // differential test proves the two agree. cmd_history.no_file_index = init.environ_map.get("ZIT_NO_FILE_INDEX") != null; const code = run(gpa, io, arena, args, streams, amb); // Evidence seam (zit#172). A listing renders comment, user, branch and // tags from the derived state; a manifest read per row is the regression // this reports, and `src/e2e.zig` asserts the count stays flat as history // grows. Off unless asked for, and it changes nothing else. if (init.environ_map.get("ZIT_FILEINDEX_WALK") != null) { streams.err.print("fileindex-walk: {d}\n", .{zit.repoview.change_set_visits}) catch {}; streams.err.print("fileindex-subs: {d}\n", .{zit.repoview.tree_subs_resolved}) catch {}; if (cmd_history.file_index_unknown) |n| { streams.err.print("fileindex-unknown: {d}\n", .{n}) catch {}; } else { streams.err.writeAll("fileindex-unknown: none\n") catch {}; } } if (init.environ_map.get("ZIT_MANIFEST_READS") != null) { streams.err.print("manifest-reads: {d}\n", .{Repository.manifest_reads}) catch {}; } // Evidence seam (zit#172, clone memory): the persist drain's peak deferred // waiting set — what a streaming persist would have to keep resident or // spill. The streaming decision is gated on this number being measured. // Evidence seam (zit#182, derive memory): what the derive's record body is // made of and what the state retains, so a rung of the zit#182 ladder is // bought or declined on measured composition rather than a guess. if (init.environ_map.get("ZIT_DERIVE_FOOTPRINT") != null) { const rv = zit.repoview; const rs = zit.repostate; streams.err.print( "derive-footprint: records={d} body-peak-capacity={d} hash={d} parent={d} tag={d} comment={d} user={d} path={d} filehash={d} state-records={d} state-comment={d} state-user={d}\n", .{ rv.derive_records, rv.derive_body_peak_capacity, rv.derive_hash_bytes, rv.derive_parent_bytes, rv.derive_tag_bytes, rv.derive_comment_bytes, rv.derive_user_bytes, rv.derive_path_bytes, rv.derive_filehash_bytes, rs.state_records, rs.state_comment_bytes, rs.state_user_bytes, }, ) catch {}; } if (init.environ_map.get("ZIT_PERSIST_WAITING") != null) { streams.err.print("persist-waiting-peak: {d} frames, {d} bytes\n", .{ cmd_sync.persist_waiting_peak, cmd_sync.persist_waiting_peak_bytes, }) catch {}; } if (init.environ_map.get("ZIT_SYNC_STATS") != null) { streams.err.print("sync-stats: {d} rounds, sent {d} bytes, received {d} bytes; cluster index {d} clusters, walked {d} rids\n", .{ cmd_sync.sync_rounds, cmd_sync.sync_sent_bytes, cmd_sync.sync_received_bytes, cmd_sync.cluster_index_names, zit.clusterindex.walk_rids, }) catch {}; } streams.out.flush() catch {}; streams.err.flush() catch {}; if (code != .ok) std.process.exit(@intFromEnum(code)); } /// Each verb's `--help` block, keyed by `cli.Verb` — the one place a verb is /// paired with its help text. `zit help <verb>` and `zit <verb> --help` both /// read this, so they teach identical text (DX D4). Verbs with no dedicated /// help (`version`, `help`) and the `unknown` catch-all map to `null`; the /// caller then falls back to `top_usage`. Keyed by the enum, so a new verb /// forces a matching entry here at compile time — no help topic can silently /// drop. const verb_help = std.EnumArray(cli.Verb, ?[]const u8).init(.{ .init = init_help, .add = add_help, .rm = rm_help, .mv = mv_help, .status = status_help, .stat = stat_help, .diff = diff_help, .extras = extras_help, .clean = clean_help, .addremove = addremove_help, .commit = commit_help, .log = log_help, .open = open_help, .update = update_help, .merge = merge_help, .revert = revert_help, .undo = undo_help, .redo = redo_help, .leaves = leaves_help, .ui = ui_help, .timeline = timeline_help, .branch = branch_help, .tag = tag_help, .amend = amend_help, .info = info_help, .ls = ls_help, .cat = cat_help, .artifact = artifact_help, .annotate = annotate_help, .grep = grep_help, .frames = frames_help, .crosslink = crosslink_help, .bisect = bisect_help, .serve = serve_help, .clone = clone_help, .pull = pull_help, .push = push_help, .sync = sync_help, .remote = remote_help, .settings = settings_help, .pack = pack_help, .submanifest = submanifest_help, .cone = cone_help, .worktrees = worktrees_help, .trust = trust_help, .config = config_help, .users = users_help, .tarball = tarball_help, .zip = zip_help, .checkpoint = checkpoint_help, .stash = stash_help, .precedent = precedent_help, .import_git = import_git_help, .import_fossil = import_fossil_help, .export_git = export_git_help, .shun = shun_help, .purge = purge_help, .verify = verify_help, .wiki = wiki_help, .ticket = ticket_help, .forum = forum_help, .landing = landing_help, .technote = technote_help, .attach = attach_help, .version = null, .help = null, .unknown = null, }); test "help: no verb help carries a leaked verb-list entry" { // `export_git_help` once concatenated a line lifted from the verb list // straight into its usage block, so `zit export-git --help` printed // " \\ import-fossil bring a fossil repository's history into zit" // as its second line, backslash included. Nothing caught it: the verbatim // help pins cover `merge` and `stash` only, and a pin per verb would not // have generalized anyway. // // The smell is precise. Help text is written as a multiline string whose // `\\` prefixes are consumed by the compiler, so a rendered help line can // never legitimately begin with a backslash; one that does was pasted in // by hand from a source listing. Checking the SHAPE catches the whole // class for every verb, including verbs added later. inline for (comptime std.enums.values(cli.Verb)) |v| { // No `continue` here: `inline for` makes it comptime control flow, and // the lookup below is a runtime value. if (verb_help.get(v)) |text| { var lines = std.mem.splitScalar(u8, text, '\n'); while (lines.next()) |line| { const body = std.mem.trimStart(u8, line, " "); if (body.len != 0 and body[0] == '\\') { std.debug.print( "\nzit {s} help has a line starting with a backslash, which means a " ++ "source listing was pasted into it:\n {s}\n", .{ @tagName(v), line }, ); return error.HelpTextLeak; } } } } } /// An ambiguous prefix names its candidates and, so the next attempt lands, /// the shortest spelling that reaches each one (zit#170): /// `zit: ambiguous command 'st' (stash, stat, status); try stas, stat, or statu`. fn ambiguousVerb(streams: cli.Streams, who: []const u8, typed: []const u8, a: cli.Ambiguity) cli.Exit { const e = streams.err; e.print("{s}: ambiguous command '{s}' (", .{ who, typed }) catch {}; for (a.names[0..a.n], 0..) |n, i| e.print("{s}{s}", .{ if (i == 0) "" else ", ", n }) catch {}; e.writeAll(if (a.more) ", …); try " else "); try ") catch {}; for (a.names[0..a.n], 0..) |n, i| { const sep: []const u8 = if (i == 0) "" else if (i + 1 == a.n and !a.more) (if (a.n == 2) " or " else ", or ") else ", "; e.print("{s}{s}", .{ sep, cli.shortestPrefix(n) }) catch {}; } e.writeAll(if (a.more) ", …\n" else "\n") catch {}; return .usage; } /// Verb help ends with the spellings that reach the same verb (zit#170). fn withAliases(comptime v: cli.Verb, comptime text: []const u8) []const u8 { comptime { var line: []const u8 = ""; for (cli.verb_aliases) |a| { if (a.verb != v) continue; line = line ++ (if (line.len == 0) "\naliases: " else ", ") ++ a.alias; } return if (line.len == 0) text else text ++ line ++ "\n"; } } /// The `--help` block for a verb named on the command line, or `null` when the /// verb carries no dedicated help (or the name is unknown) — the caller then /// falls back to `top_usage`. Resolves the name through `cli.verbOf` so help /// topics track the dispatched verb set exactly. fn helpFor(name: []const u8) ?[]const u8 { if (topicFor(name)) |t| return t; return switch (cli.resolveVerb(name)) { .verb => |v| verb_help.get(v), else => null, }; } /// `zit help TOPIC` (#68): non-verb subjects — cross-cutting concepts a verb /// help points at instead of restating. Names are exact. fn topicFor(name: []const u8) ?[]const u8 { const topics = [_]struct { name: []const u8, text: []const u8 }{ .{ .name = "names", .text = names_topic_help }, .{ .name = "sparse", .text = sparse_topic_help }, .{ .name = "repo", .text = repo_topic_help }, .{ .name = "exit-codes", .text = exit_codes_topic_help }, }; for (topics) |t| { if (std.mem.eql(u8, name, t.name)) return t.text; } return null; } /// Dispatches on the verb (a total switch) and returns the process exit code. /// Never errors: every failure is rendered as an educating diagnostic by the /// verb and folded into the returned `Exit`. fn run( gpa: std.mem.Allocator, io: std.Io, arena: std.mem.Allocator, args: []const []const u8, streams: cli.Streams, amb: cli.Ambient, ) cli.Exit { const verb = if (args.len >= 2) args[1] else "help"; // `-R`/`--repo <path>` is a GLOBAL option: it is lifted out here, before the // verb's own parser runs, so ten read verbs share one implementation instead // of each re-declaring the flag. The verb then sees exactly the arguments it // has always seen, and the resolved store rides along in `Ambient`. // Bare `zit` carries no verb and therefore no verb arguments; `args[2..]` // would be out of bounds on it. const raw_args: []const []const u8 = if (args.len >= 2) args[2..] else &.{}; // zit#156: exact verb or alias, else an unambiguous prefix; an ambiguous // one names its candidates and stops here. const resolved: cli.Verb = switch (cli.resolveVerb(verb)) { .verb => |v| v, .ambiguous => |a| return ambiguousVerb(streams, "zit", verb, a), .unknown => .unknown, }; const global = cli.parseGlobalArgs(arena, raw_args) catch return stateOom(streams, verb); const verb_args = switch (global) { .ok => |g| blk: { if (g.repo != null and !cli.acceptsRepoOverride(resolved)) return cli.usageError(streams, verb, .repo_unsupported, "-R/--repo"); break :blk g.args; }, // A dangling `-R` is a dangling value option, phrased the way every // other value option in the CLI phrases one. .missing_value => |tok| return cli.usageError(streams, verb, .unknown_option, tok), .repeated_option => |tok| return cli.usageError(streams, verb, .repeated_option, tok), .empty_value => |tok| return cli.usageError(streams, verb, .empty_value, tok), }; const amb_r: cli.Ambient = .{ .now_epoch = amb.now_epoch, .now_ms = amb.now_ms, .user_env = amb.user_env, .push_password = amb.push_password, .environ = amb.environ, .repo_override = switch (global) { .ok => |g| g.repo, else => unreachable, // every other arm returned above }, }; switch (resolved) { .init => return runInit(gpa, io, arena, verb_args, streams), .add => return cmd_tracking.runAdd(gpa, io, arena, verb_args, streams, amb), .rm => return cmd_tracking.runRm(gpa, io, arena, verb_args, streams), .mv => return cmd_tracking.runMv(gpa, io, arena, verb_args, streams), .status => return cmd_tracking.runStatus(gpa, io, arena, verb_args, streams, !std.mem.eql(u8, verb, "changes"), amb), .stat => return cmd_history.runStat(gpa, io, arena, verb_args, streams, amb_r), .diff => return cmd_tracking.runDiff(gpa, io, arena, verb_args, streams, amb_r), .extras => return cmd_tracking.runExtras(gpa, io, arena, verb_args, streams, amb), .clean => return cmd_tracking.runClean(gpa, io, arena, verb_args, streams, amb), .addremove => return cmd_tracking.runAddremove(gpa, io, arena, verb_args, streams, amb), .commit => return cmd_checkout.runCommit(gpa, io, arena, verb_args, streams, amb_r), .log => return cmd_history.runLog(gpa, io, arena, verb_args, streams, amb_r), .open => return cmd_sync.runOpen(gpa, io, arena, verb_args, streams, amb), .update => return cmd_checkout.runUpdate(gpa, io, arena, verb_args, streams, amb), .merge => return cmd_checkout.runMerge(gpa, io, arena, verb_args, streams, amb_r), .revert => return cmd_checkout.runRevert(gpa, io, arena, verb_args, streams), .undo => return cmd_checkout.runUndo(gpa, io, arena, verb_args, streams), .redo => return cmd_checkout.runRedo(gpa, io, arena, verb_args, streams), .leaves => return cmd_history.runLeaves(gpa, io, arena, verb_args, streams, amb_r), .ui => return runUi(gpa, io, arena, verb_args, streams, amb_r), .timeline => return cmd_history.runTimeline(gpa, io, arena, verb_args, streams, amb_r), .branch => return cmd_history.runBranch(gpa, io, arena, verb_args, streams, amb_r), .tag => return cmd_history.runTag(gpa, io, arena, verb_args, streams, amb_r), .amend => return cmd_checkout.runAmend(gpa, io, arena, verb_args, streams, amb_r), .info => return cmd_history.runInfo(gpa, io, arena, verb_args, streams, amb_r), .ls => return cmd_history.runLs(gpa, io, arena, verb_args, streams, amb_r), .cat => return cmd_history.runCat(gpa, io, arena, verb_args, streams, amb_r), .artifact => return cmd_history.runArtifact(gpa, io, arena, verb_args, streams, amb_r), .annotate => return cmd_history.runAnnotate(gpa, io, arena, verb_args, streams, amb_r, std.mem.eql(u8, verb, "blame") or std.mem.eql(u8, verb, "praise")), .grep => return cmd_history.runGrep(gpa, io, arena, verb_args, streams, amb_r), .frames => return cmd_history.runFrames(gpa, io, arena, verb_args, streams, amb_r), .crosslink => return cmd_history.runCrosslink(gpa, io, arena, verb_args, streams, amb_r), .bisect => return cmd_checkout.runBisect(gpa, io, arena, verb_args, streams), .serve => return cmd_sync.runServe(gpa, io, arena, verb_args, streams, amb_r), .clone => return cmd_sync.runClone(gpa, io, arena, verb_args, streams), .import_git => return runImportGit(gpa, io, verb_args, streams, amb_r), .import_fossil => return runImportFossil(gpa, io, arena, verb_args, streams), .export_git => return runExportGit(gpa, io, verb_args, streams), .pull => return cmd_sync.runPull(gpa, io, arena, verb_args, streams, amb_r.repo_override), .push => return cmd_sync.runPush(gpa, io, arena, verb_args, streams, amb_r), .sync => return cmd_sync.runSync(gpa, io, arena, verb_args, streams, amb_r), .remote => return cmd_sync.runRemote(gpa, io, arena, verb_args, streams, amb_r.repo_override), .settings => return cmd_sync.runSettings(gpa, io, arena, verb_args, streams, amb_r), .pack => return cmd_sync.runPack(gpa, io, arena, verb_args, streams), .submanifest => { // `convert SRC DEST` is the converter under the verb that owns // sub-manifests (zit#168); it needs no checkout, so it is routed // before the settings actions resolve one. switch (cli.parseSubmanifestArgs(verb_args)) { .ok => |p| if (p.action == .convert and !p.help) { const src = p.src orelse { streams.err.writeAll("zit submanifest convert: wants SRC (a flat zit store) and DEST (a directory to create)\n") catch {}; streams.err.writeAll("try 'zit submanifest --help'\n") catch {}; return .usage; }; const dest = p.dest orelse { streams.err.writeAll("zit submanifest convert: wants DEST, the directory to create\n") catch {}; streams.err.writeAll("try 'zit submanifest --help'\n") catch {}; return .usage; }; if (p.names_path != null or p.replay_all) { streams.err.writeAll("zit submanifest convert: --names and --replay-all are 'analyze' options\n") catch {}; return .usage; } return fossil_import_cli.runSubmanifestImport(gpa, io, arena, src, dest, p.k, "submanifest convert", streams); }, else => {}, } return cmd_checkout.runSubmanifest(gpa, io, arena, verb_args, streams, amb_r); }, .cone => return cmd_checkout.runCone(gpa, io, arena, verb_args, streams, amb), .worktrees => return cmd_checkout.runWorktrees(gpa, io, arena, verb_args, streams, amb_r), .trust => return cmd_checkout.runTrust(gpa, io, arena, verb_args, streams, amb), .config => return cmd_sync.runConfig(gpa, io, arena, verb_args, streams, amb_r), .users => return cmd_history.runUsers(gpa, io, arena, verb_args, streams, amb_r), .tarball => return cmd_archive.runTarball(gpa, io, arena, verb_args, streams, amb_r), .zip => return cmd_archive.runZip(gpa, io, arena, verb_args, streams, amb_r), .checkpoint => return cmd_checkout.runCheckpoint(gpa, io, arena, verb_args, streams), .stash => return cmd_stash.runStash(gpa, io, arena, verb_args, streams, amb), .precedent => return cmd_precedent.runPrecedent(gpa, io, arena, verb_args, streams), .shun => return cmd_sync.runShun(gpa, io, arena, verb_args, streams, amb_r), .purge => return cmd_purge.runPurge(gpa, io, arena, verb_args, streams, amb_r), .verify => return cmd_verify.runVerify(gpa, io, arena, verb_args, streams, amb_r), .wiki => return cmd_records.runWiki(gpa, io, arena, verb_args, streams, amb_r), .ticket => return cmd_records.runTicket(gpa, io, arena, verb_args, streams, amb_r), .forum => return cmd_records.runForum(gpa, io, arena, verb_args, streams, amb_r), .landing => return cmd_landing.runLanding(gpa, io, arena, verb_args, streams, amb_r), .technote => return cmd_records.runTechnote(gpa, io, arena, verb_args, streams, amb_r), .attach => return cmd_records.runAttach(gpa, io, arena, verb_args, streams, amb_r), .version => { streams.out.writeAll("zit 0.3.18 (targets Fossil RFC rev " ++ zit.rfc_revision ++ ")\n") catch {}; return .ok; }, .help => { // `zit help <verb>` teaches that verb's own help — the same text as // `zit <verb> --help` (DX D4). Bare `zit`, `zit help`, and // `zit --help` list the commands. An unknown or help-less topic // (e.g. `version`) falls back to the command list, which now names // every real verb including `status`, so nothing reads as absent. if (args.len >= 3) { if (helpFor(args[2])) |topic_help| { streams.out.writeAll(topic_help) catch {}; return .ok; } // A REAL verb with no dedicated help (e.g. `version`) keeps // the command-list fallback below; only a token that is // neither a verb nor a topic is refused (#68): a typo // deserves a diagnostic in a CLI that refuses everywhere else. if (cli.resolveVerb(args[2]) == .ambiguous) { return ambiguousVerb(streams, "zit help", args[2], cli.resolveVerb(args[2]).ambiguous); } if (cli.resolveVerb(args[2]) != .verb) { streams.err.print("zit help: no command or topic named '{s}'\n", .{args[2]}) catch {}; streams.err.writeAll("run 'zit help' for the command and topic list\n") catch {}; return .usage; } } streams.out.writeAll(top_usage) catch {}; return .ok; }, .unknown => { // A git muscle-memory verb (rebase, checkout, push, …) gets a // lesson that corrects the zit model instead of a bare error, so // the diagnostic teaches (DX D2). Still a usage error: exit 2, // stderr only, nothing on stdout. if (cli.reflexLesson(verb)) |lesson| { streams.err.print("zit: '{s}' is not a zit command. {s}\n", .{ verb, lesson }) catch {}; return .usage; } // A planned-but-unbuilt zit command (e.g. `commit`, which init's // help points the user at) resolves to a promise, not a dead-end: // every verb the shipped help names owns a handler or this stub. if (cli.isPlanned(verb)) { streams.err.print("zit: '{s}' is planned but not implemented yet.\n", .{verb}) catch {}; return .usage; } // Otherwise the generic path: name the unknown command, offer a // near-miss suggestion when one is close (DX D4), then the command // list so discoverability survives a growing verb set. streams.err.print("zit: unknown command '{s}'\n", .{verb}) catch {}; if (cli.didYouMean(verb)) |suggestion| { streams.err.print("did you mean '{s}'?\n", .{suggestion}) catch {}; } // Two lines, never the whole command list (zit#156): a typo is // not a request for the manual. streams.err.writeAll("try 'zit help' for the command list\n") catch {}; return .usage; }, } } /// `zit import-git` — the option surface is `cli.import_git_usage`, named /// rather than re-transcribed here because this comment was a fourth copy of /// that line and had already fallen behind it. /// /// Parse the command surface, split DEST into the parent handle and basename /// the transaction owns, then run the production Git/import dependencies. /// `zit export-git SOURCE DEST` — mirror the source repository's canonical /// check-in graph into a Git repository, incrementally (see git_export_cli). fn runExportGit( gpa: std.mem.Allocator, io: std.Io, verb_args: []const []const u8, streams: cli.Streams, ) cli.Exit { const parsed = cli.parseExportGitArgs(verb_args) catch |err| { streams.err.print("zit export-git: {s}\n", .{switch (err) { error.MissingSource => "SOURCE is required", error.MissingDestination => "DEST is required", error.ExtraArgument => "too many arguments", error.UnknownOption => "unknown option", error.MissingValue => "--email-domain wants a value", error.DuplicateOption => "--email-domain given twice", }}) catch {}; streams.err.writeAll("usage: " ++ cli.export_git_usage ++ "\n") catch {}; return .usage; }; if (parsed.help) { streams.out.writeAll(export_git_help) catch {}; return .ok; } // SOURCE resolves the way `serve`/`open` document it: a directory // holding `.zit`, or the `.zit` file itself (use its directory). var source_dir = blk: { const st = std.Io.Dir.cwd().statFile(io, parsed.source, .{}) catch { streams.err.print("zit export-git: cannot read SOURCE '{s}'\n", .{parsed.source}) catch {}; return .failure; }; if (st.kind == .directory) { break :blk std.Io.Dir.cwd().openDir(io, parsed.source, .{}) catch { streams.err.print("zit export-git: cannot open SOURCE '{s}'\n", .{parsed.source}) catch {}; return .failure; }; } const dirname = std.fs.path.dirname(parsed.source) orelse "."; break :blk std.Io.Dir.cwd().openDir(io, dirname, .{}) catch { streams.err.print("zit export-git: cannot open SOURCE '{s}'\n", .{parsed.source}) catch {}; return .failure; }; }; defer source_dir.close(io); var repo: zit.Repository = undefined; zit.Repository.open(&repo, gpa, io, source_dir, .{ .mode = .full }) catch { streams.err.print("zit export-git: '{s}' is not a readable zit repository\n", .{parsed.source}) catch {}; return .failure; }; defer repo.deinit(gpa); const source_key = blk: { const log = repo.logStore(gpa) catch { streams.err.writeAll("zit export-git: cannot read the source log\n") catch {}; return .failure; }; break :blk log.logKey() catch { streams.err.writeAll("zit export-git: cannot key the source log\n") catch {}; return .failure; }; }; // DEST: create the directory when absent; git init happens in preflight. std.Io.Dir.cwd().createDir(io, parsed.destination, .default_dir) catch {}; var dest_dir = std.Io.Dir.cwd().openDir(io, parsed.destination, .{}) catch { streams.err.print("zit export-git: cannot open DEST '{s}'\n", .{parsed.destination}) catch {}; return .failure; }; defer dest_dir.close(io); var production = git_export_cli.ProductionGit{ .io = io, .dest_dir = dest_dir }; var session = git_export_cli.Session{ .gpa = gpa, .io = io, .repo = &repo, .source_log_key = source_key, .dest_path = parsed.destination, .dest_dir = dest_dir, .ops = production.ops(), .rebuild = parsed.rebuild, .email_domain = parsed.email_domain, }; defer session.deinit(); const outcome = session.run() catch |err| { streams.err.print("zit export-git: {s}: {s}\n", .{ @errorName(err), session.diagnostic.items }) catch {}; return .failure; }; if (outcome.provisional_fixture and !parsed.quiet) { streams.err.writeAll( "note: the source holds sub-manifest (S-card) artifacts under a PROVISIONAL\n" ++ "specification — treat this mirror as disposable and re-export after any\n" ++ "format revision rather than building on it (see 'zit submanifest --help').\n", ) catch {}; } streams.out.print("exported {d} commits into {s}\n", .{ outcome.commits_emitted, parsed.destination }) catch {}; return .ok; } fn runImportFossil( gpa: std.mem.Allocator, io: std.Io, arena: std.mem.Allocator, verb_args: []const []const u8, streams: cli.Streams, ) cli.Exit { const parsed = switch (cli.parseImportFossilArgs(verb_args)) { .ok => |p| p, .unknown_option => |tok| return cli.usageError(streams, "import-fossil", .unknown_option, tok), .extra_argument => |tok| return cli.usageError(streams, "import-fossil", .extra_argument, tok), .bad_k => |tok| { streams.err.print("zit import-fossil: --k wants an integer 1..12 (got '{s}')\n", .{tok}) catch {}; return .usage; }, .both_modes => { streams.err.writeAll("zit import-fossil: --compatible and --submanifest are exclusive; the choice is the point\n") catch {}; return .usage; }, }; if (parsed.help) { streams.out.writeAll(import_fossil_help) catch {}; return .ok; } const source = parsed.source orelse return cli.usageError(streams, "import-fossil", .no_file, ""); const dir = parsed.dir orelse return cli.usageError(streams, "import-fossil", .no_file, ""); const mode = parsed.mode orelse { streams.err.writeAll( "zit import-fossil: choose --compatible or --submanifest. The difference is\n" ++ "permanent: --compatible keeps the corpus verbatim and stays in sync with the\n" ++ "origin; --submanifest re-mints history as sub-manifest trees under a new\n" ++ "project code and can never sync with the origin again.\n", ) catch {}; return .usage; }; if (mode == .compatible) { if (parsed.k != null) { streams.err.writeAll("zit import-fossil: --k belongs to --submanifest\n") catch {}; return .usage; } // The compatible import IS the clone: fossil's sync protocol is the // import path, and preserving the corpus verbatim is what clone does. streams.out.writeAll("compatible import: preserving the corpus verbatim via the sync protocol\n") catch {}; return cmd_sync.runClone(gpa, io, arena, &.{ source, dir }, streams); } return fossil_import_cli.runSubmanifestImport(gpa, io, arena, source, dir, parsed.k, "import-fossil", streams); } fn runImportGit( gpa: std.mem.Allocator, io: std.Io, verb_args: []const []const u8, streams: cli.Streams, amb: cli.Ambient, ) cli.Exit { const parsed = cli.parseImportGitArgs(verb_args) catch |err| { // The conflict gets its own sentence: the generic form would print // "invalid arguments (ConflictingSubmanifestOptions)", which names the // cause without saying which two options collided or why. if (err == error.ConflictingSubmanifestOptions) { streams.err.writeAll("zit import-git: --no-submanifests and --submanifest-k cannot be combined; --submanifest-k tunes which k a tree import uses, it does not turn trees off\n") catch {}; return .usage; } streams.err.print("zit import-git: invalid arguments ({s}); see 'zit import-git --help'\n", .{@errorName(err)}) catch {}; return .usage; }; if (parsed.help) { streams.out.writeAll(import_git_help) catch {}; return .ok; } const parent_path = std.fs.path.dirname(parsed.destination) orelse "."; const destination_name = std.fs.path.basename(parsed.destination); var destination_parent = std.Io.Dir.cwd().openDir(io, parent_path, .{}) catch |err| { streams.err.print("zit import-git: cannot open destination parent '{s}': {s}\n", .{ parent_path, @errorName(err) }) catch {}; return .failure; }; defer destination_parent.close(io); var git = git_import_cli.ProductionGit.init(gpa, io, amb.environ); defer git.deinit(); git.setDefaultBranch(parsed.default_branch); git.setRefuseSigned(parsed.refuse_signed); var dependencies = git.dependencies(.real_filesystem); if (parsed.max_export_payload_bytes) |bytes| dependencies.limits.max_export_payload_bytes = bytes; return git_import_cli.run(gpa, io, .{ .source = parsed.source, .destination_parent = destination_parent, .destination_name = destination_name, // The parser has already made the pair unreachable, so the branch is // total: `no_submanifests` implies `submanifest_k == null`. .submanifest = if (parsed.no_submanifests) .flat else .{ .tree = parsed.submanifest_k }, .attribution = switch (parsed.attribution) { .committer => .committer, .author => .author, }, .default_branch = parsed.default_branch, .quiet = parsed.quiet, .provenance = parsed.provenance, }, streams, dependencies); } /// `zit init [-q|--quiet] [--] [directory]` — the thin wiring: parse, resolve /// an absolute target, then drive the existing library sequence (create `.zit`, /// `LogStore.open`/`commit`/`deinit`, `checkout.write`). Filesystem access is /// confined to this function. All-or-nothing: any failure after `.zit` is /// created rolls it back so a failed init never poisons the already-exists /// guard (B-init-9). (contracts/init.md B-init-1..12.) pub fn runInit( gpa: std.mem.Allocator, io: std.Io, arena: std.mem.Allocator, verb_args: []const []const u8, streams: cli.Streams, ) cli.Exit { // 1. Parse (B-init-1). Usage errors → exit 2 (B-init-10). const parsed = switch (cli.parseInitArgs(verb_args)) { .ok => |p| p, .unknown_option => |tok| return cli.usageError(streams, "init", .unknown_option, tok), .extra_argument => |tok| return cli.usageError(streams, "init", .extra_argument, tok), }; // --help wins over every effect (B-init-11). if (parsed.help) { streams.out.writeAll(init_help) catch {}; return .ok; } // 2. Resolve an absolute target directory and repo path (B-init-2/5/12). // `resolve` normalizes `.`/`..` and, when `dir_arg` is already // absolute, ignores the cwd — yielding a stable, parseable success token. const dir_arg = parsed.directory orelse "."; // Uses the grow-buffer helper (not std's `currentPathAlloc`, which aborts on // a cwd deeper than the platform PATH_MAX). A cwd we cannot name is reported // honestly, never a crash. const cwd = (cli.currentPathAlloc(arena, io) catch |e| return renderStorage(streams, dir_arg, reasonOf(e))) orelse return renderStorage(streams, dir_arg, "cannot determine the current directory"); const abs_dir = std.fs.path.resolve(arena, &.{ cwd, dir_arg }) catch |e| return renderStorage(streams, dir_arg, reasonOf(e)); const abs_zit = std.fs.path.join(arena, &.{ abs_dir, repo_filename }) catch |e| return renderStorage(streams, abs_dir, reasonOf(e)); // 3. Ensure the target directory exists (B-init-5); a file in the way is // "not a directory" (B-init-8). const cwd_dir = std.Io.Dir.cwd(); cwd_dir.createDirPath(io, abs_dir) catch |e| switch (e) { error.NotDir => return renderNotDir(streams, abs_dir), else => return renderStorage(streams, abs_dir, reasonOf(e)), }; // 4. Open a handle to the target dir; all subsequent file ops go through it // so `.zit`/`.zit-checkout` land in the resolved directory. var target = cwd_dir.openDir(io, abs_dir, .{}) catch |e| switch (e) { error.NotDir => return renderNotDir(streams, abs_dir), else => return renderStorage(streams, abs_dir, reasonOf(e)), }; defer target.close(io); // 5. Create `.zit` exclusively — this defines the "already exists" error // out of existence (no separate stat probe, no TOCTOU). Detection is // precisely the presence of `.zit`; an orphan `.zit-checkout` is // irrelevant (B-init-7). var file = target.createFile(io, repo_filename, .{ .read = true, .exclusive = true }) catch |e| switch (e) { error.PathAlreadyExists => return renderAlreadyExists(streams, abs_zit), error.IsDir, error.NotDir => return renderNotDir(streams, abs_zit), else => return renderStorage(streams, abs_dir, reasonOf(e)), }; // 6. Default hash policy (B-init-3): a fresh repo with no SHA-named // artifacts is SHA3-256 (`.sha3` = 2). Threaded opaquely into the header. const hash_kind: u8 = @intFromEnum(zit.artifact.defaultPolicy(false, false)); // 7. Log lifecycle (B-init-2): `open` on the empty file writes the 64-byte // header (zero frames); `commit` fsyncs it (durability before success). // On any failure, close and roll back `.zit` (B-init-9). var fs = storage.FileStorage.init(io, file, 0); var log = logstore.LogStore.open(gpa, fs.storageFile(), hash_kind) catch |e| { file.close(io); target.deleteFile(io, repo_filename) catch {}; return renderStorage(streams, abs_dir, reasonOf(e)); }; // §9.1 — give the repository a stable, unique project code (sync §9.1: // `projectcode = artifact-hash`). Derived deterministically from the // repo's absolute path — reproducible, distinct per repository, and with // no ambient RNG (the determinism seam). Persisted as repo config so // `serve` emits it in the clone reply's `push` card and a clone adopts it. const project_code = zit.sync.deriveProjectCode(&.{abs_zit}); log.putConfig(.{ .hash_policy = hash_kind, .default_branch = "trunk", .project_id = "", .project_code = &project_code, }) catch |e| { log.deinit(gpa); file.close(io); target.deleteFile(io, repo_filename) catch {}; return renderStorage(streams, abs_dir, reasonOf(e)); }; log.commit() catch |e| { log.deinit(gpa); file.close(io); target.deleteFile(io, repo_filename) catch {}; return renderStorage(streams, abs_dir, reasonOf(e)); }; log.deinit(gpa); file.close(io); // 8. Checkout record (B-init-2/4): trunk unborn (`version == ""`). // `checkout.write` is itself atomic, so a partial record is impossible; // on failure roll back `.zit` (B-init-9). checkout.write(io, target, .{ .version = "", .branch = "trunk", .merge_parent = "", .special_op = "", .manifest_flags = "" }) catch |e| { target.deleteFile(io, repo_filename) catch {}; return renderStorage(streams, abs_dir, reasonOf(e)); }; // 9. Success feedback (B-init-2/6/12): a single plain-ASCII line whose // trailing whitespace-delimited token is the absolute `.zit` path; // `-q`/`--quiet` writes nothing. if (!parsed.quiet) { streams.out.print("Initialized empty zit repository in {s}\n", .{abs_zit}) catch {}; } return .ok; } /// The one wording for "0.1.0 does not version symlinks", shared by explicit /// `add`'s refusal and recursive `add`'s skip notice so the two can never drift /// apart — the drift between the recursive and direct paths IS blocker B3 /// (docs/plans/CLI-VERB-BREAKDOWN.md §B3). A fifo/socket keeps the older, /// separate "not a regular file" wording: those are not the same refusal and /// must not read as one. pub const symlink_unversioned = "symlink (not versioned in 0.1.0)"; /// The repository file's fixed name (logstore plan E: a copied `.zit` is a /// complete repository). pub const repo_filename = zit.repo_filename; // --- educating diagnostics (all to err, all exit 1) ----------------------- fn renderAlreadyExists(s: cli.Streams, abs_zit: []const u8) cli.Exit { s.err.print("zit: a zit repository already exists at {s}\n", .{abs_zit}) catch {}; return .failure; } fn renderNotDir(s: cli.Streams, path: []const u8) cli.Exit { s.err.print("zit: cannot initialize: {s} is not a directory\n", .{path}) catch {}; return .failure; } pub fn renderStorage(s: cli.Streams, abs_dir: []const u8, reason: []const u8) cli.Exit { s.err.print("zit: cannot initialize repository at {s}: {s}\n", .{ abs_dir, reason }) catch {}; return .failure; } /// Maps a raw error to a short, honest, human phrase for the B-init-9 line. /// Pure rendering, not logic — the switch is over the union of every error /// the init path can surface (filesystem, `LogStore`, `checkout`). pub fn reasonOf(err: anyerror) []const u8 { return switch (err) { error.OutOfMemory => "out of memory", error.AccessDenied, error.PermissionDenied => "permission denied", error.ReadOnlyFileSystem => "read-only file system", error.NoSpaceLeft, error.DiskQuota => "no space left on device", error.FileNotFound => "no such file or directory", error.Corrupt => "repository is corrupt", // `error.Storage` (LogStore/checkout device failure) and every other // device/OS error collapse to one honest phrase. else => "input/output error", }; } // --- help / usage text ---------------------------------------------------- /// Renders a checkout-state read failure for any verb; the verb prefixes the /// message so one mapper serves both the read-only working-tree verbs and commit. pub fn checkoutFail(s: cli.Streams, verb: []const u8, err: checkout.CheckoutError) cli.Exit { switch (err) { error.Corrupt => s.err.print( "zit {s}: cannot read checkout state: .zit-checkout is corrupt\n", .{verb}, ) catch {}, error.Storage => s.err.print( "zit {s}: cannot read checkout state: input/output error\n", .{verb}, ) catch {}, } return .failure; } /// Renders a managed-set read failure for any verb; the verb prefixes the /// message so one mapper serves both the read-only working-tree verbs and commit. pub fn managedFail(s: cli.Streams, verb: []const u8, err: managed.ManagedError) cli.Exit { switch (err) { error.Corrupt => s.err.print( "zit {s}: cannot read tracked files: .zit-checkout is corrupt\n", .{verb}, ) catch {}, error.Storage => s.err.print( "zit {s}: cannot read tracked files: input/output error\n", .{verb}, ) catch {}, error.TooLong => s.err.print( "zit {s}: cannot read tracked files: a tracked path is too long\n", .{verb}, ) catch {}, error.TooMany => s.err.print("zit {s}: cannot read tracked files: too many tracked files\n", .{verb}) catch {}, error.OutOfMemory => s.err.print("zit {s}: out of memory\n", .{verb}) catch {}, } return .failure; } pub fn stateOom(s: cli.Streams, verb: []const u8) cli.Exit { s.err.print("zit {s}: out of memory\n", .{verb}) catch {}; return .failure; } // ========================================================================== // `zit diff` — read-only metadata comparison of managed paths against the // checkout's fully resolved F-set. It buffers every record before writing so // an I/O/corruption/OOM failure never leaves a partial report on stdout. // ========================================================================== /// Upper bound on one managed working-tree file read by `commit` or `diff`. /// The boundary is inclusive, so a file exactly this size is valid; larger /// files are refused before allocation. /// /// Defined in `managed` so the undo journal's pre-image capture shares the one /// number; a layer holding a smaller one refuses files the others accepted. pub const max_tracked_file_bytes: usize = managed.max_tracked_file_bytes; comptime { // The working-tree ceiling IS the storage frame ceiling. A tracked file's // content becomes one artifact frame, and `logstore.put`/`putArtifact` // assert `bytes.len <= max_payload` — so if these ever diverged, commit // would trip that assert (undefined behaviour in a release build) instead of // refusing the file. Asserted here because this is the layer that imports // both; `managed` deliberately imports only `std`, so it cannot state it. std.debug.assert(max_tracked_file_bytes == logstore.max_payload); } pub fn trackedFileSizeAllowed(size: u64) bool { return size <= @as(u64, max_tracked_file_bytes); } pub fn executable(permissions: File.Permissions) bool { if (comptime File.Permissions.has_executable_bit) { return permissions.toMode() & 0o111 != 0; } return false; } /// The diagnostic every working-tree verb (add/rm/status) prints when discovery /// finds no repository. Shared so all three fail identically (B-add-8 / B-rm-8 / /// B-status-9 "identical shape") — a single, byte-identical line, exactly as each /// contract quotes it. /// #85 — say what was actually looked for. Discovery requires a CHECKOUT /// (`probeRoot` tests for `.zit-checkout` first), so the old wording — "no .zit /// found" — was flatly false in the case that matters: a bare store, where /// `.zit` is sitting right there in the directory being complained about. An /// operator reading it had every reason to think the repository was corrupt. /// /// The second line is the actual remedy, and it is why this stayed one constant /// rather than becoming a probe at 54 call sites: naming `-R` is right for a /// bare repository AND harmless where there is none. pub const not_a_repo_msg = "zit: no zit checkout here (looked for .zit-checkout in this directory and its parents)\n" ++ "if this is a bare repository, name it with -R, or run 'zit open' to create a working tree\n"; /// Report a `cli.resolveRoot` that found nothing (#97). /// /// The message above is right for a DISCOVERY failure and wrong for a `-R` /// one: it tells the operator to name the repository with `-R` when `-R` is /// exactly what they named. Worse, it reads identically whether the path was /// mistyped or simply holds no repository, so a stale path in a script looks /// like a bug in the verb. /// /// Re-probes the given path to separate the two mistakes an operator actually /// makes. The probe is one `openDir` on a path already known to have failed, /// so it costs nothing on the success path and runs once on the way out. pub fn reportNoRoot(streams: cli.Streams, io: std.Io, verb: []const u8, override: ?[]const u8) void { const given = override orelse { streams.err.writeAll(not_a_repo_msg) catch {}; return; }; // `-R <dir>/.zit` names the store file; the repository is its parent, and // the resolver accepts both spellings, so the probe must too or the // diagnostic would contradict what was accepted. const probe = if (std.mem.eql(u8, std.fs.path.basename(given), ".zit")) (std.fs.path.dirname(given) orelse ".") else given; var dir = std.Io.Dir.cwd().openDir(io, probe, .{}) catch { streams.err.print("zit {s}: no such repository: {s}\n", .{ verb, given }) catch {}; return; }; defer dir.close(io); streams.err.print( "zit {s}: no repository at '{s}' (no .zit there)\n", .{ verb, given }, ) catch {}; } // ========================================================================== // `zit log CHECKIN` — one explicit check-in and its reachable parent graph. // ========================================================================== /// One honest phrase for a repository the read-only family cannot open or stat. pub fn repoReadFail(s: cli.Streams, verb: []const u8) cli.Exit { s.err.print("zit {s}: cannot read repository: input/output error\n", .{verb}) catch {}; return .failure; } /// A derive that rejected a specific artifact, named. Distinct from /// `repoOpenFail`'s bare corruption line because it is a different claim: the /// repository is readable, and ONE artifact in it is not the shape the deriver /// requires. The name is actionable — `zit cat` it, or look it up in an import /// map — where "repository is corrupt" is not. pub fn repoDeriveFault(s: cli.Streams, verb: []const u8, f: repoview.RepoView.Fault) cli.Exit { const why: []const u8 = switch (f.reason) { .missing_timestamp => "carries no D-card timestamp", .unparsable_timestamp => "carries a D-card timestamp that cannot be parsed", }; s.err.print( "zit {s}: cannot read repository: artifact {s} {s}\n", .{ verb, f.artifactName(), why }, ) catch {}; return .failure; } /// Maps a `Repository.open` failure to the verb's diagnostic + exit 1. Closed /// over `Repository.OpenError` so the CLI switch stays exhaustive; general over /// the verb name so every read-only verb shares one map. The `not-a-repo` and /// corrupt/IO phrasings are byte-identical to the pre-facade hand-wired opens. pub fn repoOpenFail(s: cli.Streams, verb: []const u8, err: Repository.OpenError) cli.Exit { switch (err) { error.OutOfMemory => s.err.print("zit {s}: out of memory\n", .{verb}) catch {}, error.NotARepo => s.err.writeAll(not_a_repo_msg) catch {}, error.CannotRead => s.err.print("zit {s}: cannot read repository: input/output error\n", .{verb}) catch {}, error.Corrupt => s.err.print("zit {s}: cannot read repository: repository is corrupt\n", .{verb}) catch {}, } return .failure; } /// Discover the checkout root and open the repository through the `Repository` /// facade for the read-only verb family (leaves, branch, tag, info). One place /// owns the discover + open + per-verb diagnostics so every read-only verb reads /// identical text. On `.ok`, `repo` is filled (caller `deinit`s it) and /// `root_dir` is set (caller closes it AFTER `repo.deinit`); on any non-`.ok` /// the diagnostic is already on stderr, `repo` is untouched, and any opened root /// dir has been closed here. pub fn openRepo( gpa: std.mem.Allocator, io: std.Io, arena: std.mem.Allocator, streams: cli.Streams, verb: []const u8, repo_override: ?[]const u8, bypass_cache: bool, fast_read: bool, repo: *Repository, root_dir: *std.Io.Dir, ) cli.Exit { // `resolveRoot` is the one place the `-R` route and the walk-up-from-cwd // route converge, so no verb below has to know which one it got. const root = (cli.resolveRoot(arena, io, repo_override) catch return stateOom(streams, verb)) orelse { reportNoRoot(streams, io, verb, repo_override); return .failure; }; // Hot verbs take the zero-parse image path (`.fast_read`). Since #98 that // includes everything needing ancestry: the image carries adjacency, so // Since #98 that is every read verb: the image carries adjacency and the // resolved §5 tag surface, so nothing here re-derives the log to answer a // name, a branch listing, or a display override. The write verbs stay on // the full derive because an image is read-only by construction. repo.open(gpa, io, root.dir, .{ .bypass_cache = bypass_cache, .mode = if (fast_read) .fast_read else .full, }) catch |e| { var d = root.dir; d.close(io); // The derive records WHICH artifact it rejected. Reported here because // this is the one funnel every verb's open passes through, and because // `error.Corrupt` on its own sends whoever hit it to bisect a store that // took minutes to walk. if (repo.view.fault) |f| return repoDeriveFault(streams, verb, f); return repoOpenFail(streams, verb, e); }; // zit#161: with a checkout under foot, `current`/`prev`/`next` have a // referent. A bare store named with -R has none. if (repo_override == null) { var ck_buf: [checkout.max_ck_payload + 1]u8 = undefined; if (checkout.read(io, root.dir, &ck_buf) catch null) |ck| { repo.setCurrentHint(ck.version); } } root_dir.* = root.dir; return .ok; } /// Displayed prefix of the full artifact hash on a timeline line. Display only: /// the total order keys on the FULL hash inside `checkinsByMtime`, so a display /// collision never perturbs ordering. pub const timeline_hash_display = 12; /// The first line of a check-in comment (up to, excluding, the first LF), so a /// multi-line comment never injects a newline into the one-line-per-check-in /// listing (B-timeline-6). A comment with no LF is returned whole. pub fn firstCommentLine(comment: []const u8) []const u8 { const nl = std.mem.indexOfScalar(u8, comment, '\n') orelse return comment; return comment[0..nl]; } /// Renders the §6.3 branch listing to stdout, one line per entry in `entries` /// order (name-ascending, as `branches` returns them). `current` is the branch /// name of the checked-out check-in (§6.1/§6.4) or null; the matching entry — /// at most one, since §6.3 grouping yields distinct names — gets a leading /// "* ", every other a two-space indent. A representative-closed branch (§6.3) /// appends " (closed)". Pure; bounded by `entries.len`. pub fn renderBranchList( out: *std.Io.Writer, entries: []const repostate.RepoState.BranchEntry, current: ?[]const u8, ) void { var marked: usize = 0; for (entries) |entry| { const is_current = current != null and std.mem.eql(u8, entry.name, current.?); if (is_current) marked += 1; std.debug.assert(marked <= 1); // distinct names ⇒ at most one mark out.writeAll(if (is_current) "* " else " ") catch {}; out.writeAll(entry.name) catch {}; if (entry.closed) out.writeAll(" (closed)") catch {}; out.writeAll("\n") catch {}; } std.debug.assert(marked <= 1); } /// The reserved 'sym-' prefix (repo-state §2): a symbolic name is stored with /// it and surfaced with it stripped. pub const sym_prefix = "sym-"; /// Ascending raw-byte order over two artifact IDs, so the merge-parent lines /// print in a deterministic order (IDs are unique per check-in). pub fn lessByHash(_: void, a: []const u8, b: []const u8) bool { return std.mem.order(u8, a, b) == .lt; } pub fn updateApplyFail(s: cli.Streams, target_id: []const u8, err: update.ApplyError) cli.Exit { switch (err) { // applyCheckin never emits this: only applyMerge's cross-cone guard // does, and this switch maps a checkin materialization. The unreachable // IS the assertion; a future emitter trips it in tests immediately. error.SparseMergeUnsupported => unreachable, error.FileContentMissing => { var b: [512]u8 = undefined; s.err.print("zit update: {s}\n", .{update.stageFaultReason(&b)}) catch {}; // Same class `open`'s preflight names. `update` is handed its target // rather than resolving one, so it has no preflight — but the cause // and the wording should not depend on which verb met it. var lines = std.mem.splitScalar(u8, update.foreign_checkin_note, '\n'); while (lines.next()) |line| { if (line.len == 0) continue; s.err.print("zit update: {s}\n", .{line}) catch {}; } }, error.OutOfMemory => s.err.writeAll("zit update: out of memory\n") catch {}, error.InvalidCheckinId => s.err.print( "zit update: invalid check-in ID '{s}' (expected 40 or 64 lowercase hexadecimal digits)\n", .{target_id}, ) catch {}, error.CheckinNotFound => s.err.print("zit update: check-in '{s}' not found\n", .{target_id}) catch {}, error.CheckinContentMissing => s.err.print( "zit update: cannot read repository: check-in '{s}' content is missing\n", .{target_id}, ) catch {}, error.CheckinNameMismatch => s.err.print( "zit update: cannot read repository: check-in '{s}' content does not match its name\n", .{target_id}, ) catch {}, error.AggregateChecksumMismatch => s.err.print( "zit update: check-in '{s}' R checksum disagrees with the content its F cards name\n", .{target_id}, ) catch {}, error.TargetNotManifest => s.err.print("zit update: check-in '{s}' is not a valid manifest\n", .{target_id}) catch {}, error.BaselineNotFound => s.err.print( "zit update: cannot read repository: check-in '{s}' baseline is not found\n", .{target_id}, ) catch {}, error.BaselineContentMissing => s.err.print( "zit update: cannot read repository: check-in '{s}' baseline content is missing\n", .{target_id}, ) catch {}, error.BaselineNameMismatch => s.err.print( "zit update: cannot read repository: check-in '{s}' baseline content does not match its name\n", .{target_id}, ) catch {}, error.BaselineNotManifest => s.err.print( "zit update: check-in '{s}' baseline is not a valid manifest\n", .{target_id}, ) catch {}, error.BaselineIsDelta => s.err.print( "zit update: check-in '{s}' baseline is not fully resolvable\n", .{target_id}, ) catch {}, error.StateRecordInvalid => s.err.writeAll("zit update: cannot read repository: " ++ update.state_record_invalid_reason ++ "\n") catch {}, error.Corrupt => s.err.writeAll("zit update: cannot read repository: repository is corrupt\n") catch {}, error.InvalidPath => s.err.print("zit update: check-in '{s}' names an invalid file path\n", .{target_id}) catch {}, error.ReservedPath => s.err.print("zit update: check-in '{s}' names a reserved file path\n", .{target_id}) catch {}, error.SymlinkUnsupported => s.err.print( "zit update: check-in '{s}' contains an unsupported symlink entry\n", .{target_id}, ) catch {}, error.CurrentStateInvalid => s.err.writeAll("zit update: current working-tree state is invalid\n") catch {}, error.WorktreeNotClean => s.err.writeAll("zit update: working tree is not clean\n") catch {}, error.UnmanagedCollision => s.err.writeAll("zit update: an untracked file or directory is in the way\n") catch {}, error.Storage => s.err.writeAll("zit update: cannot update working tree: input/output error\n") catch {}, error.TransactionUnavailable => s.err.writeAll("zit update: cannot create a transaction directory\n") catch {}, error.RollbackFailed => s.err.writeAll( "zit update: update failed and could not be rolled back; the previous working-tree state is retained for recovery\n", ) catch {}, error.CleanupFailed => s.err.writeAll( "zit update: update committed, but the transaction directory could not be removed\n", ) catch {}, } return .failure; } // ========================================================================== // `zit commit` — mints a check-in (contracts/commit.md B-commit-1..20). The // first verb that appends to the log AND advances the working state. A thin // consumer: it decides which files at which content, hands the library RFC card // structs, and the library owns all format work (`artifact.create` computes the // Z self-check, orders/escapes cards, frames the artifact). No logic enters any // library module; no `std.http`, no network; history is append-only. The one // deep, RFC-normative unit — the F-set composition — is `cli.composeFCards`, // pure and separately unit-tested; `runCommit` is thin wiring around it. // ========================================================================== /// SHA3-256 digest length: the on-disk log frame is keyed by these 32 raw bytes /// (logstore.max_hash_len), while the artifact NAME is their 64-hex form. `init` /// wrote the SHA3-256 policy (B-commit-16), so the algorithm is this constant. pub const digest_len = 32; /// Decodes an artifact name into its binary digest — the log-frame key — writing /// into `out` and returning the used prefix. Returns null for anything that is /// not a name this suite mints, so the caller maps it to a diagnostic rather /// than trapping. /// /// LENGTH IS DERIVED FROM THE NAME, NOT ASSUMED. [FossilArtifact] §7.2 admits /// two widths — 20 bytes (sha1) and 32 bytes (sha3-256) — and a repository is /// free to hold either. This used to require exactly `digest_len * 2` hex /// characters because `zit init` writes the sha3-256 policy, which made the /// sha3 case look like the only case. It is not: `zit import-git` opens its /// destination under git's native sha1, so every check-in in an imported /// repository is named in 40 hex characters. The old check rejected all of /// them, and `zit commit` reported the parent check-in as corrupt on a store /// `zit verify` called clean (issue #24). /// /// The buffer is `digest_len` wide because that is the larger of the two; a /// 20-byte digest uses the prefix. Callers pass the returned SLICE onward — /// passing `&out` would reintroduce the bug with the padding included. pub fn hexToDigest(hex: []const u8, out: *[digest_len]u8) ?[]const u8 { if (hex.len != 40 and hex.len != digest_len * 2) return null; const n = hex.len / 2; _ = std.fmt.hexToBytes(out[0..n], hex) catch return null; return out[0..n]; } /// Lowercase-hex-encodes a raw binary `digest` into `out`, returning the used /// prefix — the log-frame key's wire NAME (sync §3.3). The inverse of /// `decodeWireName`; both live at the log↔store boundary so no other module /// sees a raw digest cross into wire space. pub fn digestToWireName(digest: []const u8, out: *[2 * digest_len]u8) []const u8 { std.debug.assert(digest.len <= digest_len); const hexdigits = "0123456789abcdef"; for (digest, 0..) |b, i| { out[2 * i] = hexdigits[b >> 4]; out[2 * i + 1] = hexdigits[b & 0x0f]; } return out[0 .. 2 * digest.len]; } /// Decodes a wire artifact NAME (40- or 64-hex, sync §3.3) into its raw binary /// digest — the log-frame key. Returns null for anything that is not an /// even-length hex string of at most `digest_len` bytes, so a malformed peer /// name is rejected cleanly rather than trapping. The inverse of /// `digestToWireName`. pub fn decodeWireName(name: []const u8, out: *[digest_len]u8) ?[]const u8 { if (name.len == 0 or name.len % 2 != 0) return null; const nbytes = name.len / 2; if (nbytes > digest_len) return null; _ = std.fmt.hexToBytes(out[0..nbytes], name) catch return null; return out[0..nbytes]; } /// Reads the whole-second wall clock for the D card (B-commit-7), preferring the /// `SOURCE_DATE_EPOCH` reproducible-build convention when set (a well-known, /// tell-free way to pin the clock across a process boundary — how the e2e suite /// fixes the timestamp of the spawned binary). Falls back to the real /// (wall-clock) `Io.Clock`. The single ambient-time read in the CLI. fn commitEpoch(io: std.Io, env: *const std.process.Environ.Map) i64 { if (env.get("SOURCE_DATE_EPOCH")) |raw| { const trimmed = std.mem.trim(u8, raw, " \t\r\n"); if (std.fmt.parseInt(i64, trimmed, 10)) |v| return v else |_| {} } return Io.Timestamp.now(io, .real).toSeconds(); } /// #75 — the millisecond within `commitEpoch`'s second. Deterministic runs /// (SOURCE_DATE_EPOCH) pin 0, so a reproducible build stays reproducible; /// a live clock reads the real sub-second remainder. fn commitEpochMs(io: std.Io, env: *const std.process.Environ.Map) u16 { if (env.get("SOURCE_DATE_EPOCH") != null) return 0; const ns = Io.Timestamp.now(io, .real).nanoseconds; const ms: i64 = @intCast(@mod(@divFloor(ns, std.time.ns_per_ms), 1000)); return @intCast(ms); } /// `zit amend NAME [--comment TEXT] [--user NAME] [--date ISO] [--branch NAME]` /// (contracts/amend.md B-amend-1..17). amend NEVER rewrites a check-in: it mints /// ONE control artifact whose T cards change how the target check-in DISPLAYS /// (comment/user/date/branch), which the crosslinker applies on the next read /// (repo-state §4/§5). History stays append-only — the original manifest is /// untouched (B-amend-5). The one durable write is the control artifact + log /// fsync; every validation (parse, NAME resolution, `--date` predicate, /// target-is-a-check-in) precedes it, so a refusal mints nothing. /// Assemble the ONE control artifact carrying `tags`, serialize it (artifact /// §5.2: exactly D, T…, U, Z), and store it durably in the log — the single /// fsync. Returns its 64-hex artifact name, written into `name_buf`. /// /// This is the mint step shared by `amend` and `tag add`: both compose a T run /// over a resolved check-in and then need exactly this. Everything a caller does /// before calling is read-only or in-memory, so a crash before the call mints /// nothing; after it returns, the control is durable and crosslinks on the next /// read (B-amend-5/7). The library owns card order, escaping, the Z self-check, /// and framing. D is the mint time — the §5.3 supersede clock (B-amend-6); U is /// the author's env login (empty → `anonymous` on read, §4.7.17). /// /// The error set is `logstore.OpenError`, the widest of the three the body can /// produce, so one caller-side mapper renders all of them under its own verb. pub fn mintControlArtifact( gpa: std.mem.Allocator, io: std.Io, root_dir: std.Io.Dir, amb: cli.Ambient, tags: []const artifact.TCard, name_buf: *[64]u8, ) logstore.OpenError![]const u8 { std.debug.assert(tags.len >= 1); // a control artifact with no T run says nothing var ts_buf: [23]u8 = undefined; const ts = cli.formatWriterTimestampMs(amb.now_epoch, amb.now_ms, &ts_buf); var m: artifact.Manifest = .{ .arena_state = std.heap.ArenaAllocator.init(gpa), .kind = .control, .a = null, .b = null, .c = null, .d = .{ .timestamp = ts }, .e = null, .g = null, .h = null, .i = null, .k = null, .l = null, .n = null, .r = null, .u = .{ .login = amb.user_env orelse "" }, .z = null, .w = null, .f = &.{}, .j = &.{}, .m = &.{}, .p = &.{}, .q = &.{}, .t = tags, }; defer m.deinit(); // frees the empty arena_state; card storage is the caller's const bytes = try artifact.create(&m, gpa); defer gpa.free(bytes); // Correctness self-check (safety builds only): the emitted bytes round-trip // to a control artifact. A failure means the CLI mis-populated a card — a bug // caught here, never shipped (mirrors commit's self-check). if (std.debug.runtime_safety) { var check = artifact.parse(gpa, bytes) catch |e| std.debug.panic("the control mint produced an unparseable artifact: {s}", .{@errorName(e)}); defer check.deinit(); std.debug.assert(check.kind == .control); } var file = cli.openStoreAppend(io, root_dir) catch return error.Storage; defer file.close(io); const log_st = file.stat(io) catch return error.Storage; var fs = storage.FileStorage.init(io, file, log_st.size); var log = try logstore.LogStore.open(gpa, fs.storageFile(), 0); defer log.deinit(gpa); var cdigest: [digest_len]u8 = undefined; std.crypto.hash.sha3.Sha3_256.hash(bytes, &cdigest, .{}); const control_name = artifact.artifactName(.sha3_256, bytes, name_buf); _ = try log.putArtifact(&cdigest, bytes, null); try log.commit(); return control_name; } const top_usage = \\zit — a Fossil version-control system in Zig \\ \\usage: zit <command> [args] \\ \\commands: \\ init create an empty zit repository \\ add mark files to include in your next check-in \\ rm stop tracking files (undo add) \\ mv rename a tracked file \\ status show the working checkout state \\ stat report the repository store's size and artifact census \\ diff compare managed files with the current check-in \\ extras list files in the checkout that zit isn't tracking \\ clean delete files in the checkout that zit isn't tracking \\ addremove track new files and forget deleted ones in one pass \\ commit record a check-in from your tracked files \\ log show a check-in and its reachable parents \\ open fill a freshly cloned directory with its files \\ update materialize a check-in in the working tree \\ merge merge another check-in into the working tree \\ revert restore managed files to their checked-in contents \\ undo undo the last update or revert \\ redo reapply an undone change \\ leaves list the open leaves of the check-in DAG \\ timeline list every check-in, most recent first \\ branch list the repository's branches \\ tag list the tags in use, or give a check-in a name \\ amend attach display overrides to a check-in \\ info resolve a name to a check-in and describe it \\ ls list the files of a check-in \\ cat emit a file's contents at a check-in \\ artifact print or describe a raw artifact by its hash \\ annotate show which check-in introduced each line of a file \\ grep search a file's history for a literal string \\ frames stream raw artifacts appended since a cursor (indexer feed) \\ crosslink stream derived per-check-in records (indexer feed) \\ bisect binary-search the check-in DAG for a good/bad boundary \\ serve serve this repository over HTTP for sync \\ ui open this repository in a browser (needs quarry) \\ clone copy a repository from a URL into a new directory \\ pull fetch new artifacts from a URL into this repository \\ push send this repository's artifacts to a URL \\ sync reconcile with a peer in both directions \\ remote show, set, or clear the default sync remote \\ settings show or change settings, for this repository or globally \\ config fetch a configuration area from a peer, or apply a file \\ users print the repository's user table \\ trust accept or withdraw consent to run the project's checks \\ pack repack the repository store into a compact new generation \\ submanifest show or change this repository's sub-manifest settings \\ checkpoint show, build, verify, or drop the local rebuild checkpoint \\ cone show or edit this checkout's sparse cone \\ worktrees list the working trees this repository serves \\ stash set working-tree changes aside and restore them later \\ landing propose a branch to land on trunk, as a reviewable record \\ precedent reuse a remembered conflict resolution (alias: rerere) \\ import-git import a Git repository into a new zit repository \\ import-fossil bring a fossil repository's history into zit \\ export-git mirror a zit repository into a Git repository \\ purge remove leaf check-ins, recoverably, and reclaim their bytes \\ shun permanently remove artifacts and record them as never-fetch \\ tarball write a check-in's files as a gzip-compressed tar archive \\ zip write a check-in's files as a zip archive \\ verify check this repository's store for damage \\ version print the version \\ help show this help \\ \\topics ('zit help TOPIC'): \\ names every accepted form of a check-in name, timestamps included \\ sparse sparse checkouts and sparse stores (--cone and friends) \\ repo reading a repository without a working tree (-R, --no-cache) \\ exit-codes the exit-status contract and its departures \\ \\aliases: 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 also works \\ (zit up, zit tim); an ambiguous one lists its candidates. \\ ; const export_git_help = "usage: " ++ cli.export_git_usage ++ "\n" ++ \\ \\Mirror the source's canonical check-in graph into a Git repository \\through `git fast-import` — flat, sub-manifest, and delta-manifest \\history alike, every file set read through the submanifests §9.3 \\enumeration seam. Incremental and resumable: a local-only mark map in \\DEST/.zit-export-state makes re-runs export only new check-ins, and \\deleting it costs a full re-export, never a wrong answer. \\ \\A source holding S-card artifacts is a PROVISIONAL-format fixture; the \\mirror it exports is disposable and the command says so once per run \\(--quiet suppresses the notice). \\ \\options: \\ --email-domain DOMAIN synthesized identity domain (default zit.invalid) \\ --rebuild ignore prior state; DEST must carry no refs \\ --quiet suppress the provisional-fixture notice \\ -h, --help show this help and exit \\ ; const import_git_help = "usage: " ++ cli.import_git_usage ++ "\n" ++ \\ \\Import Git repository SOURCE into fresh destination DEST. The import is \\transactional: DEST appears only after Git export, artifact creation, and \\Git ref publication complete successfully. \\ \\Branches are reconstructed from the commit graph, not from the ref each \\commit was exported under: 'git fast-export' emits every commit exactly \\once, under whichever ref reached it first, and Git itself has no \\branch-of-a-commit to ask. The destination's main branch claims its whole \\first-parent chain down to the root; every other branch then claims, \\shallowest tip first with ties broken by name, from its own tip down \\first parents until it reaches a commit already claimed. So a merge stays \\on the branch it was merged into, and history below a fork stays with the \\branch that continues through it. Only the deepest check-in of a claimed \\run records its branch — the rest inherit it, exactly as they would in a \\repository zit had written itself. Two branches on the same commit are \\the one degenerate case: there is a single run, the first name \\alphabetically records it, and the other still resolves as a name but is \\not listed by 'zit branch'. \\ \\A destination imported by an earlier zit took each check-in's branch from \\that exporting ref instead, which scrambled branch membership and \\inflated 'zit leaves'. Only the derived branch state was wrong: files, \\check-ins, and parent links were correct, and a re-import writes \\byte-identical check-in manifests, so recorded check-in hashes survive \\it. Re-import such a destination from its Git source to repair it; one \\that has been committed to with zit since its import cannot be repaired \\that way. \\ \\options: \\ --submanifest-k K \\ pin K (1 through 12) instead of analyzing. By default the \\ import measures the source's own file names and pins the \\ sub-manifest optimum; override only to match an existing \\ repository's pin, which is what lets two repositories \\ share sub-manifests \\ --no-submanifests \\ import with sub-manifests off: every check-in gets a flat \\ manifest and the destination keeps the default settings \\ (enable-submanifests off, no pinned k). Flat is the \\ default shape for a new repository; it is this verb that \\ turns sub-manifests on. Cannot be combined with \\ --submanifest-k, which chooses which k a tree import \\ uses rather than whether one is used \\ --attribution committer|author \\ record Git committer identities (default) or author identities \\ --default-branch NAME \\ make NAME the destination's main branch instead of \\ choosing it from the source (refs/heads/main when the \\ source has it, otherwise its first head). NAME must be a \\ branch the source publishes, or the import is refused. \\ This is not only a label: the main branch claims its \\ first-parent chain before any other, so NAME also decides \\ which branch keeps the history below every fork \\ --refuse-signed \\ refuse a source whose commits or tags are signed instead \\ of importing it. By default a signature is STRIPPED and \\ the import proceeds: Fossil has no signature card, so no \\ import can carry one through, and refusing withheld the \\ repository without saving the signature. Nothing else \\ about a commit changes either way \\ --provenance keep the import's instrumentation in the destination: \\ '.zit-import-map.jsonl' (per-object provenance -- what \\ every Git object became and why the mirror classifier \\ decided what it did) and 'refs' (the source ref \\ manifest). By default both are verified in staging and \\ then discarded, leaving only the repository and its \\ checkout record. The repository is identical either way \\ --max-export-payload-bytes BYTES \\ cumulative Git payload this import may read \\ (default 1099511627776). It is a bound on total \\ history, not on memory: payload is streamed and \\ released per event. Raise it when an import \\ reports ExportPayloadLimitExceeded \\ --quiet suppress progress diagnostics \\ -h, --help show this help and exit \\ ; /// `zit help names` (#68): the ONE statement of the check-in name-resolution /// procedure. Verbs point here instead of restating it, so a new form (the /// timestamp forms were documented in one help out of six before this /// existed) lands in every verb's documentation at once. pub const names_topic_help = \\zit help names — how a check-in name resolves \\ \\Wherever a command accepts a check-in (CHECKIN, VERSION, NAME), every \\form below is accepted, resolved by one shared procedure: \\ \\ a full artifact ID 40 or 64 hex characters \\ a unique ID prefix 4+ hex characters; ambiguity is refused \\ a branch name resolves to that branch's most recent check-in \\ a tag name resolves to the most recent check-in wearing it \\ tip the most recent check-in in the repository \\ current, ckout the check-in the working tree is on \\ prev, previous the current check-in's primary parent \\ next the youngest child of the current check-in \\ rid:N a store row id (debugging aid; not stable \\ across clones or rebuilds) \\ tag:NAME force tag namespace when a name is ambiguous \\ root:BRANCH the first check-in of BRANCH \\ start:BRANCH alias of root: \\ merge-in:BRANCH the latest check-in merged INTO the current \\ branch from BRANCH \\ a timestamp 2026-08-01, 2026-08-01T14:30, date:... — the \\ youngest check-in at or before that moment \\ BRANCH:TIMESTAMP the timestamp form scoped to one branch \\ (trunk:2026-08-01) \\ \\Resolution follows recency, not open-leaf status: a name can resolve to \\a closed or non-leaf check-in that 'zit leaves' would not list. Branch \\and tag names win over same-spelled hex prefixes; use tag: or a longer \\prefix to disambiguate. One trailing punctuation character is dropped \\and the name retried (a 'trunk.' pasted from prose still resolves), so \\git's ^ and ~ suffixes have no meaning here: 'trunk^' is 'trunk', and \\the parent is 'prev' or 'zit info' on the check-in. The checkout- \\relative names need a working tree; on a bare store (-R) they are not \\found. \\ ; /// `zit help sparse` (#68): the cone model in one place, instead of three /// verbs' flag tables. pub const sparse_topic_help = \\zit help sparse — sparse checkouts and sparse stores \\ \\A cone is a set of repository paths: directory subtrees and single \\files, given as --cone=PATH (repeatable), subtracted from by \\--cone-not=PATH exclusions, or read from a file with --cone-file=FILE \\(one member per line, '#' comments, a '!' prefix marks an exclusion). \\The most specific member covering a path decides: 'src' plus \\'--cone-not=src/vendor' plus 'src/vendor/keep' includes src, carves \\out vendor, and keeps the deeper re-inclusion. \\ \\Two independent scopes wear cones: \\ \\ the CHECKOUT (worktree) cone — 'open --cone', 'update --cone', or \\ the 'zit cone' verb. Only in-cone files materialize and track; \\ out-of-cone content stays in the record, and commits are \\ indistinguishable from dense commits of the same change. \\ \\ the STORE cone — 'clone --cone'. Only artifacts the cone needs are \\ held; the rest are deliberately elided phantoms, refetched on demand \\ when the cone widens (through the saved remote, one bounded retry). \\ \\Widening fetches; narrowing dematerializes but never deletes stored \\content ('zit pack --drop-elided' reclaims it explicitly). Losing \\sparse state heals toward dense: the cost is bandwidth, never data. \\A sparse store serves cone-scoped peers filtered, and refuses to be a \\clone origin for cones it does not cover. 'zit verify' counts elided \\artifacts and never reports elision as damage. \\ ; /// `zit help repo` (#68): the -R / --no-cache semantics, stated once. pub const repo_topic_help = \\zit help repo — reading a repository without a working tree \\ \\Read-only inspectors (log, timeline, leaves, branch, tag, info, ls, \\cat, artifact, annotate, frames, verify) and the sync family (pull, \\push, sync, remote — a bare mirror needs no working tree to talk to \\its peer) accept \\ \\ -R, --repo <path> \\ \\naming the repository directly: <path> is the directory holding '.zit' \\or the '.zit' file itself. No working tree is required or consulted. \\Verbs that read or write a checkout have nowhere to apply the \\override and refuse it. \\ \\Derived-state readers also accept --no-cache: bypass every derived \\sidecar (the cache AND the rebuild checkpoint) and re-derive from the \\full log, re-verifying whole-log integrity. The cache is refreshed as \\a side effect, so one --no-cache run also repairs a stale sidecar. \\ ; /// `zit help exit-codes` (#68): the global convention and every departure. pub const exit_codes_topic_help = \\zit help exit-codes — the exit-status contract \\ \\Every verb: \\ \\ 0 success \\ 1 operational failure (not a repository, unreadable state, a \\ refused action) — the diagnostic on stderr names it \\ 2 usage error (an unknown option or unexpected argument) \\ \\Departures: \\ \\ frames 3 = the cursor is current, no new artifacts (the trailer is \\ still emitted); 4 = the cursor is invalid after a store \\ rewrite — re-read from --since=0 \\ \\Success is silent where reasonable, diagnostics go to stderr, data to \\stdout — a script can consume stdout without filtering. \\ ; /// The one definition of the global `-R, --repo` option's help lines (#68): /// spliced into every read verb's options table, so an edit propagates and /// the e2e byte-pins catch it everywhere at once. pub const repo_option_help = \\ -R, --repo <path> \\ read this repository instead of the one found from the \\ current directory; <path> is the directory holding '.zit' \\ (or the '.zit' file itself). No working tree is required. \\ ; /// The one definition of the `--no-cache` option's help lines (#68). pub const no_cache_option_help = \\ --no-cache bypass every derived sidecar (the cache AND the rebuild \\ checkpoint) and re-derive from the full log, re-verifying \\ whole-log integrity (the cache is refreshed) \\ ; /// The conventional exit-status contract (#68): 0/1/2 exactly. Spliced into /// every help whose verb follows it; verbs that depart (frames, shun, /// commit, ...) write their own block. `zit help exit-codes` states the /// convention once, with the departures listed. pub const exit_status_conventional_help = \\ \\exit status: \\ 0 success \\ 1 operational failure (not a repository, unreadable state, a refused \\ action) — the diagnostic on stderr names it \\ 2 usage error (an unknown option or unexpected argument) \\ ; pub const purge_help = \\usage: zit purge checkins [--dry-run] [--explain] [--reason TEXT] [--force] [--no-cache] CHECKIN... \\ zit purge list \\ zit purge undo ID \\ zit purge obliterate [--force] ID... \\ \\Remove one or more check-ins and reclaim the bytes they introduced, keeping \\them recoverable. Removed artifacts go to this repository's graveyard, from \\which `zit purge undo` restores them and `zit purge obliterate` discards \\them for good. \\ \\This is the tool for the ordinary accident — an `add .` that swept in a \\build tree, noticed before anyone else has the check-in. It is NOT a \\takedown: a purge is local, so a peer that already holds the content keeps \\it, and a later sync will fetch it straight back. Use `zit shun` when the \\content must never exist here again. \\ \\Only leaves can be purged. A check-in some other check-in names as a parent \\cannot be removed without orphaning that child, so name the descendants too \\— `zit purge checkins A B` where B is A's child is accepted, because after \\the purge nothing is left pointing at either. The check-in your working tree \\is on cannot be purged at all; run `zit update` elsewhere first. \\ \\A blob is removed only when NO retained check-in still references it. A file \\the purged check-ins share with history that survives them stays exactly \\where it is; removing it would not free anything, it would only turn held \\content into a phantom that the next update fails on. Tags naming a purged \\check-in are removed with it. \\ \\By default purge refuses and removes nothing: it reports what would go and \\stops. Pass --force to proceed. purge never prompts. --dry-run reports the \\same plan and exits 0, for a script that wants the answer without the \\refusal. \\ \\Reclaiming rewrites the whole store into a new generation and swaps it in \\atomically, exactly as `zit pack` does; a crash before the swap leaves the \\original untouched and the graveyard unchanged. Only one repository rewrite \\runs at a time, so a concurrent `zit pack` or `zit shun --force` refuses \\this one rather than colliding with it. \\ \\The graveyard is local and disposable, like the undo journal: it is never an \\artifact, never syncs, and is not carried by a clone. Losing it costs \\recoverability, not data — the purge itself already happened. \\ \\options: \\ --dry-run report the plan and exit 0 without changing anything \\ --explain list every artifact the plan removes, not only the counts \\ --reason TEXT \\ record why, shown by `zit purge list` \\ --force actually remove \\ ++ no_cache_option_help ++ \\ -h, --help show this help and exit \\ \\exit status: \\ 0 the plan was reported (--dry-run), the graveyard was listed, or the \\ purge, undo or obliterate completed \\ 1 refused (no --force), a check-in could not be purged, another rewrite \\ is in progress, or not a zit repository \\ 2 usage error (an unknown option or an unexpected argument) \\ ; pub const log_help = \\usage: zit log [--long] [-n N] [-R <path>] CHECKIN [PATH] \\ \\Show CHECKIN followed by every reachable parent check-in in depth-first \\P-card order. CHECKIN accepts every form 'zit help names' lists — \\IDs, prefixes, branches, tags, reserved forms, and timestamps. Each \\check-in is shown once; a referenced parent missing from this \\repository is shown once as a phantom. \\ \\With PATH, show only the reachable check-ins that change PATH — where \\PATH's effective content differs from its primary parent's, including \\where PATH is added or deleted. PATH is an exact repo-relative path, not \\a pattern; a path that never changed yields no output. \\ \\--long adds each check-in's own facts after its id: \\ \\ checkin <id> <timestamp> <user> <comment> \\ \\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. Absent timestamp or user read '-'. \\ \\This record grammar is a contract: consumers parse it positionally, and \\no display option, present or future, will change it. Presentation \\extras (the timeline's --weekday, for one) belong to the verbs whose \\output is for eyes, not parsers. \\ \\options: \\ --long add timestamp, user and comment to each entry \\ -n, --limit N show only the N most recent entries; 0 means all ++ "\n" ++ repo_option_help ++ \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; pub const open_help = \\usage: zit open [CHECKIN | URL] [--store=PATH] [--manifest[=r,u,t]] \\ [--cone=PATH] [--cone-not=PATH] [--cone-file=FILE] \\ [-h|--help] \\ \\Materialize a check-in in the working tree of a not-yet-open checkout. \\Given a URL instead, this is 'zit clone URL': the repository is cloned \\into a directory named from the URL and opened there. \\This is the second half of 'zit clone URL DIR': clone builds DIR and its \\'.zit', open fills DIR with the files of a check-in. Run it from inside \\the checkout directory. \\ \\Without CHECKIN, the tip of this repository's main branch is opened. \\CHECKIN accepts every form 'zit help names' lists — IDs, prefixes, \\branches, tags, reserved forms, and timestamps — so a fresh clone can \\open straight at a release tag without materializing the tip first. \\ \\A working tree that is already open at a check-in is left untouched; use \\'zit update' to move an open tree to a different check-in. A repository \\with no check-in yet is already in its correct (empty) state, which open \\reports and leaves alone — though a CHECKIN named explicitly must \\resolve, so in an empty repository it fails as not found. \\ \\--manifest asks this checkout for the files some build systems read to \\stamp a version into their output: 'manifest' (the check-in artifact, r), \\'manifest.uuid' (its hash, u) and 'manifest.tags' (its symbolic tags, t). \\The bare flag or 'on' means 'ru'; 'off' writes none, whatever the \\project's 'manifest' setting says. The choice is remembered, so every \\later 'zit update' rewrites the files for the check-in it moves to. \\Without the flag the project's 'manifest' setting decides, and a file of \\one of those names that the check-in itself tracks is never replaced. \\ \\--store=PATH opens a second working tree of an EXISTING repository: run \\it in an empty directory and name the checkout (or the '.zit' file) that \\holds the store. The new tree keeps its own checkout state but reads and \\writes the one shared store, so a commit in either tree is visible to \\the other immediately; concurrent commits serialize on the store's own \\lock. The directory must not already be a checkout. \\ \\--store on a tree that is ALREADY open re-points it instead: after the \\repository moves, run open --store=NEWPATH in the stranded tree and it \\resumes where it was, uncommitted work intact. The new PATH must hold \\the same repository -- one containing this tree's current check-in -- \\so a tree cannot be re-pointed at an unrelated store. \\ \\options: \\ --store=PATH create a detached working tree of the repository \\ at PATH before opening \\ --manifest[=r,u,t] write manifest, manifest.uuid, manifest.tags \\ --cone=PATH materialize and track only PATH's subtree \\ (repeatable) — a sparse checkout from the start \\ --cone-not=PATH exclude PATH's subtree from an enclosing --cone \\ member (repeatable); the most specific member \\ decides coverage \\ --cone-file=FILE read cone members from FILE, one per line \\ ('#' comments and blank lines are skipped; \\ a '!' prefix marks an exclusion) \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; pub const update_help = withAliases(.update, \\usage: zit update [-n|--dry-run] [-v|--verbose] [--no-sync] [--cone=PATH] \\ [--cone-not=PATH] [--cone-file=FILE] [--dense] \\ [--latest] [CHECKIN] \\ \\Materialize CHECKIN in the working tree. CHECKIN resolves like 'zit info' \\— a full or abbreviated artifact ID, a branch, or a tag. With no \\CHECKIN (or with --latest) update moves to the current branch's most \\recent check-in, as Fossil's bare 'update' does. With autosync on and a \\saved remote, update pulls first so CHECKIN resolves against the \\freshest history. \\ \\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 the check-in you \\left and the one you are on, its 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 moving to a branch tip that advanced under you leaves your \\edits in place on top of it and the next commit is an ordinary child of \\that tip. Conflicts are written into the working files with markers and \\listed on stderr; update then exits 1 so a script notices. A file you \\changed that CHECKIN deletes or changes the type of cannot be merged \\either way — update names it and refuses, changing nothing. \\ \\To discard local changes instead of carrying them, run 'zit revert' first. \\ \\options: \\ --cone=PATH materialize and track only PATH's subtree (repeatable); \\ replaces the checkout's stored sparse cone \\ --cone-not=PATH exclude PATH's subtree from an enclosing --cone \\ member (repeatable); the most specific member decides \\ --cone-file=FILE read cone members from FILE, one per line \\ ('#' comments and blank lines are skipped; a '!' \\ prefix marks an exclusion) \\ --dense drop the stored cone and rematerialize everything \\ --no-sync skip the autosync pull for this invocation \\ -n, --dry-run print the receipt; write nothing \\ --latest update to the current branch's most recent check-in \\ -v, --verbose list unchanged files in the receipt as well \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help); pub const merge_help = \\usage: zit merge [-X ours|theirs] [--no-sync] [--no-precedent] \\ [--baseline B] [--integrate] [NAME] \\ zit merge --probe|-n|--dry-run NAME [--into TARGET] \\ zit merge --cherrypick NAME \\ zit merge --backout NAME \\ \\Merge the check-in NAME into the working tree with a three-way merge and \\record it as a pending merge parent. NAME resolves like 'zit info' — a full \\or abbreviated artifact ID, a branch, or a tag; with no NAME, the one \\other open leaf is merged (several is an error naming them). This does \\not create a check-in; run 'zit commit' to record the merge. Conflicts are written into \\the working files with conflict markers and listed on stderr — resolve them \\and commit, or run 'zit undo' to abandon the merge. \\ \\Uncommitted work does not block the merge: your working file is the merge's \\own "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, the same as any other conflict, and 'zit undo' restores the \\uncommitted bytes. \\ \\-X resolves every CONTENT conflict in favour of one side instead of writing \\markers: 'ours' keeps the working tree's version wherever the two sides \\disagree, 'theirs' takes NAME's. Regions only one side changed are 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". The mechanical cases \\are what it is for: a regenerated lockfile, a vendored tree taken whole \\from upstream. \\ \\--probe answers "would this merge cleanly?" and writes NOTHING -- no merge, \\no working tree touched, no autosync. It prints one line, `merges` followed \\by clean, up-to-date, unrelated, or conflict (N), and then one \\`conflict PATH` line per unresolvable path. Exit status is 0 in every one \\of those cases: "has conflicts" is an answer, not a failure, so a caller \\reads the line rather than the status. \\ \\Being a read, --probe 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, exactly as a real merge \\takes its other side. A real merge refuses -R: it writes a working tree. \\ \\-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, which is the misread worth avoiding. \\ \\One case -X deliberately does not answer: 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 for you to settle, with or without -X. \\ \\--cherrypick and --backout are special merges that pick an alternate \\baseline instead of the common ancestor: --cherrypick applies only NAME's \\own change onto the working tree, --backout reverses it. 'zit commit' then \\records the operation as a cherry-pick/backout, not as a merge parent. \\ \\options: \\ -h, --help show this help and exit \\ -X, --strategy ours|theirs \\ resolve content conflicts in favour of one side \\ --no-sync skip the autosync pull for this invocation \\ --no-precedent do not replay remembered conflict resolutions for \\ this merge (see 'zit precedent'); nothing is \\ forgotten, only declined \\ --cherrypick NAME apply only NAME's own change onto the working tree \\ --backout NAME reverse NAME's change on the working tree \\ -n, --dry-run the same as --probe \\ --baseline B use check-in B as the common ancestor instead of \\ the computed merge base \\ --integrate close NAME's leaf when this merge is committed \\ ++ exit_status_conventional_help; pub const bisect_help = \\usage: zit bisect bad NAME | good NAME | next | status | reset \\ \\Binary-search the check-in DAG for the check-in that introduced a defect. \\Mark a known-bad and a known-good check-in; zit narrows the candidate span \\to the ancestors of the bad check-in that are not ancestors of any good one \\and moves the working tree to the midpoint for you to test. Repeat until one \\check-in remains — the first bad check-in. \\ \\subcommands: \\ bad NAME mark NAME as containing the defect \\ good NAME mark NAME as free of the defect \\ next move to (or re-announce) the current probe check-in \\ status show the marks, the current probe, and the remaining count \\ reset end the bisection and restore the starting check-in \\ \\NAME resolves like 'zit info' — a full or abbreviated artifact ID, a branch, \\or a tag. Marking moves the working tree through the update engine, so a move \\is refused over a dirty working tree and is recoverable with 'zit undo'. The \\bisection state lives in .zit-checkout and never enters the repository. \\ \\options: \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; pub const crosslink_help = \\usage: zit crosslink [--no-cache] \\ \\Stream every check-in's DERIVED record for an external consumer (#109): \\identity, timestamp, author, comment, tag operations, and the set of \\files the check-in changed relative to its primary parent -- the answers \\an indexer otherwise re-derives by parsing and expanding every manifest \\itself. The first line names the stream format: \\ \\ crosslink 1 \\ \\then, per check-in, in the store's persisted order: \\ \\ checkin HASH MTIME [PARENT ...] \\ user LOGIN (omitted when the manifest has none) \\ comment LEN \\ <LEN raw bytes> \\ tag (+|-|*)NAME (TARGET|*) [VALUE] \\ file (A|M|D) PATH \\ end \\ \\MTIME is the D card as milliseconds since the Unix epoch. LOGIN, NAME, \\VALUE and PATH are card-escaped exactly as an artifact carries them, so \\every line is newline-safe and the artifact-format decoder a consumer \\already has applies. A rename reads as a delete plus an add. In place of \\file lines a record may say \\ \\ files unknown \\ \\when the change set could not be determined at derivation time -- a parent \\the store does not hold, or a sub-manifest that is absent or unreadable. \\Unknown is deliberate: an empty change set is a claim, and this verb never \\claims what it did not compute. A tree (sub-manifest) check-in is diffed \\by S card and reports its files like any other. \\ \\Records are served from the derived cache when it provably covers the \\whole log (the same key-and-shun-fingerprint discipline every read verb \\follows) and derived from the log when it does not. --no-cache forces \\the derivation and the two outputs are byte-identical -- pinned by test. \\ \\options: \\ --no-cache derive from the log, bypassing the cache ++ "\n" ++ repo_option_help ++ \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; pub const frames_help = \\usage: zit frames [--since=CURSOR] [--verify=TOKEN] [--structural] \\ \\Stream every artifact appended after CURSOR as raw bytes, for an \\external incremental consumer (an indexer polling through the zit \\binary). Each artifact is framed as a header line \\ \\ artifact HASH SIZE \\ \\followed by exactly SIZE raw bytes (the whole artifact, byte-identical \\to 'zit artifact HASH --raw' — delta storage is resolved before \\emission) and a newline. After the last artifact one trailer line \\ \\ cursor CURSOR TOKEN \\ \\names the position this invocation reached; pass both values back as \\--since and --verify on the next poll. Both are opaque: their content \\may change with the store format, and only round-tripping them is \\supported. Without --since the full artifact census streams from the \\beginning (the bootstrap read). \\ \\--verify answers whether the store prefix the consumer already consumed \\is still byte-identical. Appends (commits, sync arrivals) keep a cursor \\valid; a store rewrite ('zit pack', a shun reclaim) relocates frames and \\fails validation with exit 4 — the cursor is dead, and the consumer's \\remedy is a re-read from --since=0 (derived indexes rebuild; the loss is \\detectable by design, never silent). A shun recorded with --defer masks \\content without moving bytes: the cursor stays valid, already-emitted \\artifacts are not retracted, and the reclaiming rewrite invalidates. \\ \\Shun-masked artifacts and phantoms (known names whose bytes have not \\arrived) are skipped; the cursor still advances over them. \\ \\--structural emits only artifacts that parse as structural artifacts \\(manifests, control artifacts, wiki, tickets, technotes) -- the same \\gate the repository's own crosslink applies -- and skips content \\blobs, which dominate the bytes of a full stream. Skipped frames are \\not hashed or emitted; the cursor still advances over them, so the \\flag never changes what a later poll sees and a consumer may mix \\filtered and unfiltered polls against one cursor. A poll whose new \\frames are all content emits nothing and exits 3, trailer included. \\ \\options: \\ --since=CURSOR emit only artifacts appended after CURSOR \\ --verify=TOKEN validate the consumed prefix before emitting \\ --structural emit only structural artifacts; skip content blobs ++ "\n" ++ repo_option_help ++ \\ -h, --help show this help and exit \\ \\exit status: \\ 0 one or more artifacts were emitted \\ 1 the repository cannot be read \\ 2 usage error (an unknown option or an unexpected argument) \\ 3 the cursor is current: no new artifacts (trailer still emitted) \\ 4 the cursor is invalid: the store was rewritten; re-read from zero \\ ; pub const import_fossil_help = \\usage: zit import-fossil SOURCE DIR (--compatible | --submanifest) \\ [--k N] [-h|--help] \\ \\Bring a fossil repository's history into zit. 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. The MODE IS THE DECISION and it is permanent, so there is no \\default: \\ \\--compatible preserves the artifact corpus verbatim: 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 and with every clone of it. This is the clone \\machinery wearing its import name; fossil's sync protocol is the import \\path and no SQLite is ever read. \\ \\--submanifest is a REBIRTH: the fossil is cloned into a transient store \\beside DIR (skipped when SOURCE already is one), then 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: the result can never sync \\with the origin or its clones, PGP signatures cannot survive rehashing \\(stripped and counted), and external hash citations resolve only \\through the shipped provenance map. 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, and every pair is \\proven file-set-equivalent before anything is published. 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. \\ \\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. Note that \\'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. \\ \\options: \\ --compatible verbatim corpus; stays in the origin's sync world \\ --submanifest re-mint as sub-manifest trees; new project code \\ --k N submanifest only: pin the partition parameter \\ (1..12) instead of sweeping for it \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; pub const annotate_help = withAliases(.annotate, \\usage: zit annotate PATH [VERSION] [--full] [-n N] [-l] \\ \\Show, for each line of PATH, the check-in that introduced that line and \\when. PATH is an exact repo-relative path to a tracked file; each output \\line is the introducing check-in's 12-character artifact-ID prefix, the \\ISO date it was recorded (UTC), then the file line. \\ \\VERSION selects the annotated check-in the same way 'zit info' does; with \\it omitted, the current checkout is annotated. With -R and a VERSION no \\working tree is required. \\ \\Provenance is computed over the file's whole version history: a line is \\attributed to the oldest check-in that carries it. A line that arrived from \\a merged branch is credited to its origin on that branch, never to the merge \\check-in itself. Only committed content is annotated — uncommitted edits in \\the working file are not shown. \\ \\options: \\ -n, --limit N look back at most N check-ins; a line older than the \\ window is credited to the oldest check-in shown \\ -l, --log list the check-ins consulted (newest first) before \\ the lines, separated by a '---' line \\ --full emit the full artifact ID per line instead of the \\ 10-character prefix (stable as an index key) ++ "\n" ++ repo_option_help ++ \\ -h, --help show this help and exit \\ \\exit status: \\ 0 the annotation was written \\ 1 PATH is not tracked at the annotated check-in, its content is \\ binary, VERSION does not resolve, or this is not a zit repository \\ 2 usage error (a missing PATH, an unknown option, or a third positional) \\ ); pub const revert_help = \\usage: zit revert [-r VERSION] [-n] [PATH...] \\ \\Restore managed files to their contents at the current check-in, discarding \\working-tree edits. With no PATH, every managed file is reverted; name one \\or more paths to revert just those. -r VERSION takes the contents from \\another check-in instead, leaving the checkout where it is (the tree then \\shows those files as edited); -n names what would be restored and writes \\nothing. A revert is undone with 'zit undo'. \\ \\revert changes file contents only — it never changes which files are \\tracked. A managed file that the current check-in does not contain (added \\but never committed, or deleted at this check-in) is reported and left \\alone, never recreated. \\ \\options: \\ -r, --revision VERSION \\ restore from VERSION (any check-in name) instead of the \\ current check-in \\ -n, --dry-run print 'would revert PATH' per file; write nothing \\ -h, --help show this help and exit \\ \\exit status: \\ 0 the named files were reverted (or had nothing to restore) \\ 1 a path is not tracked, a file could not be written, the checkout is \\ unborn, or this is not a zit repository \\ 2 usage error (an unknown option) \\ ; pub const undo_help = \\usage: zit undo [-n|--dry-run] [-h|--help] \\ \\Undo the last working-file change made by 'zit update' or 'zit revert', \\restoring every affected file and the checkout record to their state before \\that change. Single level: only the most recent change can be undone; a second \\'zit undo' in a row has nothing left to undo. \\ \\Use 'zit redo' to reapply a change you just undid. A new update or revert \\clears the redo slot. \\ \\options: \\ -n, --dry-run name the files (and the checkout record) a undo would \\ restore; change nothing \\ -h, --help show this help and exit \\ \\exit status: \\ 0 the change was undone \\ 1 nothing to undo, the journal is damaged, or this is not a zit repository \\ 2 usage error (an unknown option or an unexpected argument) \\ ; pub const redo_help = \\usage: zit redo [-n|--dry-run] [-h|--help] \\ \\Reapply the change you just undid with 'zit undo', restoring every affected \\file and the checkout record to their state after that change. Single level: \\redo is available only immediately after an undo; a new update or revert \\clears it. \\ \\options: \\ -n, --dry-run name the files (and the checkout record) a redo would \\ restore; change nothing \\ -h, --help show this help and exit \\ \\exit status: \\ 0 the change was reapplied \\ 1 nothing to redo, the journal is damaged, or this is not a zit repository \\ 2 usage error (an unknown option or an unexpected argument) \\ ; pub const pack_help = \\usage: zit pack [--drop-elided] [-h|--help] \\ \\Repack this repository's store into a compact new generation, re-encoding \\full-form artifacts as deltas against their lineage (a check-in against its \\parent, a file against the same path in the parent check-in) and dropping \\superseded config records. Apart from shunned content (below), every \\artifact's content and name are preserved exactly; the on-disk layout is the \\only thing that changes. \\ \\pack is also where shunned content is RECLAIMED. A shun only masks an \\artifact from being read or served; its bytes stay in the store until a pack \\rewrites the store without them. A run that removed something says how many \\in its summary line. The tombstones themselves are kept, so a reclaimed \\artifact still reads as absent and syncing will not fetch it back. \\Reclamation is not reversible: once packed, the bytes are gone. Reclaiming an \\artifact that other artifacts were stored as deltas against can leave the \\store slightly LARGER, since its content has to move into one of them. \\ \\pack streams the whole store, verifying every frame's checksum and that each \\reconstructed artifact still hashes to its name — so it doubles as a \\whole-store integrity check. Reclaimed artifacts are never reconstructed, so \\a shunned artifact whose content no longer matches its name does not block \\the pack that removes it. A frame whose CHECKSUM is damaged still does: the \\checksum pre-flight covers the whole store and runs before any artifact is \\reclaimed. The packed log is written to a temporary file and swapped into \\place atomically: a crash before the swap leaves the original untouched. It \\transiently needs room for both the old and new store. \\ \\--drop-elided is the sparse-cone narrow-then-compact deletion, and the \\only one the sparse spec defines: it drops the blobs this store's cone \\ELIDES -- content referenced exclusively out-of-cone -- composed with the \\ordinary shun reclaim above. It is refused without a store cone on record, \\because with no policy there is nothing "elided" to drop and a bare pack \\must never guess one. Like shun reclamation it is NOT reversible; unlike \\shun it leaves no tombstone, so a later widening re-fetches the content \\from a peer rather than reading it as absent. \\ \\options: \\ --drop-elided also drop blobs the store cone elides (needs a cone) \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; /// #71 machine verbs. Deliberately ABSENT from the top-level command list: /// they exist for another process (quarry), and the porcelain a person /// browses should not offer them. `zit wiki --help` and `zit help wiki` /// still answer in full — unlisted, never undocumented. pub const wiki_help = \\usage: zit wiki commit <title> [-R <repo>] [--user NAME] \\ [--mimetype TYPE] < body \\ \\Record a wiki artifact for <title>, body from stdin (read to EOF). \\A machine verb: stdout carries exactly the new artifact hash, one \\line; diagnostics go to stderr. Operates on a bare store via -R — \\no working tree is required. Concurrent writers serialize on the \\store's write lock. \\ \\--user is the recorded author (default: $USER); an empty author is \\refused. --mimetype records an N card (e.g. text/x-markdown); \\without it readers assume Fossil wiki markup. \\ \\options: \\ --user NAME author to record \\ --mimetype TYPE the body's mimetype \\ -h, --help show this help and exit \\ \\exit status: \\ 0 the artifact was recorded; its hash is on stdout \\ 1 user error (empty title or author, unreadable store) \\ 2 usage error \\ ; pub const ticket_help = \\usage: zit ticket new [-R <repo>] [--user NAME] field=value ... \\ zit ticket set <ticket-id> [-R <repo>] [--user NAME] field=value ... \\ \\Record a ticket-change artifact: one J card per field=value (sorted; \\a repeated field is refused). `new` mints a fresh 40-hex ticket id \\and prints "<ticket-id> <artifact-hash>"; `set` requires an id some \\stored ticket artifact already carries and prints the artifact hash. \\A machine verb: stdout carries exactly that one line; diagnostics go \\to stderr. Operates on a bare store via -R. Concurrent writers \\serialize on the store's write lock. \\ \\options: \\ --user NAME author to record (default: $USER); empty is refused \\ -h, --help show this help and exit \\ \\exit status: \\ 0 recorded; the result line is on stdout \\ 1 user error (unknown ticket id, no fields, empty author) \\ 2 usage error \\ ; pub const forum_help = \\usage: zit forum post [-R <repo>] [--user NAME] [--title TITLE] \\ [--mimetype TYPE] < body \\ zit forum reply <post-hash> [-R <repo>] [--user NAME] \\ [--mimetype TYPE] < body \\ \\Record a forum artifact: `post` opens a new thread (--title is the \\thread's H card; the body from stdin is the W card); `reply` answers \\<post-hash> inside its thread, recording G (the thread root, resolved \\from the replied-to post) and I (in-reply-to) cards. A machine verb: \\stdout carries exactly the new artifact hash; diagnostics go to \\stderr. Operates on a bare store via -R; writers serialize on the \\store's write lock. \\ \\options: \\ --user NAME author to record (default: $USER); empty is refused \\ --title TITLE the thread title (post only; a reply inherits it) \\ --mimetype TYPE the body's mimetype (N card), e.g. text/x-markdown; \\ omitted, the reader applies its own default \\ -h, --help show this help and exit \\ \\exit status: \\ 0 recorded; the artifact hash is on stdout \\ 1 user error (unknown post, empty body or author) \\ 2 usage error \\ ; pub const attach_help = \\usage: 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] \\ \\Attach a file to a ticket, technote, forum post (TARGET is its hash, \\full or abbreviated) or wiki page (TARGET is the page name). `add` \\stores FILE as a blob, then an attachment artifact naming it; FILE may \\be - for stdin, which needs --name. NAME defaults to FILE's final path \\component; a directory prefix is always discarded. Prints the \\attachment artifact's hash, then the blob's hash. `rm` records that \\NAME is no longer attached (nothing leaves the store). `list` prints \\the live attachments as NAME SIZE MIMETYPE BLOB DATE USER, with the \\target first when listing the whole repository; a quarry attachment \\stub is shown as external with the size it points at. \\ \\The setting attachment-size-limit caps what `add` stores, in bytes \\(0 = unlimited); unset, it is 1 MiB. A machine verb: diagnostics go \\to stderr; operates on a bare store via -R. \\ ; pub const technote_help = \\usage: zit technote add [-R <repo>] [--user NAME] [--comment TEXT] \\ [--tag NAME]... [--mimetype TYPE] \\ [--event EVENT-ID] [--time TIMESTAMP] < body \\ \\Record a technote (event) artifact, body from stdin. --comment is the \\note's title (C card), which is what a timeline renders as its name — \\a note recorded without one shows its event id instead, and no later \\verb can attach the card, so give it here or not at all. --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 event timestamp (default: now); the D card is always the writer \\clock. Prints "<event-id> <artifact-hash>" on stdout. A machine verb: \\diagnostics go to stderr; operates on a bare store via -R; writers \\serialize on the store's write lock. \\ \\options: \\ --user NAME author to record (default: $USER); empty refused \\ --comment TEXT the note's title (C card); empty is refused \\ --tag NAME a symbolic tag the note is found by, written \\ 'T +sym-NAME *' as fossil writes it. Repeatable \\ (at most 64); names carrying whitespace, and the \\ same name twice, are refused \\ --mimetype TYPE the body's mimetype (N card) \\ --event EVENT-ID the technote to add an edit to \\ --time TIMESTAMP the event's own timestamp (E card) \\ -h, --help show this help and exit \\ \\exit status: \\ 0 recorded; "<event-id> <artifact-hash>" is on stdout \\ 1 user error (bad event id or timestamp, empty author) \\ 2 usage error \\ ; pub const verify_help = \\usage: zit verify [-R <path>] [-h|--help] \\ \\Check this repository's store for damage. verify is zit's own fsck and a \\local design decision, not a verb any specification requires: repo-state \\§7.3 blesses rebuilding a repository from its artifacts as a recovery \\fallback, but it says nothing about how a check should look, and nothing \\here claims otherwise. \\ \\It makes two passes. The first reads every frame in the store and checks \\its whole-frame checksum, which catches a bitflip in bytes already on disk. \\The second reconstructs every stored artifact through its delta chain and \\re-hashes it against its own name, which catches a frame whose checksum is \\perfectly valid over the wrong bytes. Each finding names what is damaged \\and where, and the summary says how much of the store was actually read. \\ \\verify is read-only. It takes no lock, writes nothing, and never truncates, \\so it is safe to run against a repository a `zit serve` has open. \\ \\Four things it deliberately does not tell you. Config records, shun rows \\and user rows are checksum-checked but not structurally verified, because \\they are never interned as artifacts and the per-artifact pass cannot see \\them. A manifest \\that hashes correctly but is semantically malformed is a zit bug, not \\storage damage, and is out of scope. Derived caches are not checked; a \\stale cache is not damage. And the walk covers the store as it was when \\verify started, so frames a concurrent `zit commit` appends while it runs \\are not examined. \\ \\An incomplete final frame — the ordinary result of a write interrupted by a \\crash — is reported but is not a fault: the next write truncates it away. \\ \\Shunned artifacts whose bytes have not yet been reclaimed ARE verified and \\reported, and are labelled as such. This differs from `zit pack`, which \\filters them out before verifying so that damage cannot block its own \\removal; verify has no removal to block, and must not report clean about \\bytes it declined to look at. \\ \\options: ++ "\n" ++ repo_option_help ++ \\ -h, --help show this help and exit \\ \\exit status: \\ 0 no fault was found (an incomplete final frame from an interrupted write \\ is reported but is not a fault) \\ 1 a fault was found, the store could not be read, or not a zit repository \\ 2 usage error (an unknown option or an unexpected argument) \\ ; pub const users_help = \\usage: zit users [--raw] [-R <path>] [-h|--help] \\ \\Print the repository's user table -- the logins, capabilities, and \\account metadata a fossil repository carries, as brought across by \\`zit config import` (or, from a peer, `zit config pull user`). \\ \\Output is one row per line, TAB-separated and sorted by login, so it \\reads by machine: \\ \\ LOGIN <TAB> CAPS <TAB> MTIME <TAB> INFO \\ \\and with --raw the password hash is included, after CAPS: \\ \\ LOGIN <TAB> CAPS <TAB> PWHASH <TAB> MTIME <TAB> INFO \\ \\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. \\ \\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. \\ \\A repository that has never had a user table imported prints nothing \\and exits 0: an empty table is a fact, not a failure. \\ \\options: \\ --raw include the password hash column ++ "\n" ++ repo_option_help ++ \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; pub const shun_help = \\usage: zit shun [--reason TEXT] [--defer] [--force] HASH... \\ zit shun --list \\ \\Permanently remove one or more artifacts from this repository and record \\them so they are never fetched again. This is irreversible: `zit undo` does \\not recover a shun, and the content cannot be re-fetched from a peer. \\ \\By default shun refuses and removes nothing: it reports exactly what would \\be destroyed and stops. Pass --force to proceed. shun never prompts. \\ \\Each hash is recorded in this repository's shun table, and every sync \\direction honours it: the artifact is never requested from a peer, never \\announced or served to one, and an arriving copy is discarded rather than \\stored. A peer that still holds it can keep offering it; a later sync will not \\land it back in this store. Shunning is not deletion at the peer: a peer that \\has not shunned the artifact keeps it. \\ \\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. That 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, and a clone can never come up serving content its \\origin was ordered to remove. \\ \\A hash this repository does not hold may still be shunned; the entry is \\recorded and blocks a future fetch, and no rewrite runs because there is \\nothing to reclaim. Give such a hash in full: an abbreviated prefix that \\matches nothing here is refused, because the table is keyed by complete \\artifact name and a prefix cannot be completed. \\ \\A hash whose row already exists but whose bytes are still on disk — anything \\shunned earlier with --defer — is reclaimed by --force, not reported as a \\no-op. Only a hash that is both recorded and gone is nothing to do. \\ \\The check-in your working tree is currently on cannot be shunned, with or \\without --force; run `zit update` to a different check-in first. \\ \\Removing the bytes rewrites the whole store into a new generation and swaps \\it in atomically, exactly as `zit pack` does; a crash before the swap leaves \\the original untouched, with no tombstone recorded. It transiently needs \\room for both the old and new store, and it also reclaims any shun that was \\deferred earlier. \\ \\Pass --defer to record the shun without rewriting. Reads answer identically \\either way, but --defer does NOT make the content unreachable: until \\`zit pack` runs, the bytes remain on disk and stay reconstructible through \\any delta stored against them, because the mask is on the name rather than \\the bytes. Only the rewrite removes them. --defer still needs --force: the \\tombstone it appends is just as irreversible. \\ \\A `zit serve` already running when a shun lands picks it up on its next \\request, with no restart: a --defer row is appended to the store it has \\open, and a --force reclaim's rewritten store is noticed by the server's \\per-round path check, which reopens the new generation before answering. \\ \\Only one repository rewrite runs at a time: if a `zit pack` or another \\`zit shun --force` is already rewriting this store, this one is refused \\rather than allowed to collide with it. A --defer run is refused the same \\way, because a row appended while another process was swapping in a new \\generation would be lost with the old one. Nothing is recorded when that \\happens. \\ \\A shun is also refused if the store changed between the moment it was \\examined and the moment it would be written — a `zit commit` landing one of \\the named hashes, say. The report would otherwise describe a repository that \\no longer exists. Nothing is recorded; run it again. \\ \\--reason must be a single line: a newline in the note would forge a row in \\--list, the only view of a record that cannot be removed. \\ \\options: \\ --reason TEXT record a note with the shun (shown by --list) \\ --defer record the shun; leave the bytes until `zit pack` \\ --force actually record and remove \\ --list list this repository's shun table and exit \\ -h, --help show this help and exit \\ \\exit status: \\ 0 the shun table was listed, every hash was shunned, or every hash was \\ already shunned AND already gone (a no-op needs no --force) \\ 1 refused (no --force), a hash could not be shunned, another rewrite is in \\ progress, the repository changed while it was being examined, or not a \\ zit repository \\ 2 usage error (an unknown option or an unexpected argument) \\ ; /// zit#165: the browser view is quarry's single-repository local mode /// (quarry RFC 0010); this verb is its launcher and knows nothing of its flags. pub const ui_help = \\usage: zit ui [-h|--help] [QUARRY-ARGS...] \\ \\Open this repository in a browser: timeline with a graph, side-by-side \\diffs, a file browser, annotate, branches — Fossil's 'fossil ui'. \\ \\That view is quarry's, zit's forge, in its single-repository local mode: \\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. The two pages people open most also render statically: \\ \\ zit diff --by side-by-side HTML for any diff, opened in the \\ browser from a terminal \\ zit timeline -F html the timeline as a page on stdout \\ \\options: \\ -h, --help show this help and exit (anything else passes through) \\ \\exit status: \\ 0 quarry exited 0 (or help shown) \\ 1 quarry could not be started, its exit status otherwise, or this is \\ not a zit repository \\ 2 usage error \\ ; fn runUi(gpa: std.mem.Allocator, io: std.Io, arena: std.mem.Allocator, verb_args: []const []const u8, streams: cli.Streams, amb: cli.Ambient) cli.Exit { if (verb_args.len != 0 and (std.mem.eql(u8, verb_args[0], "-h") or std.mem.eql(u8, verb_args[0], "--help"))) { streams.out.writeAll(ui_help) catch {}; return .ok; } const root = (cli.discoverRoot(arena, io) catch return stateOom(streams, "ui")) orelse { streams.err.writeAll(not_a_repo_msg) catch {}; return .failure; }; var root_dir = root.dir; root_dir.close(io); var env_map = amb.environ.createMap(gpa) catch return stateOom(streams, "ui"); defer env_map.deinit(); const bin: []const u8 = env_map.get("ZIT_QUARRY") orelse "quarry"; var argv: std.ArrayList([]const u8) = .empty; argv.append(arena, bin) catch return stateOom(streams, "ui"); argv.append(arena, "ui") catch return stateOom(streams, "ui"); argv.append(arena, root.abs) catch return stateOom(streams, "ui"); for (verb_args) |a| argv.append(arena, a) catch return stateOom(streams, "ui"); streams.out.flush() catch {}; streams.err.flush() catch {}; var child = std.process.spawn(io, .{ .argv = argv.items, .environ_map = &env_map }) catch { streams.err.print( "zit ui: '{s}' could not be started; the browser view is quarry's single-repository local mode\n" ++ "install quarry (https://github.com/danmestas/quarry) or set ZIT_QUARRY to the binary\n" ++ "meanwhile: 'zit diff --by' renders a side-by-side page and 'zit timeline -F html' renders the timeline\n", .{bin}, ) catch {}; return .failure; }; const term = child.wait(io) catch return .failure; return switch (term) { .exited => |code| if (code == 0) .ok else .failure, else => .failure, }; } pub const leaves_help = \\usage: zit leaves [--all] [--no-cache] [-h|--help] \\ \\List the open leaves of the check-in DAG — check-ins with no child on the \\same branch that carry no active closed tag. Each line is Fossil's: \\`(N) time [hash] comment (user: … tags: …)`, with the weekday and a \\twelve-character hash; the checkout's leaf carries *CURRENT*, as in \\timeline. Leaves are ordered most-recent first. \\ \\options: \\ --all also list closed leaves, tagged "closed" ++ "\n" ++ no_cache_option_help ++ repo_option_help ++ \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; pub const timeline_help = \\usage: zit timeline [-n N] [--offset N] [-b BRANCH | -c] [-p PATH] [-u LOGIN] \\ [-r] [-v] [-F oneline|medium|full|html] [--weekday] \\ [-R <path>] [-h|--help] \\ \\List every check-in in the repository, most recent first. Each line shows \\the check-in's short hash, its ISO timestamp, its branch, and the first \\line of its comment. A check-in that joins two or more lines of development \\is marked "M". Order is by event time descending, ties broken by artifact \\hash, so the listing is byte-identical across runs. \\ \\Filters narrow the listing: -b BRANCH to one branch, -c to the checkout's \\branch, -p PATH to the check-ins that change PATH against their primary \\parent, -u LOGIN to one author. -r lists oldest first; --offset N skips \\the first N matches before -n counts. The default (-F medium) is \\Fossil's shape — a date header once per day, with the weekday, then \\time, a twelve-character hash, *CURRENT* on the checkout's check-in, \\the comment, and (user: … tags: …); -F oneline is one line per \\check-in (hash, ISO time, branch, comment) for scripts; -F full adds \\each check-in's changed files, which -v adds to any format. \\ \\--weekday prints each check-in's day of week between the hash and the \\date. Days are UTC, like every timestamp here, so a late Friday commit \\in your local zone can read Saturday. The computation is Ben Joffe's \\branchless day-of-week (benjoffe.com/fast-day-of-week). \\ \\options: \\ -n, --limit N show only the N most recent entries; 0 means all \\ --offset N skip the first N matching entries \\ -b, --branch B only check-ins on branch B \\ -c, --current-branch only check-ins on the checkout's branch \\ -p, --path P only check-ins that change P \\ -u, --for-user U only check-ins by U \\ -r, --reverse oldest first \\ -v, --verbose list each check-in's changed files \\ -F, --format F oneline (default), medium, or full; \\ --oneline/--medium/--full are the same \\ --weekday show the UTC day of week beside each date ++ "\n" ++ repo_option_help ++ \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; pub const branch_help = \\usage: zit branch [list] [-c] [-t] [-r] [-v] [--no-cache] \\ zit branch new NAME [BASIS] \\ zit branch (close | reopen | hide | unhide) NAME \\ zit branch current \\ zit branch info NAME \\ \\List the repository's branches, one per line, ordered by name. Branches are \\derived from the active branch tags of the check-ins; the branch of the current \\checkout is marked with a leading "*". A branch whose most recent check-in is \\closed is marked "(closed)". \\ \\The other actions are Fossil's (zit#164). 'new' records a check-in that \\changes no file and starts NAME at the checkout's check-in (BASIS, when \\given, must be that check-in — update to it first). 'close' and 'reopen' \\add and cancel the closed tag on NAME's tip; 'hide' and 'unhide' do the \\same with the hidden tag. 'current' prints the checkout's branch; 'info' \\prints NAME's tip, count and root. \\ \\options: \\ -v, --verbose each branch with its tip and time \\ -c, --closed list closed branches only \\ -t most recently changed first \\ -r reverse the order ++ "\n" ++ no_cache_option_help ++ repo_option_help ++ \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; pub const landing_help = \\usage: zit landing open --branch NAME [--to TARGET] -m MESSAGE [--user NAME] \\ [--mimetype TYPE] \\ zit landing ls [--all] \\ zit landing show ID \\ zit landing comment ID -m MESSAGE [--reply-to POST] [--user NAME] \\ zit landing accept ID [-m MESSAGE] [--user NAME] \\ zit landing reject ID [-m MESSAGE] [--user NAME] \\ zit landing retarget ID --to TARGET [--user NAME] \\ zit landing update ID [-m MESSAGE] [--user NAME] \\ \\Propose a branch to land on a target branch, as a record that can be \\reviewed, commented on, accepted or rejected — and that a pipeline can \\fire on. TARGET defaults to the repository's main branch. \\ \\A landing is not a new artifact kind. The record is an ordinary forum \\thread, every comment and state change is a reply in it, and the outcome \\is a raw tag on the check-in that landed. A stock fossil clone reads all \\of it; only the ceremony is zit's. \\ \\comment replies to the record's opener by default, so the thread reads \\flat. --reply-to POST aims the reply at an earlier post in the same \\thread, exactly as `forum reply` does; a POST outside this landing's \\thread is refused. \\ \\The record names a BRANCH, not a frozen range: commits added after the \\landing opens belong to it, which is what the merge-target-in-while-you- \\work habit needs. \\ \\open's MESSAGE is split the way a commit message is: the first line is \\the record's title, and anything after it is the prose beneath. A \\one-line message is a title with no prose. \\ \\ls shows open landings; --all includes accepted and rejected ones. ID is \\any unambiguous prefix of a landing id. \\ \\show reports mergeability: whether the branch would land cleanly, read \\from the store without writing anything, so a forge can flag a conflict \\before anyone accepts. It also lists the check-ins being proposed and \\the thread; both are bounded and say when they truncate. \\ \\STACKS. A landing may target another branch that has its own open landing; \\that pair is a stack, and the chain is read from those targets rather than \\recorded anywhere. Reviewing a stacked landing shows only its own \\check-ins, because the diff is computed against the branch below it. \\ \\Stacks land bottom-up. accept refuses a landing whose target has its own \\open landing, naming it; and accepting one retargets whatever was stacked \\on it onto the branch it just landed on, so the next segment is unblocked \\without touching a single check-in. Nothing is rebased -- a segment's \\branch already descends from the one below it, so after that lands the \\merge sees only the segment's own change. \\ \\accept also refuses a landing whose target was REJECTED, or that already \\landed: in both cases the merge would succeed and put the work on a branch \\trunk will never see. Retarget it instead; the refusal names the command. \\ \\Rejecting a landing rejects the PROPOSAL, not the code beneath whatever is \\stacked on it. A segment's branch descends from the one below it, so it \\contains that segment's check-ins -- land it on trunk after its base was \\rejected and the rejected change lands too. To be rid of that change, \\revert it; rejecting its landing does not. \\ \\retarget says a landing should land somewhere else. accept does it for the \\stack it unblocks; run it by hand when a stack is reordered, or when a \\segment's base was rejected and its work should go straight to trunk. \\ \\update brings the target INTO the proposed branch, on the store, as an \\ordinary merge check-in on that branch -- the refresh a reviewer with no \\clone needs, and what lets the rungs above a stacked landing see the \\combined state before anything lands. It always merges in a working \\area it creates and removes, 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 (exit 5) with its paths and nothing written -- \\resolve it from a checkout and push. The refresh is recorded as a reply \\in the record's thread. \\ \\accept works in a checkout, or against a bare store with -R -- there it \\merges in a working area it creates and removes, materializing only the \\files both sides changed. A landing that does not merge cleanly is \\refused (exit 5) with nothing written; resolve it from a checkout and \\push. Nothing here ever commits conflict markers. \\ \\options: \\ --branch NAME the branch being proposed (open) \\ --to TARGET the branch it should land on (open and retarget; \\ default on open: main) \\ -m, --message M the message (required on open and comment); on open \\ its first line is the title, the rest is the prose \\ --user NAME author to record (default: $USER); empty is refused \\ --mimetype T the body's mimetype (N card), e.g. text/x-markdown; \\ omitted, the reader applies its own default \\ --all ls: include settled landings \\ ++ repo_option_help ++ \\ -h, --help show this help \\ ++ exit_status_conventional_help; pub const tag_help = \\usage: zit tag list [--raw] [CHECKIN] [--no-cache] \\ zit tag add [--raw] [--propagate] NAME CHECKIN [VALUE] \\ zit tag cancel [--raw] NAME CHECKIN \\ \\List tags, name a check-in, or attach a property to one. Plain list prints \\active singleton symbolic release names across the repository or on \\CHECKIN, one per line, sorted. Branch names, raw properties, and \\propagating tags are excluded. List --raw prints every active tag with \\its stored prefix and value. \\ \\A tag is one of two things, and --raw is what picks between them. \\ \\Without --raw, NAME is a symbolic NAME for one check-in: it resolves to \\that check-in everywhere a check-in name is accepted, and it stays put — \\unlike a branch name, later check-ins do not inherit it. Use it 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. Use it for a fact many check-ins may carry — a CI \\verdict, a review marker — optionally with a VALUE. Because raw tags are \\not names, plain list does not show them; list --raw does. \\ \\cancel retracts a tag from a check-in. It takes the same --raw choice as \\the add that created it: cancelling 'v1' and cancelling '--raw v1' are \\different tags. \\ \\Names beginning wiki-, tkt-, or event- are refused: fossil crosslinks \\wiki pages, tickets, and technotes through them. \\ \\ ls alias for list \\ \\options: \\ --raw NAME is a raw tag name, not a symbolic check-in name; \\ on list, show every tag with its prefix intact \\ --propagate add only: descendants inherit the tag (mint '*', not \\ '+'). Hazardous without --raw: an inherited symbolic \\ name drifts to the newest descendant carrying it. \\ ++ no_cache_option_help ++ repo_option_help ++ \\ -h, --help show this help \\ ++ exit_status_conventional_help; pub const serve_help = \\usage: zit serve [--port N] [--fronted] [-h|--help] \\ \\Serve this repository over HTTP so a peer can sync against it. Answers a \\POST to /xfer carrying an application/x-fossil body per the sync protocol, \\one request per connection, single-threaded. Binds 127.0.0.1 on port N \\(default 8080). Stop it with Ctrl-C. \\ \\--fronted declares that a trusted proxy on this machine is the only \\client, and honours its per-request X-Zit-Protect header: a glob list of \\branches whose membership that push may not change. 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 -- the same error \\card a content-policy refusal uses, so a stock fossil client reads it. \\The server never learns who pushed; the front decides that and sends \\only the rule. Without --fronted the header is ignored, and nothing \\about this server changes. \\ \\options: \\ --port N listen on port N (default 8080) \\ --fronted honour a trusted front's X-Zit-Protect header \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; pub const clone_help = \\usage: zit clone URL [DIR] [--no-open] [--once] [--cone=PATH] \\ [--cone-not=PATH] [--cone-file=FILE] [-h|--help] \\ \\Copy a repository served at URL into a new directory DIR — by default \\the URL's last path segment ('.fossil' dropped), so 'zit clone \\https://host/proj' lands in ./proj. Creates DIR (and its `.zit`), opens \\an HTTP connection to URL, syncs the repository's artifacts down to \\convergence, stores them, and then opens a working tree in DIR (the \\same 'zit open' you would run next); --no-open leaves the bare store. \\ \\URL is the peer's sync endpoint — an http:// or https:// address, used \\verbatim (the path is not rewritten). The clone is anonymous: no login. \\ \\The URL is saved as DIR's default remote, so `zit pull`, `push`, and \\`sync` work there with no URL argument. The saved form never carries a \\password: a `user:secret@host` URL is stored as `user@host`. `zit remote` \\shows the saved value; `zit remote URL` overrides it. --once clones \\without saving. \\ \\options: \\ --no-open clone only; do not open a working tree in DIR \\ --once do not save URL as the default remote \\ --cone=PATH fetch only the artifacts PATH's subtree needs — a sparse \\ store; the rest stay elided phantoms (repeatable) \\ --cone-not=PATH exclude PATH's subtree from an enclosing --cone \\ member (repeatable); the most specific member decides \\ --cone-file=FILE read cone members from FILE, one per line \\ ('#' comments and blank lines are skipped; a '!' \\ prefix marks an exclusion) \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; pub const pull_help = \\usage: zit pull [URL] [-h|--help] \\ \\Fetch artifacts from the repository served at URL into the current repository. \\Opens an HTTP connection to URL, announces what this repository already holds, \\requests everything it lacks, and stores the newly received artifacts so this \\repository converges toward the peer's content. \\ \\URL is the peer's sync endpoint — an http:// or https:// address, used \\verbatim (the path is not rewritten). The pull is anonymous: no login. With \\URL omitted the saved remote is used ('zit remote URL' saves one); a repo \\with no saved remote and no URL is an error. \\ \\options: \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; pub const push_help = \\usage: zit push [URL] [--user U [--password P]] [-h|--help] \\ \\Send this repository's artifacts to the repository served at URL. Opens an \\HTTP connection to URL, announces what this repository holds, and uploads \\every artifact the peer lacks until both sides converge. The peer stores the \\uploaded artifacts; nothing on this side changes. \\ \\URL is the peer's sync endpoint — an http:// or https:// address, used \\verbatim (the path is not rewritten). With URL omitted the saved remote is \\used ('zit remote URL' saves one); a repo with neither is an error. \\ \\With --user the push authenticates: the shared secret is derived from the \\password and the login card is signed with it. The password comes from \\--password, else the ZIT_PASSWORD environment variable, else a no-echo \\prompt. After a successful authenticated push the derived secret (never the \\password) is saved in the repository at 0600, and from then on every push \\without --user -- including autosync -- authenticates as that login, the way \\fossil reuses a remembered one. With nothing saved a push without --user is \\anonymous (no login card); --user "" pushes anonymously regardless. The \\saved login is reused as it stands: after a password change on the peer, \\pass --user again to derive and save the new secret. \\ \\options: \\ --user U authenticate as login U (sync §6) \\ --password P U's password (prompted without echo if omitted) \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; pub const sync_help = \\usage: zit sync [URL] [--user U [--password P]] [-h|--help] \\ \\Reconcile this repository with the one served at URL in BOTH directions in a \\single exchange: this side uploads every artifact the peer lacks and receives \\every artifact it lacks, until both converge. This is one session, not a pull \\followed by a push — sync history transfers both ways and never discards \\artifacts (so there is no force-push). \\ \\URL is the peer's sync endpoint — an http:// or https:// address, used \\verbatim (the path is not rewritten). With URL omitted the saved remote is \\used ('zit remote URL' saves one); a repo with neither is an error. \\ \\With --user it authenticates for the upload exactly as 'zit push' does: the \\shared secret is derived from the password (from --password, else \\ZIT_PASSWORD, else a no-echo prompt) and, on success, saved in the repository \\at 0600. From then on a sync without --user authenticates as that saved \\login; with nothing saved it is anonymous, and --user "" forces anonymous. \\ \\options: \\ --user U authenticate as login U (sync §6) \\ --password P U's password (prompted without echo if omitted) \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; pub const settings_help = \\usage: zit settings [--global] [--changed] [--value] [-R <path>] [NAME [VALUE]] \\ \\Show or change settings. Bare, list every setting and the value in \\effect; with NAME, print that setting; with NAME VALUE, set it. \\ \\A value can live in four places. Highest wins: \\ \\ environment $ZIT_PGP_COMMAND, for the one setting that names one \\ versioned a TRACKED .fossil-settings/<name> or .zit-settings/<name> \\ file -- fossil's spelling wins when both exist, so a \\ repository mirrored from a Fossil server keeps its own \\ policy without renaming anything -- clones with the \\ repository, reviewable in a diff, and overrides both \\ stored tiers below \\ repository this repository (the default target of a write) \\ global this operator, shared by every repository (--global) \\ \\autosync takes no versioned file: when to go on the wire is this \\machine's business, not the project's. A tracked .fossil-settings or \\.zit-settings file for it is ignored, with a note saying so. \\ \\When the value in force does not come from this repository, a note on \\stderr names the tier it came from -- a shadowed value you cannot see is \\how a setting gets edited twice with no effect. stdout stays a clean \\"NAME VALUE" line for scripts. \\ \\The global file is $ZIT_CONFIG, else $XDG_CONFIG_HOME/zit/settings, else \\$HOME/.config/zit/settings: "name value" lines, '#' comments. \\ \\-R names a store with no working tree. Its repository and global tiers \\answer and take a write there; the versioned tier has no checkout to \\read, so a listing reports the two stored tiers only. \\ \\settings: \\ autosync on|off|pullonly \\ pull from the saved remote before commit, update, \\ and merge, and push after a successful commit \\ (default on; a failed autosync warns and the local \\ operation proceeds); pullonly pulls but never pushes \\ ignore-glob LIST globs of untracked files that extras, addremove, \\ clean, and add's \\ directory expansion leave alone ('*' matches '/', \\ so a directory is 'zig-out/*'). Commas AND \\ whitespace separate patterns; quote one that \\ needs either ('"a b.txt"'). An empty LIST \\ clears it. A root .zitignore (gitignore dialect) \\ also applies; a path either names is ignored. \\ Naming an ignored file explicitly still adds it. \\ keep-glob LIST globs that clean never deletes. Unlike ignore-glob \\ these files still appear in extras -- "keep telling \\ me it is here, never remove it", for local config \\ and scratch you want to notice but not lose. \\ manifest r,u,t write build-stamp files at open and update \\ clearsign on|off clearsign every commit, as though --sign had been \\ passed (default off) \\ pgp-command CMD the clearsign command, invoked as \\ 'CMD OUTFILE INFILE' (default 'gpg --clearsign -o ', \\ trailing space included; a command whose basename \\ starts with 'ssh' takes the detached form \\ 'CMD INFILE' and writes INFILE.sig; a false-ish \\ value -- off/no/0/false -- disables signing) \\ before-commit CMD a check run before each commit; a non-zero exit \\ aborts it. A versioned declaration is INERT until \\ trusted here with 'zit trust'. \\ binary-glob LIST the binaries this project expects. SET it and a \\ commit refuses any other file containing NUL bytes; \\ leave it empty and the class is not checked at all. \\ crlf-glob LIST the same, for CR/LF line endings. \\ crnl-glob LIST fossil's older spelling of crlf-glob, read only \\ when no tier supplies crlf-glob at all. \\ encoding-glob LIST \\ the same, for files whose encoding this project \\ expects (UTF-16, or bytes that are not UTF-8). \\ clean-glob LIST untracked files clean removes without asking. \\ empty-dirs LIST directory names open and update create even \\ though the check-in holds no file inside them. \\ dotfiles on|off list names beginning with '.' in extras, status, \\ clean, addremove and add's directory expansion \\ (default off; --dotfiles turns it on for one run). \\ Written as text and read by fossil's truth rule, \\ so on/yes/true and off/no/false all work. \\ mimetypes LIST extension-to-MIME rows a web UI serves files \\ with. Accepted so a repository mirrored from a \\ Fossil server is not an error; nothing reads it \\ until zit serves a UI. \\ editor CMD the program 'zit commit' opens for a comment when \\ -m is not given: after --editor, before $VISUAL \\ and $EDITOR. Executable tier: never versioned. \\ \\The content globs are opt-in: an empty glob is "no opinion", not "refuse \\everything", so a repository that never set one commits exactly as before. \\Setting 'binary-glob *.png' says PNGs are expected, so a stray .o is \\refused. \\--no-checks waives the check once. \\ \\None of them runs code, so unlike before-commit they need no consent: a \\tracked .zit-settings file applies the moment it is cloned. That is the \\division -- rules that only INSPECT content hold for everyone, rules that \\EXECUTE hold only where someone accepted them. \\ \\clearsign and pgp-command can never come from a versioned file, and \\never cross the wire: pgp-command is a command line this machine runs, \\so a value a peer could set would be remote code execution. That is why \\before-commit splits in two -- the project declares the check, this \\machine decides whether to run it. \\ \\options: \\ --global read or write the operator-scope tier \\ --changed list only settings whose value in effect is not the \\ default \\ --value print the bare value, without the name \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; pub const remote_help = \\usage: zit remote [URL | --unset] [-h|--help] \\ \\Show, set, or clear the default sync remote — the peer URL that 'zit sync', \\'zit pull', and 'zit push' use when you do not name one. With no argument, \\print the saved remote (or 'no remote set'). With URL, save it. With --unset, \\clear it. \\ \\There is a single default remote, not a named-remote registry. URL is stored \\verbatim and used anonymously — never put a credential in it; authenticate a \\push or sync with --user instead. \\ \\options: \\ --unset clear the saved remote \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; pub const precedent_help = withAliases(.precedent, \\usage: zit precedent [list | show ID | forget ID | forget --all] [-h|--help] \\ zit rerere ... (the same verb, for people arriving from git) \\ \\Reuse a conflict resolution you already authored. 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 are 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. \\ \\A merge run with `--no-precedent` declines replay for that invocation and \\behaves exactly as it did before this feature existed. Declining does not \\forget: use `forget` for that. \\ \\actions: \\ list one line per precedent: id, path, sizes (the default) \\ show ID the conflict a precedent fires on, and its resolution \\ forget ID drop one; the next such merge conflicts normally again \\ forget --all drop every one \\ \\ID is a prefix of the id `list` prints. An ambiguous prefix is refused. \\ \\options: \\ --all with `forget`, drop every precedent \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help); pub const stash_help = \\usage: zit stash [save [-m MSG] | snapshot [-m MSG] | list | show [ID] | \\ diff [ID] | apply [--merge] [ID] | pop [--merge] [ID] | \\ goto [ID] | drop [ID]] [-h|--help] \\ \\Set the working tree's uncommitted changes aside and restore them later. \\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. \\ \\actions: \\ save capture every added/modified/deleted file versus the current \\ check-in into a new entry, then restore the check-in's state \\ snapshot capture the same entry and leave the working tree as it is \\ list one line per entry: id, check-in, file count, message \\ show an entry's files as A/M/D lines (the newest without ID) \\ diff an entry's changes as unified hunks against its check-in \\ apply restore an entry's changes into the working tree. 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. \\ --merge relaxes the second rule for modified files: the \\ stashed change is three-way merged into the dirty file, \\ conflicts get markers and are listed, and the exit is 5 \\ pop apply, then drop \\ goto update to the entry's check-in, then apply it \\ drop delete an entry (ids are never reused) \\ \\options: \\ -m, --message MSG label the saved entry \\ --merge apply/pop: merge into modified files instead of \\ refusing (zit#164) \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; pub const config_help = \\usage: zit config pull shun|user [URL] [--overwrite] [-R <path>] \\ [--user=NAME] [--password=SECRET] [-h|--help] \\ zit config import FILE \\ zit config merge FILE \\ \\Fetch a configuration area from a peer and install it -- fossil's \\'fossil configuration pull AREA'. \\ \\Configuration otherwise travels at CLONE time only. 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. \\ \\areas: \\ shun the peer's shun table (its takedown records). Rows are added, \\ never removed: pulling cannot un-shun anything here. \\ user the peer's user table -- logins, capabilities, and password \\ hashes. Fossil serves this area to an ADMINISTRATOR only, so \\ the pull is credentialed: pass --user=NAME with either \\ --password=SECRET or $ZIT_PASSWORD. Without credentials the \\ peer answers with no rows and this verb says so rather than \\ reporting an empty success. Read the result with `zit users`. \\ \\A pull MERGES by default -- a row wins only if its mtime is newer than \\the one already held -- which is fossil's own default. --overwrite \\replaces the local table instead, for a mirror that tracks an upstream \\authoritatively. A local-only login survives a merge and does not \\survive an --overwrite; that is the whole difference. \\ \\import and merge apply a file written by `fossil configuration export \\AREA FILE`, whose cards are byte-identical to what the wire carries. \\import replaces, merge folds -- the same two modes, named as fossil \\names them, so no flag is needed (or accepted) to pick between them. \\ \\URL defaults to the saved remote. Only configuration moves; no artifacts \\are transferred. \\ \\-R names a store with no working tree. The user and shun areas are store \\tables, so pull, import, and merge all reach them without a checkout. \\ \\options: \\ --overwrite replace the local table instead of merging \\ --user=NAME log in as NAME (the user area needs admin) \\ --password=SECRET NAME's password; $ZIT_PASSWORD otherwise \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; pub const trust_help = \\usage: zit trust [--yes] [--revoke] [-h|--help] \\ \\Show, grant, or withdraw this checkout's consent to run the project's \\'before-commit' check. \\ \\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, this 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 ('zit settings before-commit ...') \\needs no trust record -- writing the command is the consent. \\ \\options: \\ --yes accept the declaration as shown; commits then run it \\ --revoke withdraw consent; commits are refused until re-accepted \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; pub const worktrees_help = \\usage: zit worktrees [-h|--help] \\ \\List every working tree registered against this repository's store: the \\tree the command runs in, and every detached tree ('zit open --store') \\or co-located checkout that has opened or updated against the store. \\Each line is the tree's absolute path, its checked-out branch, and its \\check-in prefix; the invoking tree is marked with a leading '*'. \\ \\The registry is advisory and self-healing: trees stamp themselves at \\open and update, and this listing lazily prunes entries whose path is \\gone or whose checkout no longer points at this store (a tree deleted \\with plain rm, or re-pointed elsewhere). A tree opened by an older zit \\appears after its next open or update. \\ \\With -R the store is named directly and no working tree is required -- \\"which trees does this store serve?" from anywhere. \\ ++ repo_option_help ++ \\ -h, --help show this help and exit ++ exit_status_conventional_help; pub const cone_help = \\usage: zit cone [add PATH... | remove PATH... | \\ test [--store] [--quiet] PATH...] [-h|--help] \\ \\Show, edit, or query this checkout's sparse cone (sparse-cone RFC \\section 3). With no arguments, list the cone's members ("dense" when \\there is none). \\ \\actions: \\ add union PATHs into the cone and rematerialize at the current \\ check-in, fetching newly covered content through the saved \\ remote when the store lacks it \\ remove narrow the cone; the files leave the working tree, the store \\ keeps their bytes until 'zit pack --drop-elided' reclaims them \\ test classify PATHs against the stored cone and name the member \\ that decided each, without materializing anything \\ \\'add' and 'remove' both drive the same machinery as 'zit update --cone'. \\ \\'test' answers "would this cone keep this path, and why". Coverage is \\decided by the LONGEST member covering the path, across both polarities, \\so the deciding member is rarely the first one listed and ordering is \\never what decided it. Each line is the verdict, the path, and that \\member -- printed bare, so it can be handed straight to 'zit cone \\remove'. A named member under 'elided' is necessarily an exclusion. \\Give '-' to read paths from stdin, one per line. \\ \\options: \\ --store test against the store cone instead of the worktree cone; \\ the two need not agree, and a path present in the checkout \\ but absent from the store is a distinct condition \\ --quiet suppress the per-path lines and leave only the exit status \\ -h, --help show this help and exit \\ \\'test' exits 0 when every path given is covered and 1 when any is elided, \\which makes it usable as a predicate. \\ ++ exit_status_conventional_help; pub const tarball_help = \\usage: zit tarball VERSION [OUTPUTFILE] [--name DIR] [--include GLOBS] \\ [--exclude GLOBS] [--list] [--no-cache] [-h|--help] \\ \\Write VERSION's files as a gzip-compressed tar archive. VERSION resolves \\like 'zit info' (id, prefix, branch, tag, timestamp). No working tree is \\involved and none is created: the check-in's file set is read straight \\from the repository, so '-R' works and a bare mirror can cut a release. \\ \\Every member sits under one top-level directory. --name sets it; without \\one 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. \\ \\Control files are never archived. An F card's permission travels with the \\file: an executable stays executable and a symbolic link stays a link, \\which is what a checkout-then-tar workaround routinely loses. \\ \\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 the \\same VERSION on different machines produce identical files, which is what \\makes the output usable in a release pipeline. \\ \\An OUTPUTFILE of '/dev/null' or the empty string writes nothing; combined \\with --list that is how you preview an archive's contents. A file the \\store does not hold (a sparse store elides out-of-cone content) is \\refused by name -- never omitted, which would look like success. \\ \\options: \\ --name DIR top-level directory inside the archive \\ --include GLOBS archive only files matching this glob list \\ --exclude GLOBS archive no file matching this glob list; a file \\ matching BOTH lists is excluded \\ --list name the members instead of writing an archive \\ --no-cache bypass the derived-state cache and re-derive \\ -h, --help show this help and exit \\ \\GLOBS is a comma- or newline-separated list in the same dialect as the \\'ignore-glob' setting. Unlike fossil, a pattern may not be quoted, so a \\glob cannot contain a comma. \\ ++ exit_status_conventional_help; pub const zip_help = \\usage: zit zip VERSION [OUTPUTFILE] [--name DIR] [--include GLOBS] \\ [--exclude GLOBS] [--list] [--no-cache] [-h|--help] \\ \\Write VERSION's files as a zip archive. Identical to 'zit 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 when the store does not hold every file. \\ \\Modes travel in the central directory's external attributes, so an \\executable unpacks executable and a symbolic link unpacks as a link. \\A member is deflated only when that makes it smaller, so an \\already-compressed file is stored rather than grown. \\ \\Sizes are limited to 4 GiB per member: zip64 is not emitted, and a file \\that would need it is refused by name rather than written with a \\truncated size field. Use 'zit tarball' for such a check-in. \\ \\options: \\ --name DIR top-level directory inside the archive \\ --include GLOBS archive only files matching this glob list \\ --exclude GLOBS archive no file matching this glob list; a file \\ matching BOTH lists is excluded \\ --list name the members instead of writing an archive \\ --no-cache bypass the derived-state cache and re-derive \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; pub const checkpoint_help = \\usage: zit checkpoint [show | build [CHECKIN] | verify | drop] [-h|--help] \\ \\Manage this checkout's rebuild checkpoint: a LOCAL, disposable accelerator \\that lets a from-scratch derivation skip re-parsing the log's permanently \\flat prefix. It is a sidecar file only — building one never writes the \\repository, it is never synced and never history; deleting it changes no \\answer, and --no-cache bypasses it entirely. The first full derivation \\plants one automatically at no extra cost, and the ordinary open path \\keeps it advanced as history grows; an explicit build only re-anchors it. \\ \\actions: \\ show print the stored claim and whether it still validates (default) \\ build build or rebuild the checkpoint, anchored at CHECKIN (a full \\ artifact ID) or at the newest check-in in the repository \\ verify re-derive everything the checkpoint claims from real history \\ and compare, byte for byte; nothing is trusted, nothing changes \\ drop delete the checkpoint (a missing one is success) \\ \\options: \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; pub const submanifest_help = \\usage: zit submanifest [show | enable [--k N] | disable | retune --k N | \\ analyze [--names PATH] [--replay-all] | \\ convert SRC DEST [--k N]] [-h|--help] \\ \\Show and change this repository's sub-manifest settings, or measure the \\partition parameter. 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 and turning it on is a deliberate act with a \\consequence: from the first tree this repository writes, peers below Fossil \\2.29 are refused every S-bearing artifact rather than handed one they cannot \\read, and turning the setting back off cannot retire artifacts already \\written. The specification is provisional; a repository containing one must \\be treated as a disposable test fixture. \\ \\actions: \\ show print the settings and what commits currently emit (default) \\ enable turn emission on for NEW check-ins, pinning the partition \\ parameter the first time — k chosen by replaying recent \\ history (the snapshot objective when there is none); --k N \\ overrides. Existing check-ins stay flat, and from the next \\ commit peers below Fossil 2.29 — every released fossil — \\ receive nothing new from this repository \\ disable stop emitting new tree manifests (existing ones are permanent) \\ retune re-pin the partition parameter — always manual, never inferred. \\ The next commit rewrites every leaf, once. No history changes. \\ analyze measure the objective for k = 1..12 over a name set, report \\ the optimum, and replay recent history through the real tree \\ builder and delta encoder (both cost axes, bootstrap split \\ out). Changes nothing. With -R it measures a bare store (no \\ checkout needed; the tip check-in's file set is analyzed) — \\ the evidence a conversion decision wants, in advance. \\ convert 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 — 'zit import-fossil SRC DEST --submanifest' \\ by its proper name. SRC is never modified; DEST cannot sync \\ with SRC's peers. k from --k, else the sweep over SRC's tip. \\ Every pair is proven file-set-equivalent before DEST is \\ published; DEST is delta-encoded as it is written. \\ \\options: \\ --k N the partition parameter, 1..12 \\ --names PATH analyze a newline-separated, sorted name list from PATH \\ instead of the current check-in's file set \\ --replay-all analyze only: replay the WHOLE history instead of the \\ recent window — the full-history evidence a \\ conversion decision wants; cost scales with history \\ -h, --help show this help and exit \\ \\These settings are LOCAL to this repository and do not propagate to clones. \\ ++ exit_status_conventional_help; pub const info_help = \\usage: zit info [NAME...] [-F text|json] [--no-cache] \\ \\With no NAME, describe the check-in the working tree is on (Fossil's \\bare 'info'); NAME is any check-in name. \\ \\Resolve each NAME to a single check-in and describe it. NAME may be a \\full 40- or 64-character artifact ID, a unique ID prefix, a branch or \\tag name, a reserved form, or a timestamp — 'zit help names' states the \\full procedure. Resolution follows recency, not open-leaf status: a \\name can resolve to a closed or non-leaf check-in that 'zit leaves' \\would not list. \\ \\Several NAMEs share one repository open; their 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 and the rest are still \\described; the exit status is then 1. \\ \\options: \\ -F, --format <f> \\ text (default): the block above per check-in. \\ json: one object per NAME, in the order given, with the \\ keys name, hash, branch, leaf, parent (null for a root), \\ merge, cherrypick, backout (arrays), and comment, user, \\ date when an amend override is active; a name that does \\ not resolve is {"name":..., "error":...}. ++ "\n" ++ no_cache_option_help ++ repo_option_help ++ \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; pub const amend_help = \\zit amend NAME [--comment TEXT] [--user NAME] [--date ISO] [--branch NAME] \\ \\Attach display overrides to a check-in by minting one control artifact. \\History is never rewritten; the original check-in is untouched. \\ \\ --comment TEXT override the displayed comment \\ --user NAME override the displayed author \\ --date ISO override the displayed date (YYYY-MM-DDTHH:MM:SS) \\ --branch NAME move the check-in (and its descendants) to branch NAME \\ -h, --help show this help \\ \\At least one override flag is required. \\ ++ exit_status_conventional_help; pub const grep_help = \\usage: zit grep [-n N] [-i] [-c|-l] [-v] [--once] [--no-cache] PATTERN PATH... \\ zit grep -S|-G [-n N] [--no-cache] PATTERN [PATH...] \\ \\Search a file's HISTORY for a literal string. 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. \\ \\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. \\ \\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. \\ \\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 PICKAXE (-S) asks a different question: not "which versions contain \\this" but "when did it appear, and when did it go". It reports the \\check-ins where the NUMBER OF OCCURRENCES of PATTERN changed, one line \\each, newest first: \\ \\ <checkin12> <+N|-N> <path> \\ \\A string added in one check-in and deleted in another is exactly two \\lines, where plain grep reports every version that held it in between. \\This is 'git log -S'. zit takes the string as the ordinary PATTERN \\operand rather than git's attached -S<string>, so there is one way to \\say it; PATH is then optional and narrows the walk instead of driving it. \\ \\-G asks the same walk a different question: not "did the count move" but \\"did a changed line mention this". 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. -G lines \\are marked '~N', where N counts the changed lines that mention PATTERN. \\-G runs the diff engine per changed path, so it costs more than -S. \\Give one or the other, never both. \\ \\Both read only the blobs that actually changed: a path whose content hash \\is the same in a check-in and its parent cannot have changed the count. \\Merges are compared against the primary parent only, so content arriving \\through a merge is attributed to the merge check-in -- 'git log -S' does \\the same, and counting a check-in against every parent would report one \\appearance twice. \\ \\options: \\ -n, --limit N stop after N matching versions; 0 means all \\ -S, --pickaxe report check-ins where the occurrence COUNT changed \\ -G, --diff-match \\ report check-ins where a CHANGED LINE mentions PATTERN \\ ++ no_cache_option_help ++ repo_option_help ++ \\ -i, --ignore-case \\ match letters regardless of case \\ -c, --count print CHECKIN PATH:COUNT per file instead of the lines \\ -l, --files-with-matches \\ print CHECKIN PATH per file with a match, no lines \\ -v, --invert-match \\ select the lines that do NOT match \\ --once stop at the first matching line of each file \\ -h, --help show this help and exit \\ \\exit status: \\ 0 at least one version matched \\ 1 nothing matched, or the repository could not be read \\ 2 usage error (an unknown option or a missing operand) \\ ; pub const ls_help = \\usage: zit ls [VERSION] [--long|--size] [--under=PATH] [--no-cache] \\ \\List the files of a check-in, one name per line as Fossil does, in \\canonical filename order; --long adds the blob-id and more. VERSION selects the check-in the same way 'zit \\info' does; 'zit help names' lists every accepted form, timestamps \\included. With VERSION omitted, the current checkout is listed; an \\unborn checkout has nothing to list and is an error. \\ \\The listing is the effective file set: a delta check-in is merged with its \\baseline and deleted files are omitted. \\ \\--long adds three columns before the filename: \\ \\ <blob-id> <size> <perm> <class> <filename> \\ \\perm is the F card's 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. 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 -- in which case size reads 0 because \\nothing was measured, not because the file is empty. \\ \\--size is the CHEAP half of --long, and the split is where the cost is: \\ \\ <blob-id> <size> <perm> <filename> \\ \\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: it is 'a NUL \\anywhere in the content', the NUL may be the last byte, so class needs \\the whole blob and therefore the whole chain. That is why --long reads \\every listed blob and --size does not, and why a listing that only needs \\names and sizes should ask for --size. \\ \\Under --size an unavailable blob prints 'absent' in the size column \\rather than a number, since there is no size to report. \\ \\--long reads every listed blob, so pair it with --under to pay only for \\what is being displayed. The default two-field output reads no content \\and is unchanged. \\ \\options: \\ --long add size, permission and text/binary columns \\ --size add size and permission only; no content is read \\ --under=PATH list only entries at or beneath PATH (matched on \\ segment boundaries, so 'src' never matches \\ 'src-other'); PATH may name a single file ++ "\n" ++ no_cache_option_help ++ repo_option_help ++ \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; pub const cat_help = \\usage: zit cat FILE [VERSION] [--no-cache] \\ \\Write the contents of FILE at a check-in to standard output, verbatim — no \\trailing newline is added and binary content passes through unchanged. FILE \\is a pathname within the check-in; VERSION selects the check-in the same way \\'zit info' does; 'zit help names' lists every accepted form, timestamps \\included. With VERSION omitted, the current checkout is used; an unborn \\checkout has nothing to read and is an error. \\ \\FILE is resolved against the effective file set: in a delta check-in the \\file's own entry wins over the baseline, and a file deleted at this check-in \\is reported as not found. The emitted bytes are verified to match the file's \\recorded hash. \\ \\options: ++ "\n" ++ no_cache_option_help ++ repo_option_help ++ \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; pub const artifact_help = \\usage: zit artifact HASH [--raw] \\ \\Describe or dump a single stored artifact addressed by its hash. HASH is a full \\or abbreviated (>=4 character) lowercase hexadecimal artifact ID, resolved \\against every stored artifact — file blobs included. Branch and tag names are \\not accepted; use 'zit info' for those. \\ \\Without --raw, print a summary: the full hash, the artifact's size, its type, \\and — for a structural artifact — one line per card and whether its Z \\self-checksum verifies. A blob reports its size and whether it is binary. \\ \\With --raw, write the artifact's exact stored bytes to standard output, \\verbatim — no trailing newline is added and binary content passes through \\unchanged. Hashing those bytes reproduces HASH. \\ \\options: \\ --raw write the exact stored bytes instead of a summary ++ "\n" ++ repo_option_help ++ \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; pub const diff_help = \\usage: zit diff [--brief|--numstat|--by] [-w|-Z] [-c N] [--invert] [-N] \\ [--from CHECKIN --to CHECKIN | --checkin V | --branch B] \\ [-R <path>] [-h|--help] [PATH...] \\ \\Compare the managed working-tree files with the current check-in, or — \\with --from and --to — compare two check-ins directly. --checkin V \\compares V with its parent; --branch B compares B's tip with the \\check-in it forked from. PATH operands (files or directories) narrow \\the report. The default output is unified hunks (three context lines, \\binary files noted); --brief prints name-status only: A for additions, \\M for content or executable-mode changes, and D for removals, sorted by \\pathname; --numstat prints ADDED, REMOVED and PATH tab-separated. \\ \\options: \\ --brief name-status output only, no hunks \\ --numstat per-file added and removed line counts (- for binary) \\ --by render the diff side by side as an HTML page under \\ $TMPDIR (else /tmp), print its path, and open it in \\ the browser when stdout is a terminal \\ -w, --ignore-whitespace \\ treat lines differing only in whitespace as unchanged; \\ a file whose every difference is whitespace-only drops \\ out of the output entirely, --brief included \\ -Z, --ignore-all-space \\ the same as -w (zit already ignores every whitespace) \\ -c, --context N hunk context lines (default 3) \\ --invert swap the two sides \\ -N, --new-file accepted for Fossil parity; an added file's hunks \\ already carry its whole content \\ --from CHECKIN the older operand of a check-in comparison \\ --to CHECKIN the newer operand; required with --from \\ --checkin V compare check-in V with its primary parent \\ --branch B compare branch B's tip with the check-in it forked from \\ -R, --repo <path> \\ with a check-in comparison only: read this repository \\ instead of the one found from the current directory; \\ no working tree is required. The worktree comparison \\ refuses it. \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; /// Verbatim B-init-11 help text. Model-honest wording ("check-in", "trunk", /// "unborn", "root check-in", "SHA3-256 by default"). Ends in exactly one /// newline. The e2e suite verifies stdout equals an independent transcription /// of the contract, so drift here is caught. const init_help = withAliases(.init, \\usage: zit init [-q|--quiet] [--] [directory] \\ \\Create an empty zit repository in <directory> (default: the current \\directory). Writes two files there: \\ \\ .zit the repository — a single, self-contained content log \\ .zit-checkout the working-state record (branch trunk, no check-in yet) \\ \\The new repository has no check-ins: the default branch 'trunk' is unborn \\until your first `zit commit`, which records the root check-in. Artifacts \\are named with SHA3-256 by default. \\ \\If <directory> does not exist it is created. \\ \\options: \\ -q, --quiet do not print the confirmation line \\ -h, --help show this help and exit \\ \\exit status: \\ 0 repository created \\ 1 a repository already exists, or the path could not be initialized \\ 2 usage error (unknown option or extra argument) \\ ); /// Verbatim B-add-13 help text (contracts/add.md §Help). Model-honest wording /// ("managed", "no staging area", "check-in", "trunk", "unborn", "zit rm"). /// Ends in exactly one newline. The e2e suite compares stdout against an /// independent transcription of this block, so drift is caught. pub const add_help = \\usage: zit add [-n|--dry-run] [-q|--quiet] [-f|--force] [--dotfiles] \\ [--] <path>... \\ \\Mark one or more working-tree files as managed, so they are included in \\your next check-in. zit has no staging area: `add` records only *which* \\files to track — their contents are read from the working tree when you \\run `zit commit`, not now. Re-adding an already-tracked file does nothing. \\ \\Files stay tracked until `zit rm` forgets them. You can add files before \\the first check-in (the default branch 'trunk' is unborn until then). \\ \\Naming a directory adds every untracked file beneath it, recursively; \\`zit add .` adds the whole tree. zit's control files are never added, and \\names beginning with a dot are skipped unless --dotfiles is given (naming \\one directly always adds it). \\ \\options: \\ -n, --dry-run show what would be added; change nothing \\ -q, --quiet do not print the added files \\ -f, --force add a file named directly even when ignore-glob \\ matches it (a directory expansion always skips those) \\ --dotfiles a directory expansion also takes names beginning with a \\ dot (also settable with `zit settings dotfiles on`) \\ -h, --help show this help and exit \\ \\exit status: \\ 0 all named files are now tracked (or already were) \\ 1 a file could not be added (missing, not a regular file, outside the \\ repo), or this is not a zit repository \\ 2 usage error (no files given, or an unknown option) \\ ; /// Verbatim B-rm-13 help text (contracts/rm.md §Help). Model-honest wording /// ("tracking", "managed set", "no staging area", "check-in", and the explicit /// contrast with git's delete-by-default). Ends in exactly one newline. The e2e /// suite compares stdout against an independent transcription of this block. pub const rm_help = withAliases(.rm, \\usage: zit rm [-n|--dry-run] [--hard] [-q|--quiet] [--] <path>... \\ \\Stop tracking one or more files: remove their paths from the managed set, \\so your next check-in records their deletion. zit has no staging area, so \\this takes effect when you run `zit commit`, not now. \\ \\By default your working copy is left on disk untouched -- `rm` only forgets \\the file. Pass --hard to also delete the file from disk. (git deletes by \\default; zit keeps your file unless you ask for --hard.) \\ \\Removing a path that is not tracked is an error. Naming a directory forgets \\every tracked file beneath it, whether or not the directory still exists. \\ \\options: \\ -n, --dry-run show what would be removed; change nothing \\ --hard also delete the working-tree file from disk \\ -q, --quiet do not print the removed files \\ -h, --help show this help and exit \\ \\exit status: \\ 0 all named files are no longer tracked (and, with --hard, deleted) \\ 1 a file could not be removed (not tracked, outside the repo, or --hard \\ could not delete it), or this is not a zit repository \\ 2 usage error (no files given, or an unknown option) \\ ); /// Verbatim help text (contracts/mv.md §Help). Model-honest wording ("tracked", /// "check-in", "recorded", the explicit no-clobber / no-force / no-undo posture). /// Ends in exactly one newline. The e2e suite compares stdout against an /// independent transcription of this block, so drift is caught. pub const mv_help = withAliases(.mv, \\usage: zit mv [-n|--dry-run] [--soft|--hard] OLD... NEW|DIR \\ \\Rename tracked files: move each working file on disk, swap the \\tracked-name entries, and record every rename so your next `zit commit` \\records the new name with the old one as its former name. \\ \\With two operands and NEW not a directory, OLD becomes NEW. When the last \\operand is a directory (on disk, or a tracked prefix, or '.'), or there are \\more than two operands, each OLD moves into it keeping its base name. An \\OLD that is a tracked directory moves as a unit, every file beneath it \\recorded as its own rename. \\ \\Every OLD must be tracked and present; no NEW may already be tracked or \\exist on disk. The whole request is checked before anything moves, and \\nothing overwrites. mv is not undone by `zit undo`; to reverse a rename, \\run `zit mv NEW OLD`. \\ \\options: \\ -n, --dry-run print 'would rename OLD -> NEW' per file; change nothing \\ --soft record the renames without touching the working tree (the \\ files stay at OLD; status then shows NEW missing until \\ you move them) \\ --hard move the files too (the default) \\ -h, --help show this help and exit \\ \\exit status: \\ 0 the files were renamed \\ 1 an OLD is not tracked or missing, a NEW is already tracked or on \\ disk, or this is not a zit repository \\ 2 usage error (fewer than two operands, or an unknown option) \\ ); /// Verbatim `zit stat` help text. It states the one thing a reader of these /// numbers has to know — that `content-bytes` and `store-bytes` are different /// measurements and neither substitutes for the other — because the field list /// alone would let someone read the smaller number as "the file, minus some /// slack". Ends in exactly one newline. The e2e suite compares stdout against /// an independent transcription of this block, so drift is caught. pub const stat_help = \\usage: zit stat [--artifacts] [--type=full|delta] [-R <path>] [-h|--help] \\ \\Report what the repository store actually holds. Every number is read \\from the frames in `.zit` — nothing is estimated, and nothing is derived \\from the file's size. \\ \\Output is one 'field value' line per fact, so it reads by machine: \\ \\ project-code this repository's shared sync identity, the \\ value a peer matches on and the first input to \\ an authenticated push's derived secret. Empty on \\ a repository that predates project codes. \\ hash-policy which names this repository mints \\ store-bytes the `.zit` file itself, in bytes \\ header-bytes the store header \\ content-bytes artifact payload bytes, summed frame by frame \\ frame-overhead-bytes frame headers and 8-byte padding for those frames \\ config-bytes the repository's config record \\ shun-bytes the repository's shun table \\ shun-rows rows in that table \\ user-bytes the repository's user table (#124) \\ user-frames frames in that table -- rows plus any \\ supersede markers a pack has not yet reclaimed, \\ so this can exceed what `zit users` prints \\ artifacts stored artifacts (the config record is not one) \\ full-form artifacts stored whole \\ full-form-bytes content-bytes held by those artifacts \\ deltas artifacts stored against a base \\ delta-bytes content-bytes held by those artifacts \\ \\-R names a repository with no working tree, so a bare store answers all \\of the above without a checkout, a port, or a child process. \\ \\content-bytes and store-bytes are different measurements and must not be \\substituted for one another: the store also carries the header, a header \\per frame, and padding, so it is always the larger number. The five byte \\fields plus header-bytes account for the file exactly. \\ \\full-form-bytes and delta-bytes PARTITION content-bytes; they are not \\further components of the file and must not be added to the accounting \\above. Read them against full-form/deltas rather than instead of them: a \\store is routinely a minority full-form by artifact and a large majority \\full-form by byte, because an artifact is stored whole exactly when \\delta did not pay -- the large, poorly-compressible ones. \\ \\options: \\ -h, --help show this help and exit \\ --artifacts list the stored artifact hashes instead, one per line, \\ in ascending hash order \\ --type=KIND with --artifacts, list only 'full' or 'delta' artifacts \\ \\exit status: \\ 0 the report was written \\ 1 this is not a zit repository, or `.zit` could not be read \\ 2 usage error (an unknown option, a repeated --type, a --type without \\ --artifacts, or an unexpected argument) \\ ; /// Verbatim B-status-16 help text (contracts/status.md §Help). Model-honest /// wording ("check-in", "trunk", "unborn", "tracked", "artifact hash" — never /// git's "index"/"staged"/"HEAD"), and it teaches the slice-1 scope boundary /// (DX D2) rather than hiding it. Ends in exactly one newline. The e2e suite /// compares stdout against an independent transcription of this block, so drift /// is caught. pub const status_help = withAliases(.status, \\usage: zit status [-v] [--classify] [--added] [--edited] [--missing] \\ [--conflict] [--extra] [--differ] [--dotfiles] \\ [-h|--help] \\ \\Show the state of the working copy: the current branch, the current \\check-in (if any), and the state of your tracked files. \\ \\Under a pending merge, an edited file that still carries conflict \\markers is reported as CONFLICT. Rows use Fossil's words (ADDED, \\EDITED, MISSING, CONFLICT, EXTRA, MERGED_WITH) in an aligned column; \\--classify is accepted and changes nothing. The kind flags narrow the \\rows to those kinds; --differ is \\--edited plus --extra, and --extra lists the untracked files 'zit \\extras' would (names beginning with a dot only under --dotfiles). \\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 the block, as in Fossil. \\ \\zit has no staging area. `status` reports the files you marked with \\`zit add` and how each one stands in the working tree: \\ \\ ADDED <path> tracked and new — it will enter your next check-in \\ EDITED <path> tracked, and its bytes or mode differ from the check-in \\ MISSING <path> tracked, but not present in the working tree \\ \\A tracked file whose contents match the current check-in is not listed. \\ \\Until your first `zit commit` the branch 'trunk' is unborn: there is no \\check-in yet, so every tracked file is reported as ADDED (it will form \\the root check-in). The current check-in, once one exists, is shown as its \\full artifact hash. \\ \\Two things `status` does not report, because it walks only your tracked \\files: a file deleted from the check-in but no longer tracked (`zit diff` \\lists it) and untracked "extra" files (`zit extras` lists them). \\ \\options: \\ -v, --verbose accepted; the block is printed either way \\ --classify accepted; Fossil's words are the default \\ --added, --edited, --missing, --conflict, --extra \\ only rows of those kinds \\ --differ --edited plus --extra \\ -h, --help show this help and exit \\ \\exit status: \\ 0 the working-copy state was reported (with or without changes) \\ 1 this is not a zit repository, or its state could not be read \\ 2 usage error (an unknown option or an unexpected argument) \\ ); /// Verbatim `zit extras` help (contracts/extras.md §Help). The house /// `parseNoPositionalArgs`-verb help shape (status/diff/timeline). Ends in /// exactly one newline; the e2e suite compares stdout against an independent /// transcription, so any drift is caught. pub const extras_help = \\usage: zit extras [--dotfiles] [-h|--help] \\ \\List every file in the checkout that zit is not tracking — each on-disk file \\under the checkout root that is neither a tracked file nor a zit control file. \\Paths are shown relative to the checkout root, one per line, sorted. Files in \\subdirectories are included; directories themselves are not. Symlinks are \\listed but never followed. With nothing untracked, the output is empty. \\ \\Names beginning with a dot are not listed unless --dotfiles is given. \\ \\options: \\ --dotfiles \\ also list names beginning with a dot (also settable with \\ `zit settings dotfiles on`) \\ -h, --help show this help and exit \\ ++ exit_status_conventional_help; /// Verbatim `zit clean` help (contracts/clean.md §Help). Names the destructive, /// non-undoable, non-interactive posture up front and the `--dry-run`-wins /// precedence. Ends in exactly one newline; the e2e suite compares stdout /// against an independent transcription, so any drift is caught. pub const clean_help = \\usage: zit clean [-n|--dry-run] [-f|--force] [--emptydirs] [--dotfiles] \\ \\Delete every file in the checkout that zit is not tracking — the same set \\`zit extras` lists. This is destructive and cannot be undone: `zit undo` does \\not recover a clean. \\ \\By default clean refuses and deletes nothing: it reports how many untracked \\files would be deleted and stops. Pass --force to delete them, or --dry-run to \\preview the exact list first. clean never prompts. \\ \\Files matching the `clean-glob` setting are the exception: the setting has \\already declared them disposable, so they are deleted without --force and the \\run says how many went. `keep-glob` wins over `clean-glob`, and --dry-run \\still deletes nothing. \\ \\Tracked files and zit's control files are never touched, and names beginning \\with a dot are left alone unless --dotfiles is given. Only files are deleted; \\directories left empty stay unless --emptydirs is given, and a directory the \\`empty-dirs` setting names is never removed. \\ \\options: \\ -n, --dry-run list what would be deleted, one path per line; delete nothing \\ -f, --force delete the untracked files \\ --emptydirs \\ also remove the directories the deletions leave empty \\ --dotfiles \\ also consider names beginning with a dot (also settable \\ with `zit settings dotfiles on`) \\ -h, --help show this help and exit \\ \\If both --dry-run and --force are given, --dry-run wins and nothing is deleted. \\ \\exit status: \\ 0 nothing to delete, a --dry-run preview, or every file deleted \\ 1 refused (no --force), a file could not be deleted, or not a zit repository \\ 2 usage error (an unknown option or an unexpected argument) \\ ; /// Verbatim `zit addremove` help (contracts/addremove.md §Help). Names the /// track-new + forget-deleted model, the tracked-only (never file contents) /// posture, and the ADDED/REMOVED report shape. Ends in exactly one newline; /// the e2e suite compares stdout against an independent transcription, so any /// drift is caught. pub const addremove_help = \\usage: zit addremove [--dry-run] [--dotfiles] \\ \\Reconcile the tracked set with the working tree in one pass: mark every \\untracked file to include in your next check-in, and stop tracking every \\file you deleted from disk. This is the set `zit extras` lists, added, plus \\the tracked files now missing, removed — fossil and hg spell it `addremove`; \\git spells it `add -A`. \\ \\addremove changes only what zit tracks; it never edits, creates, or deletes \\a working file, and the next `zit commit` records the result. Each action is \\reported, sorted and relative to the checkout root: \\ \\ ADDED <path> a newly tracked file \\ REMOVED <path> a tracked file no longer on disk \\ \\A tracked file that is merely modified is left alone (content does not affect \\membership), and zit's control files are never tracked. Names beginning with \\a dot are skipped unless --dotfiles is given. With nothing to do, the output \\is empty. \\ \\options: \\ --dry-run report what would change, one action per line; change nothing \\ --dotfiles also track names beginning with a dot (also settable with \\ `zit settings dotfiles on`) \\ -h, --help show this help and exit \\ \\exit status: \\ 0 the tracked set was reconciled (with or without changes), or a --dry-run preview \\ 1 a path could not be tracked, or this is not a zit repository \\ 2 usage error (an unknown option or an unexpected argument) \\ ; /// Verbatim B-commit-20 help text (contracts/commit.md §Help). Model-honest /// wording ("check-in", "root check-in", "trunk", "artifact hash", "tracked", /// "no staging area", "immutable", "no amend/--force" — never git's /// "commit"/"index"/"staged"/"HEAD"), and it teaches the slice-1 scope boundary /// (DX D2). Ends in exactly one newline. The e2e suite compares stdout against /// an independent transcription of this block, so drift is caught. pub const commit_help = withAliases(.commit, \\usage: 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-checks|--no-warnings] [--no-submanifest-advice] \\ [--] [<path>...] \\ \\Record a check-in: save the current contents of your tracked files as a \\new, permanent point in history on the current branch. \\ \\zit has no staging area. `commit` reads each tracked file straight from \\the working tree right now — there is nothing to stage first. With no \\paths, every tracked file is captured; name one or more paths to commit \\just those (the rest of the tree is carried forward unchanged). \\ \\The first commit in a new repository creates the root check-in and starts \\the default branch 'trunk'. Each check-in is named by its full artifact \\hash (SHA3-256). \\ \\--branch <name> starts a new branch at this check-in: the check-in moves \\onto <name> and later commits inherit it. <name> must not already name an \\open branch, and there must be a parent to fork from. --close marks this \\check-in a closed leaf, so it no longer shows as an open head of its branch. \\ \\History is immutable: a check-in is never rewritten. There is no amend and \\no --force; to undo a file, change it and commit again. To stop tracking a \\file (and record its deletion), use `zit rm` then commit. \\ \\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. -n reports what would \\be recorded and writes nothing. Only regular files are captured. \\ \\On a repository with sub-manifests enabled and pinned, a commit that \\drifts far enough from the analyzed optimum (draft-fossil-submanifests-00 \\§6.4) prints one advisory line and, interactively, offers to retune. \\"Far enough" is zit's §6.4 relative margin m: a re-tune is advised only \\when it would improve the analyzed read cost by more than 15% of the \\pinned value's own cost. --no-submanifest-advice suppresses the advisory \\entirely. Never affects whether the commit itself succeeds. \\ \\options: \\ -m, --message <message> the check-in comment \\ -M, --message-file <file> read the comment from <file> \\ --editor <prog> compose the comment in <prog> (default: \\ $VISUAL, then $EDITOR, on a terminal) \\ -n, --dry-run list what would be recorded; write nothing \\ --allow-empty record a check-in with no file changes \\ --user <login> author to record (default: $USER) \\ --branch <name> start a new branch at this check-in \\ --close mark this check-in a closed leaf \\ --allow-fork commit even though the current check-in is no \\ longer the tip of its branch — a deliberate \\ fork (refused otherwise, naming the newer tip) \\ --no-sync skip autosync (the pull before and push \\ after) for this one commit \\ --no-checks waive the binary-glob/crlf-glob/encoding-glob \\ content gate for this one commit \\ (--no-warnings is Fossil's spelling of the \\ same waiver) \\ --sign clearsign the manifest before storing it. \\ The signed text IS the stored artifact, so \\ every reader strips the wrapper to reach \\ the cards and the signature travels with \\ the check-in wherever it syncs. \\ The signer is $ZIT_PGP_COMMAND, fossil's \\ pgp-command contract: default \\ 'gpg --clearsign -o ', invoked as \\ COMMAND OUTFILE INFILE; a command whose \\ basename starts with 'ssh' is invoked as \\ COMMAND INFILE, writes INFILE.sig, and its \\ armor is assembled here. 'off' disables \\ signing. Refused — never committed \\ unsigned — if the signer fails or alters \\ the manifest it was given. \\ -q, --quiet do not print the check-in line \\ --no-submanifest-advice \\ suppress the sub-manifest drift advisory \\ -h, --help show this help and exit \\ \\exit status: \\ 0 the check-in was created \\ 1 nothing to commit, a tracked file is missing, an empty message, an \\ existing branch name, or this is not a zit repository \\ 2 usage error (no message, or an unknown option) \\ ); test "commit help states the live §6.4 margin, not a copy of it" { // draft-fossil-submanifests-00 §6.4 requires the relative margin `m` to be // DOCUMENTED, and this help block is the operator-facing statement that // discharges it (`submanifest.drift_margin_pct` names it as such). The e2e // help comparison pins this text against its transcription in `e2e.zig`, // but nothing there pins either against the constant the code actually // gates on -- both could keep saying "15%" after the constant moved. This // renders the constant and requires the sentence to contain it, so a change // to `drift_margin_pct` fails here until the prose is updated too. const rendered = try std.fmt.allocPrint( std.testing.allocator, "more than {d}% of", .{zit.submanifest.drift_margin_pct}, ); defer std.testing.allocator.free(rendered); try std.testing.expect(std.mem.indexOf(u8, commit_help, rendered) != null); } test { // Pull the CLI harness's inline unit tests into `zig build test`. The // repo-view loader and its cache now live in the `zit` module and aggregate // through root.zig, so they are not re-imported here. _ = @import("transport.zig"); _ = @import("serve.zig"); _ = @import("sync_driver.zig"); _ = @import("sync_driver_test.zig"); // The pure three-way merge engine behind `zit merge` (contracts/merge.md). // It imports only std, so its inline tests ride the CLI test binary here // (no build.zig change) exactly as `extras`/`undo` ride their importers. _ = @import("merge3.zig"); // The pure per-line provenance engine behind `zit annotate` // (contracts/annotate.md). Imports only std + merge3, so its inline tests // ride the CLI test binary here the same way. _ = @import("annotate.zig"); _ = @import("cmd/sync.zig"); _ = @import("submanifest_benchmark.zig"); _ = @import("cmd/tracking.zig"); _ = @import("cmd/history.zig"); _ = @import("cmd/checkout.zig"); _ = @import("cmd/verify.zig"); } /// A fixed `Ambient` for the in-process dispatch tests: a pinned epoch (no wall /// clock) and no environment author. Verbs other than `commit` ignore it; the /// commit dispatch tests that reach assembly pin their own values via the e2e /// harness (the real binary), so this constant is only the argument shape. const test_amb: cli.Ambient = .{ .now_epoch = 0, .user_env = null }; // --- status error-mapper unit tests (B-status-10/-11; pure switches) ------- test "status B-status-10: CheckoutError maps to the contract messages and exit 1" { const Case = struct { err: checkout.CheckoutError, want: []const u8 }; const cases = [_]Case{ .{ .err = error.Corrupt, .want = "zit status: cannot read checkout state: .zit-checkout is corrupt\n" }, .{ .err = error.Storage, .want = "zit status: cannot read checkout state: input/output error\n" }, }; for (cases) |c| { var buf: [256]u8 = undefined; var w = Io.Writer.fixed(&buf); var sink: [1]u8 = undefined; var wnull = Io.Writer.fixed(&sink); const s: cli.Streams = .{ .out = &wnull, .err = &w }; try std.testing.expectEqual(cli.Exit.failure, checkoutFail(s, "status", c.err)); try std.testing.expectEqualStrings(c.want, w.buffered()); } } test "status B-status-11: ManagedError maps to the contract messages and exit 1" { const Case = struct { err: managed.ManagedError, want: []const u8 }; const cases = [_]Case{ .{ .err = error.Corrupt, .want = "zit status: cannot read tracked files: .zit-checkout is corrupt\n" }, .{ .err = error.Storage, .want = "zit status: cannot read tracked files: input/output error\n" }, .{ .err = error.TooLong, .want = "zit status: cannot read tracked files: a tracked path is too long\n" }, .{ .err = error.TooMany, .want = "zit status: cannot read tracked files: too many tracked files\n" }, .{ .err = error.OutOfMemory, .want = "zit status: out of memory\n" }, }; for (cases) |c| { var buf: [256]u8 = undefined; var w = Io.Writer.fixed(&buf); var sink: [1]u8 = undefined; var wnull = Io.Writer.fixed(&sink); const s: cli.Streams = .{ .out = &wnull, .err = &w }; try std.testing.expectEqual(cli.Exit.failure, managedFail(s, "status", c.err)); try std.testing.expectEqualStrings(c.want, w.buffered()); } } test "status B-status-9: stateOom renders the out-of-memory message and exit 1" { var buf: [64]u8 = undefined; var w = Io.Writer.fixed(&buf); var sink: [1]u8 = undefined; var wnull = Io.Writer.fixed(&sink); const s: cli.Streams = .{ .out = &wnull, .err = &w }; try std.testing.expectEqual(cli.Exit.failure, stateOom(s, "status")); try std.testing.expectEqualStrings("zit status: out of memory\n", w.buffered()); } // --- status dispatch: the io/alloc-free paths, driven in-process ----------- // // `--help` and the two usage errors touch neither the filesystem nor the // allocators, so they run through `run()` over fixed-buffer writers — the same // technique the unknown-command dispatch tests use — with the exact argv. /// Drives `run()` for `zit status <tail...>` over fixed-buffer writers, /// returning the exit code and the captured stdout/stderr (valid until the next /// call). Sized to hold the whole `status_help` block on stdout. fn runStatusCapture(tail: []const []const u8) struct { code: cli.Exit, out: []const u8, err: []const u8 } { const S = struct { var out_buf: [4096]u8 = undefined; var err_buf: [512]u8 = undefined; }; var out = Io.Writer.fixed(&S.out_buf); var err = Io.Writer.fixed(&S.err_buf); const streams: cli.Streams = .{ .out = &out, .err = &err }; var argv_buf: [8][]const u8 = undefined; argv_buf[0] = "zit"; argv_buf[1] = "status"; for (tail, argv_buf[2..][0..tail.len]) |src, *dst| dst.* = src; const code = run( std.testing.allocator, std.testing.io, std.testing.allocator, argv_buf[0 .. 2 + tail.len], streams, test_amb, ); return .{ .code = code, .out = out.buffered(), .err = err.buffered() }; } test "status B-status-12: an unknown option is a usage error (exit 2, stderr, empty stdout)" { const r = runStatusCapture(&.{"-x"}); try std.testing.expectEqual(cli.Exit.usage, r.code); try std.testing.expectEqualStrings("", r.out); try std.testing.expectEqualStrings( "zit status: unknown option '-x'\ntry 'zit status --help'\n", r.err, ); } test "status B-status-13: an unexpected argument is a usage error (exit 2, stderr, empty stdout)" { const r = runStatusCapture(&.{"foo"}); try std.testing.expectEqual(cli.Exit.usage, r.code); try std.testing.expectEqualStrings("", r.out); try std.testing.expectEqualStrings( "zit status: unexpected argument 'foo'\ntry 'zit status --help'\n", r.err, ); } test "status B-status-14: --help prints the help to stdout (exit 0, empty stderr)" { const r = runStatusCapture(&.{"--help"}); try std.testing.expectEqual(cli.Exit.ok, r.code); try std.testing.expectEqualStrings(status_help, r.out); try std.testing.expectEqualStrings("", r.err); } test "status B-status-16 DX-D4: 'zit help status' routes to the verb's own help" { // `run()`'s help arm touches neither io nor the allocators, so the topic // routing is driven in-process; the stdout must be byte-identical to // `status --help` (the DX-D4 fix: `zit help status` no longer prints the // general command list that implied the verb was absent). var out_buf: [4096]u8 = undefined; var err_buf: [256]u8 = undefined; var out = Io.Writer.fixed(&out_buf); var err = Io.Writer.fixed(&err_buf); const streams: cli.Streams = .{ .out = &out, .err = &err }; const code = run( std.testing.allocator, std.testing.io, std.testing.allocator, &.{ "zit", "help", "status" }, streams, test_amb, ); try std.testing.expectEqual(cli.Exit.ok, code); try std.testing.expectEqualStrings(verb_help.get(.status).?, out.buffered()); try std.testing.expectEqualStrings("", err.buffered()); } test "init B-init-3: default hash_kind is SHA3-256 (= 2)" { // Guards the byte the header stores for a fresh repo (artifact §7.2-m2). try std.testing.expectEqual(@as(u8, 2), @intFromEnum(zit.artifact.defaultPolicy(false, false))); } // --- unknown-command dispatch (DX D2/D4), verified through `run()` ---------- // // `run()` on an unknown verb touches neither io nor the allocators, so these // drive it in-process over fixed-buffer writers — no subprocess needed — and // assert the same exit-2 / stderr-only contract the e2e suite holds for init. /// Runs `zit <verb>` through `run()`, returning the exit code plus the captured /// stdout/stderr slices (valid until the next call — backed by static buffers, /// so a test uses one invocation at a time). const RunCapture = struct { code: cli.Exit, out: []const u8, err: []const u8, var out_buf: [256]u8 = undefined; var err_buf: [512]u8 = undefined; fn run_verb(verb: []const u8) RunCapture { return run_argv(&.{ "zit", verb }); } /// Like `run_verb` but with the full argv (post-"zit" tokens included), for /// verbs whose diagnostic depends on an option. Only safe for control paths /// that return before `discoverRoot` (which allocates into the arena — here /// `testing.allocator` — and is freed en masse by a real arena in `main`). fn run_argv(argv: []const []const u8) RunCapture { var out = Io.Writer.fixed(&out_buf); var err = Io.Writer.fixed(&err_buf); const streams: cli.Streams = .{ .out = &out, .err = &err }; const code = run(std.testing.allocator, std.testing.io, std.testing.allocator, argv, streams, test_amb); return .{ .code = code, .out = out.buffered(), .err = err.buffered() }; } }; /// Asserts a git-reflex `verb` renders its teaching line: exit 2, stdout empty, /// stderr naming the verb, stating it is not a zit command, and containing the /// lesson keyword `teaches`. fn expectReflex(verb: []const u8, teaches: []const u8) !void { const r = RunCapture.run_verb(verb); try std.testing.expectEqual(cli.Exit.usage, r.code); try std.testing.expectEqualStrings("", r.out); try std.testing.expect(std.mem.indexOf(u8, r.err, verb) != null); try std.testing.expect(std.mem.indexOf(u8, r.err, "is not a zit command") != null); try std.testing.expect(std.mem.indexOf(u8, r.err, teaches) != null); } test "cli D2 B-init-12: 'rebase' teaches immutable history, exit 2 stderr-only" { try expectReflex("rebase", "history is immutable"); } test "cli amend B-amend-12: 'amend' dispatches to the real handler, not the git-reflex lesson" { // amend is a real verb now (removed from `reflex_table`): `zit amend` with no // NAME is amend's OWN usage error, never the immutable-history reflex lesson — // teaching against a fossil-semantics amend with that lesson would conflate it // with git's history-rewriting amend. const r = RunCapture.run_verb("amend"); try std.testing.expectEqual(cli.Exit.usage, r.code); try std.testing.expectEqualStrings("", r.out); try std.testing.expect(std.mem.indexOf(u8, r.err, "is not a zit command") == null); try std.testing.expect(std.mem.indexOf(u8, r.err, "no name given") != null); try std.testing.expect(std.mem.indexOf(u8, r.err, "try 'zit amend --help'") != null); } test "cli D2 B-init-12: 'reset' teaches immutable history, exit 2 stderr-only" { try expectReflex("reset", "history is immutable"); } test "cli D2 B-init-12: 'filter-branch' teaches immutable history, exit 2 stderr-only" { try expectReflex("filter-branch", "history is immutable"); } test "cli zit#156: 'checkout' is an alias of 'update' and dispatches to it" { // An unknown option draws update's own usage error — proof the alias // reached the real handler rather than a git-reflex lesson. (A bare // `update` is no longer a usage error: it means --latest.) const r = RunCapture.run_argv(&.{ "zit", "checkout", "--nope" }); try std.testing.expectEqual(cli.Exit.usage, r.code); try std.testing.expectEqualStrings("", r.out); try std.testing.expect(std.mem.indexOf(u8, r.err, "is not a zit command") == null); try std.testing.expect(std.mem.indexOf(u8, r.err, "update") != null); } test "cli push: an unknown option is a usage error before any I/O (real verb, dispatched)" { // `push` is a real handler, not a git-reflex redirect. Under M4 an omitted URL // is no longer an immediate usage error (it falls back to the saved remote), // but a malformed option is still rejected by the arg parser before any I/O — // exit 2, the diagnostic on stderr, empty stdout — proving real dispatch. const r = RunCapture.run_argv(&.{ "zit", "push", "--nope" }); try std.testing.expectEqual(cli.Exit.usage, r.code); try std.testing.expectEqualStrings("", r.out); try std.testing.expectEqualStrings( "zit push: unknown option '--nope'\ntry 'zit push --help'\n", r.err, ); } test "cli pull: an unknown option is a usage error before any I/O (real verb, dispatched)" { // `pull` is a real handler, not a git-reflex redirect. Under M4 an omitted URL // falls back to the saved remote rather than erroring immediately; a malformed // option is still rejected before any I/O — exit 2, stderr-only. const r = RunCapture.run_argv(&.{ "zit", "pull", "--nope" }); try std.testing.expectEqual(cli.Exit.usage, r.code); try std.testing.expectEqualStrings("", r.out); try std.testing.expectEqualStrings( "zit pull: unknown option '--nope'\ntry 'zit pull --help'\n", r.err, ); } test "cli D2 B-init-12: 'fetch' teaches bidirectional sync, exit 2 stderr-only" { try expectReflex("fetch", "zit sync"); } test "cli D2 B-init-12: 'stash' states it is not implemented, exit 2 stderr-only" {} test "cli commit B-commit-5: 'zit commit' with no -m is a usage error (real verb, dispatched)" { // Regression: `commit` is now a real handler, not a planned stub. `run()`'s // commit arm raises the missing-message usage error before any I/O, so this // is driven in-process — exit 2, stderr the B-commit-5 message, empty stdout. const r = RunCapture.run_verb("commit"); try std.testing.expectEqual(cli.Exit.usage, r.code); try std.testing.expectEqualStrings("", r.out); try std.testing.expectEqualStrings( "zit commit: a check-in message is required\n" ++ " give one with -m <message>,\n" ++ " run from a terminal to compose it in $EDITOR,\n" ++ " or set an editor: zit settings editor <command>\n" ++ "try 'zit commit --help'\n", r.err, ); } test "cli clone: 'zit clone' with no arguments is a usage error (real verb, dispatched)" { // `clone` is now a real handler, not a planned stub. `run()`'s clone arm // parses the empty argument list and raises the missing-URL usage error // before any I/O — exit 2, the diagnostic on stderr, empty stdout. const r = RunCapture.run_verb("clone"); try std.testing.expectEqual(cli.Exit.usage, r.code); try std.testing.expectEqualStrings("", r.out); try std.testing.expectEqualStrings( "zit clone: no repository URL given\ntry 'zit clone --help'\n", r.err, ); } test "cli D4 B-init-12: a near-miss verb suggests the closest command, exit 2 stderr-only" { // `ini` would now resolve as a prefix of `init` (zit#156); a transposition // is the near-miss that still needs a suggestion. const r = RunCapture.run_verb("inti"); try std.testing.expectEqual(cli.Exit.usage, r.code); try std.testing.expectEqualStrings("", r.out); try std.testing.expect(std.mem.indexOf(u8, r.err, "unknown command 'inti'") != null); try std.testing.expect(std.mem.indexOf(u8, r.err, "did you mean 'init'?") != null); } test "cli D4 B-init-12: a typo of a planned verb suggests the canonical command" { // `comit` is neither reflex nor exactly planned; the generic path should // suggest the planned canonical `commit` (planned verbs join the near-miss // set), so the typo does not dead-end. const r = RunCapture.run_verb("comit"); try std.testing.expectEqual(cli.Exit.usage, r.code); try std.testing.expectEqualStrings("", r.out); try std.testing.expect(std.mem.indexOf(u8, r.err, "unknown command 'comit'") != null); try std.testing.expect(std.mem.indexOf(u8, r.err, "did you mean 'commit'?") != null); } test "cli D4 B-init-12: a typo of 'update' suggests it (every dispatched verb is suggestable)" { // `update` is a real dispatched verb, so a near-miss must land on it. Guards // against the suggester's candidate list drifting from the dispatched set. const r = RunCapture.run_verb("updtae"); try std.testing.expectEqual(cli.Exit.usage, r.code); try std.testing.expectEqualStrings("", r.out); try std.testing.expect(std.mem.indexOf(u8, r.err, "unknown command 'updtae'") != null); try std.testing.expect(std.mem.indexOf(u8, r.err, "did you mean 'update'?") != null); } test "cli D4 B-init-12: a distant unknown lists commands with no misleading guess" { const r = RunCapture.run_verb("florble"); try std.testing.expectEqual(cli.Exit.usage, r.code); try std.testing.expectEqualStrings("", r.out); try std.testing.expect(std.mem.indexOf(u8, r.err, "unknown command 'florble'") != null); try std.testing.expect(std.mem.indexOf(u8, r.err, "did you mean") == null); // Two lines, not the command list (zit#156). try std.testing.expect(std.mem.indexOf(u8, r.err, "try 'zit help'") != null); try std.testing.expect(std.mem.indexOf(u8, r.err, "commands:") == null); } // --- commit error-mapper unit tests (B-commit-15; pure switches) ----------- test "update: ApplyError maps to the contract messages and exit 1, exhaustively over all 23 variants" { const target_id = "1" ** 40; const Case = struct { err: update.ApplyError, want: []const u8 }; const cases = [_]Case{ .{ .err = error.OutOfMemory, .want = "zit update: out of memory\n" }, .{ .err = error.InvalidCheckinId, .want = "zit update: invalid check-in ID '" ++ target_id ++ "' (expected 40 or 64 lowercase hexadecimal digits)\n", }, .{ .err = error.CheckinNotFound, .want = "zit update: check-in '" ++ target_id ++ "' not found\n" }, .{ .err = error.CheckinContentMissing, .want = "zit update: cannot read repository: check-in '" ++ target_id ++ "' content is missing\n", }, .{ .err = error.CheckinNameMismatch, .want = "zit update: cannot read repository: check-in '" ++ target_id ++ "' content does not match its name\n", }, .{ .err = error.TargetNotManifest, .want = "zit update: check-in '" ++ target_id ++ "' is not a valid manifest\n" }, .{ .err = error.BaselineNotFound, .want = "zit update: cannot read repository: check-in '" ++ target_id ++ "' baseline is not found\n", }, .{ .err = error.BaselineContentMissing, .want = "zit update: cannot read repository: check-in '" ++ target_id ++ "' baseline content is missing\n", }, .{ .err = error.BaselineNameMismatch, .want = "zit update: cannot read repository: check-in '" ++ target_id ++ "' baseline content does not match its name\n", }, .{ .err = error.BaselineNotManifest, .want = "zit update: check-in '" ++ target_id ++ "' baseline is not a valid manifest\n", }, .{ .err = error.BaselineIsDelta, .want = "zit update: check-in '" ++ target_id ++ "' baseline is not fully resolvable\n", }, .{ .err = error.StateRecordInvalid, .want = "zit update: cannot read repository: " ++ update.state_record_invalid_reason ++ "\n", }, .{ .err = error.InvalidPath, .want = "zit update: check-in '" ++ target_id ++ "' names an invalid file path\n" }, .{ .err = error.ReservedPath, .want = "zit update: check-in '" ++ target_id ++ "' names a reserved file path\n" }, .{ .err = error.SymlinkUnsupported, .want = "zit update: check-in '" ++ target_id ++ "' contains an unsupported symlink entry\n", }, .{ .err = error.CurrentStateInvalid, .want = "zit update: current working-tree state is invalid\n" }, .{ .err = error.WorktreeNotClean, .want = "zit update: working tree is not clean\n" }, .{ .err = error.Corrupt, .want = "zit update: cannot read repository: repository is corrupt\n" }, .{ .err = error.UnmanagedCollision, .want = "zit update: an untracked file or directory is in the way\n" }, .{ .err = error.Storage, .want = "zit update: cannot update working tree: input/output error\n" }, .{ .err = error.TransactionUnavailable, .want = "zit update: cannot create a transaction directory\n" }, .{ .err = error.RollbackFailed, .want = "zit update: update failed and could not be rolled back; the previous working-tree state is retained for recovery\n", }, .{ .err = error.CleanupFailed, .want = "zit update: update committed, but the transaction directory could not be removed\n", }, }; for (cases) |c| { var buf: [256]u8 = undefined; var w = Io.Writer.fixed(&buf); var sink: [1]u8 = undefined; var wnull = Io.Writer.fixed(&sink); const s: cli.Streams = .{ .out = &wnull, .err = &w }; try std.testing.expectEqual(cli.Exit.failure, updateApplyFail(s, target_id, c.err)); try std.testing.expectEqualStrings(c.want, w.buffered()); } } /// Render one `ApplyError` through `updateApplyFail` and return the stderr text. fn updateFailText(buf: []u8, err: update.ApplyError) []const u8 { var w = Io.Writer.fixed(buf); var sink: [1]u8 = undefined; var wnull = Io.Writer.fixed(&sink); const s: cli.Streams = .{ .out = &wnull, .err = &w }; _ = updateApplyFail(s, "1" ** 40, err); return w.buffered(); } // `Corrupt` and `StateRecordInvalid` are two different claims and must never // share a diagnostic again. `Corrupt` says stored bytes do not match the name // they are stored under — real damage. `StateRecordInvalid` says nothing about // the repository's integrity: a stored artifact was not usable as a state // record. An intact fossil clone reaches it routinely, because fossil stores // and syncs artifacts it itself declines to crosslink, so answering it with a // corruption claim accuses an undamaged repository. test "update: the two read-path failures do not share a diagnostic" { var a: [256]u8 = undefined; var b: [256]u8 = undefined; const corrupt = updateFailText(&a, error.Corrupt); const state = updateFailText(&b, error.StateRecordInvalid); try std.testing.expect(!std.mem.eql(u8, corrupt, state)); // Only the integrity failure may allege corruption. try std.testing.expect(std.mem.indexOf(u8, corrupt, "corrupt") != null); try std.testing.expect(std.mem.indexOf(u8, state, "corrupt") == null); } test "tracked file size limit admits its boundary" { const limit: u64 = max_tracked_file_bytes; try std.testing.expect(trackedFileSizeAllowed(limit - 1)); try std.testing.expect(trackedFileSizeAllowed(limit)); try std.testing.expect(!trackedFileSizeAllowed(limit + 1)); } test "the tracked-file ceiling is the storage frame ceiling, not a policy number" { // The comptime assert beside the declaration is the real enforcement; this // states the same tie as a readable claim, and says WHY it is not tunable: // `logstore.put`/`putArtifact` assert on `max_payload`, so admitting a file // above it would trip an assert during commit rather than refuse the file. try std.testing.expectEqual(logstore.max_payload, @as(u64, max_tracked_file_bytes)); // The largest admissible file is exactly one storable frame — the boundary // the assert allows, not one byte inside or outside it. try std.testing.expect(trackedFileSizeAllowed(logstore.max_payload)); try std.testing.expect(!trackedFileSizeAllowed(logstore.max_payload + 1)); } // --- renderBranchList unit tests (repo-state §6.1/§6.3/§6.4/§9) ------------- // The derived §6.3 state (which rid represents a branch, its closed status, the // grouping) is proven in repostate_test.zig; these tests fix only the CLI // presentation over hand-built BranchEntry values transcribed from §6.3. const BranchEntry = repostate.RepoState.BranchEntry; /// Renders `entries`/`current` into a fixed buffer and returns the written /// slice — the shared body of the presentation tests. fn renderBranchesToBuf(buf: []u8, entries: []const BranchEntry, current: ?[]const u8) []const u8 { var w = Io.Writer.fixed(buf); renderBranchList(&w, entries, current); return w.buffered(); } test "renderBranchList §6.3: a representative-closed branch renders \"(closed)\"" { // §6.3 divergence: a forked branch whose greatest-mtime representative is // closed lists as closed even while an older leaf stays open. The closed // flag is the representative-row property; render appends the marker. const entries = [_]BranchEntry{ .{ .name = "trunk", .tip = 1, .mtime = 3, .count = 2, .closed = true }, }; var buf: [64]u8 = undefined; try std.testing.expectEqualStrings(" trunk (closed)\n", renderBranchesToBuf(&buf, &entries, null)); } test "renderBranchList §6.3: multiple branches render name-ascending, current marked once" { // `branches` returns entries name-ascending; render preserves that order and // marks exactly the current branch, indenting the rest. const entries = [_]BranchEntry{ .{ .name = "feature", .tip = 2, .mtime = 5, .count = 1, .closed = false }, .{ .name = "trunk", .tip = 1, .mtime = 9, .count = 3, .closed = false }, }; var buf: [64]u8 = undefined; try std.testing.expectEqualStrings( "* feature\n trunk\n", renderBranchesToBuf(&buf, &entries, "feature"), ); } test "renderBranchList §9: a forked branch (two leaves) renders as one line" { // §9: a branch with more than one leaf is forked but is still a single §6.3 // entry (count > 1), so it lists on exactly one line. const entries = [_]BranchEntry{ .{ .name = "trunk", .tip = 7, .mtime = 4, .count = 2, .closed = false }, }; var buf: [64]u8 = undefined; const out = renderBranchesToBuf(&buf, &entries, "trunk"); try std.testing.expectEqualStrings("* trunk\n", out); try std.testing.expectEqual(@as(usize, 1), std.mem.count(u8, out, "\n")); } test "renderBranchList §6.4: a null current-branch marks no row" { // Negative space: with no resolvable current branch, no line carries "*". const entries = [_]BranchEntry{ .{ .name = "feature", .tip = 2, .mtime = 5, .count = 1, .closed = false }, .{ .name = "trunk", .tip = 1, .mtime = 9, .count = 1, .closed = false }, }; var buf: [64]u8 = undefined; const out = renderBranchesToBuf(&buf, &entries, null); try std.testing.expectEqualStrings(" feature\n trunk\n", out); try std.testing.expect(std.mem.indexOfScalar(u8, out, '*') == null); } test "renderBranchList §6.1: the configured main-branch name marks its row (not a literal)" { // The current-branch name is whatever branchOf returned through the config // main-branch — here "mainline", not "trunk". Render marks that row, so a // non-trunk default is honored end to end. const entries = [_]BranchEntry{ .{ .name = "mainline", .tip = 1, .mtime = 9, .count = 1, .closed = false }, .{ .name = "trunk", .tip = 2, .mtime = 3, .count = 1, .closed = false }, }; var buf: [64]u8 = undefined; try std.testing.expectEqualStrings( "* mainline\n trunk\n", renderBranchesToBuf(&buf, &entries, "mainline"), ); } // =========================================================================== // `zit pack` — repack/GC tests. Storage on-disk layout is unspecified by the // RFCs, so the oracle is the store's OWN semantics: every artifact's content // and name survive a repack byte-for-byte, the set is preserved (no drop), the // §7.7 name↔content invariant is re-checked before every write, the packed // bytes are a deterministic function of the source content (idempotent), and a // crash before the atomic rename leaves the original store untouched. Stores // are built as FULL frames (the migration shape pack exists to compact), // through the real `LogStore`/`artifact` — never seeded from fossil. // =========================================================================== /// Store `content` as a FULL raw frame and return its 64-hex artifact name. pub fn packSeedBlob(log: *logstore.LogStore, content: []const u8) ![64]u8 { var digest: [digest_len]u8 = undefined; std.crypto.hash.sha3.Sha3_256.hash(content, &digest, .{}); _ = try log.put(&digest, content); var name: [64]u8 = undefined; _ = zit.artifact.artifactName(.sha3_256, content, &name); return name; } /// Serialize and store (as a FULL frame) a one-file check-in manifest snapshotting /// `file_name` → `blob_name`, optionally descended from `parent_name`. Writes the /// manifest's own 64-hex name into `name_out`. Root check-ins carry a propagating /// `branch=trunk` T card; children carry the parent P card and inherit trunk. pub fn packSeedCheckin( gpa: std.mem.Allocator, log: *logstore.LogStore, ts: []const u8, parent_name: ?[]const u8, file_name: []const u8, blob_name: []const u8, name_out: *[64]u8, ) !void { var pbuf: [1]zit.artifact.PCard = undefined; var p_slice: []const zit.artifact.PCard = &.{}; if (parent_name) |pn| { pbuf[0] = .{ .parents = pn, .count = 1 }; p_slice = pbuf[0..1]; } const fcards = [_]zit.artifact.FCard{ .{ .filename = file_name, .hash = blob_name, .perm = null, .old_name = null, .mode = .regular }, }; const root_tags = [_]zit.artifact.TCard{ .{ .sign = .propagate, .name = "branch", .target = null, .value = "trunk" }, }; var m: zit.artifact.Manifest = .{ .arena_state = std.heap.ArenaAllocator.init(gpa), .kind = .manifest, .a = null, .b = null, .c = .{ .comment = "c" }, .d = .{ .timestamp = ts }, .e = null, .g = null, .h = null, .i = null, .k = null, .l = null, .n = null, .r = null, .u = .{ .login = "u" }, .z = null, .w = null, .f = &fcards, .j = &.{}, .m = &.{}, .p = p_slice, .q = &.{}, .t = if (parent_name == null) &root_tags else &.{}, }; defer m.deinit(); const bytes = try zit.artifact.create(&m, gpa); defer gpa.free(bytes); var digest: [digest_len]u8 = undefined; std.crypto.hash.sha3.Sha3_256.hash(bytes, &digest, .{}); _ = try log.put(&digest, bytes); _ = zit.artifact.artifactName(.sha3_256, bytes, name_out); } /// A large, near-identical evolving file: 128 identical lines plus a per-version /// trailer, so successive versions delta to a fraction of the full blob (the /// change is materially < 90%). Caller frees. pub fn packEvolvingBlob(gpa: std.mem.Allocator, version: u8) ![]u8 { var buf: std.ArrayList(u8) = .empty; errdefer buf.deinit(gpa); var i: usize = 0; while (i < 128) : (i += 1) try buf.print(gpa, "line {d} of the tracked file\n", .{i}); try buf.print(gpa, "trailer version {d}\n", .{version}); return buf.toOwnedSlice(gpa); } /// Open a fresh, empty (header + config-less) writable `.zit` in `dir`. pub fn packOpenWritable(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, fs_out: *storage.FileStorage) !logstore.LogStore { const hash_kind: u8 = @intFromEnum(zit.artifact.defaultPolicy(false, false)); const f = try dir.createFile(io, repo_filename, .{ .read = true, .exclusive = true }); fs_out.* = storage.FileStorage.init(io, f, 0); return logstore.LogStore.open(gpa, fs_out.storageFile(), hash_kind); } /// Reopen an existing `.zit` in `dir` read-only. pub fn packOpenReadOnly(gpa: std.mem.Allocator, io: std.Io, dir: std.Io.Dir, fs_out: *storage.FileStorage) !logstore.LogStore { const f = try dir.openFile(io, repo_filename, .{}); const st = try f.stat(io); fs_out.* = storage.FileStorage.init(io, f, st.size); return logstore.LogStore.openReadOnly(gpa, fs_out.storageFile()); } test "help #68: every options-table flag appears in its verb's usage block" { // Goal: kill the usage-line/options-table drift class (the shipped // `commit --no-sync` omission) mechanically. Method: for every pub // `*_help` declaration, take the usage block (lines from `usage:` to the // first blank line) and assert each long flag introduced by an // options-table row (` --flag` / ` -x, --flag`) appears in it. // Topics and fragments carry no usage block and are skipped. const decls = comptime blk: { const all = @typeInfo(@This()).@"struct".decls; var names: [all.len][]const u8 = undefined; var n: usize = 0; for (all) |d| { if (std.mem.endsWith(u8, d.name, "_help")) { names[n] = d.name; n += 1; } } break :blk names[0..n].*; }; inline for (decls) |name| { const text: []const u8 = @field(@This(), name); try checkHelpUsageFlags(name, text); } } /// The runtime half of the usage-completeness test: one help's check. fn checkHelpUsageFlags(name: []const u8, text: []const u8) !void { { if (std.mem.indexOf(u8, text, "usage:") == null) return; const usage_start = std.mem.indexOf(u8, text, "usage:").?; const usage_end = std.mem.indexOfPos(u8, text, usage_start, "\n\n") orelse text.len; const usage = text[usage_start..usage_end]; var it = std.mem.splitScalar(u8, text[usage_end..], '\n'); while (it.next()) |line| { // An options-table row: two-or-more leading spaces, then the // flag column. Continuation lines (deeper indent, no dash) and // prose never match. const trimmed = std.mem.trimStart(u8, line, " "); if (trimmed.len == 0) continue; if (line.len - trimmed.len < 2 or line.len - trimmed.len > 6) continue; if (trimmed[0] != '-') continue; // The row's flag forms: a short `-x` and/or a long `--flag`. // The drift class is a flag ENTIRELY absent from usage (the // shipped `commit --no-sync` case) — a usage line naming only // the short form is the house convention, not drift. var short_form: ?[]const u8 = null; var long_form: ?[]const u8 = null; var toks = std.mem.tokenizeAny(u8, trimmed, " ,=["); while (toks.next()) |tok| { if (std.mem.startsWith(u8, tok, "--")) { long_form = tok; break; } if (tok.len >= 2 and tok[0] == '-') short_form = tok; } if (long_form == null and short_form == null) continue; // `-h, --help` and `-R, --repo` are GLOBAL options (lifted // before the verb's own parser; `--repo` has its own topic, // `zit help repo`) — a usage line may restate them but their // absence is not the drift class this test kills. if (long_form != null and std.mem.eql(u8, long_form.?, "--help")) continue; if (long_form != null and std.mem.eql(u8, long_form.?, "--repo")) continue; const long_ok = long_form != null and std.mem.indexOf(u8, usage, long_form.?) != null; const short_ok = short_form != null and std.mem.indexOf(u8, usage, short_form.?) != null; if (!long_ok and !short_ok) { std.debug.print("{s}: flag {s} missing from usage block\n", .{ name, long_form orelse short_form.? }); return error.TestUnexpectedResult; } } } }