Skip to main content

mkit_cli/
cli.rs

1//! CLI surface constants shared by the main dispatcher and the snapshot
2//! tests.
3//!
4//! `CLI_VERSION` MUST equal `env!("CARGO_PKG_VERSION")` — `build.rs`
5//! enforces this at compile time so cosmetic edits to `Cargo.toml` can
6//! never desync the Homebrew contract documented in `docs/CLI.md`.
7//! `mkit version` MUST emit exactly `"mkit <X.Y.Z>\n"`.
8
9/// Version string rendered by `mkit version`. Pinned to the package
10/// version at compile time via `env!`.
11pub const CLI_VERSION: &str = env!("CARGO_PKG_VERSION");
12
13/// Full help text for `mkit --help` / `mkit help` / `mkit` (with no
14/// args). Pinned by snapshot tests so downstream tooling that greps the
15/// binary output sees a stable surface.
16pub const HELP_TEXT: &str = "\
17usage: mkit <command> [args]
18
19commands:
20  init              Create a new mkit repository
21  add [-A|-u] [-f] <path>...  Stage files for the next commit
22  add .             Stage all files under cwd (respects .gitignore/.mkitignore)
23  add -A            Stage all changes incl. deletions (no path args)
24  add -u            Restage only already-tracked files (no path args)
25  add -f <path>     Stage an ignored path (overrides .gitignore/.mkitignore)
26  add -p <path>...  Interactively choose hunks to stage (y/n/q/a/d per hunk)
27  rm [--cached] [-r] [-f] <path>...  Remove path(s) and stage the deletion
28  rm --cached       Stage the removal only; keep the worktree file(s)
29  mv [-f] <source>... <dest>  Move/rename tracked path(s) and stage it
30                    (into <dest> when it is an existing directory; -f
31                    overwrites an existing destination)
32  restore [--staged] [--worktree] [--source <rev>] [-f] <path>...
33                    Discard worktree changes for path(s) (restore from the
34                    index), or --staged to unstage (restore the index entry
35                    from HEAD); -f overrides the un-staged-edit guard
36  reset [--soft|--mixed|--hard] [-f] [-q] [<commit>]
37                    Move HEAD/branch (--soft) or HEAD + reset the index to
38                    the commit's tree (--mixed, default); worktree untouched.
39                    --hard also resets the worktree (keeps untracked files);
40                    refuses to discard dirty/staged content without -f
41  hash <file>       Hash a file and store it as a blob
42  cat <hash>        Display an object by its hash
43  cat-file (-t|-s|-p) <object> | cat-file --batch
44                    Show an object's type, size, or content
45                    (-p: blob bytes, tree listing, or commit/tag summary;
46                    --batch reads object names from stdin, takes no <object>)
47  show [--stat] [<object>...] Display objects (default HEAD): a commit/remix
48                    with its diff vs the first parent, a tag then its target, a
49                    tree listing, or a blob's contents (--stat shows a diffstat
50                    instead of the patch for commits/remixes)
51  tree              Snapshot working directory as a tree object
52  ls-tree [-r] [-z] <tree-ish> [<path>...]
53                    List a tree's entries as `<mode> <type> <hash>\t<name>`
54                    (-r recurses; -z NUL-terminates with raw paths)
55  ls-files [-s] [-z] [--others] [--ignored] [--exclude-standard]
56                    List tracked files (-s adds stage info; --others lists
57                    untracked; --exclude-standard drops ignored)
58  rev-parse [--verify] [--short[=N]] [--abbrev-ref] [--show-toplevel] [<rev>...]
59                    Resolve revisions to object ids (--short abbreviates,
60                    --abbrev-ref HEAD prints the branch, --show-toplevel
61                    prints the repo root; --verify is accepted for git-script
62                    compatibility — mkit always errors on a bad revision)
63  merge-base [--is-ancestor] <a> <b>  Print the common ancestor of two
64                    commits (--is-ancestor tests ancestry: exit 0/1, no output)
65  rev-list [--count] <rev>  List commit ids reachable from <rev> (--count
66                    prints the number)
67  show-ref [--heads] [--tags]  List refs as `<hash> <refname>`
68  for-each-ref [--format=<fmt>] [<pattern>...]
69                    Iterate refs, optionally with a %(atom) format string
70  symbolic-ref [--short] <name> [<ref>]
71                    Read a symbolic ref, or (with <ref>) repoint it
72                    (e.g. symbolic-ref HEAD refs/heads/main)
73  update-ref [-d] <ref> [<newvalue> [<oldvalue>]]
74                    Create/update/delete refs/heads/* or refs/tags/*
75                    (<oldvalue> compare-and-swap; all-zero = must be absent,
76                    update mode only; -d's <oldvalue> must be concrete)
77  ref list [--pattern <glob>]  List every ref's full name + resolved hash
78                    (`<refname> <hash>`, sorted; covers refs/heads/*,
79                    refs/tags/*, refs/remotes/*/*; --pattern is a shell glob
80                    on the full name, `*` spans `/`)
81  ref cat <name>    Print the resolved hash for one ref (HEAD,
82                    refs/heads/<b>, refs/tags/<t>, or refs/remotes/<r>/<b>),
83                    following HEAD's symbolic indirection
84  commit [-a] [-q] [--amend] [-m <msg> | -F <file>] [--author <spec>] [--format=json]
85                    Create a signed commit (opens $EDITOR if -m/-F omitted).
86                    -F reads the message from <file> (`-` = stdin); --author
87                    overrides the commit author. After `merge --no-commit`
88                    (or a resolved merge) this records a two-parent merge commit.
89                    -q suppresses the summary; -S/--gpg-sign, --no-verify,
90                    --no-edit are accepted no-ops (mkit always signs, no hooks);
91                    --format=json emits a JSON result object on success
92  commit --amend [-m <msg>]  Replace HEAD: re-commit on HEAD's parent, re-sign,
93                    move the branch. Reuses HEAD's message if -m omitted.
94                    The superseded commit becomes unreachable until `gc` ships.
95  log [--oneline] [--abbrev-commit] [--abbrev[=N]] [--format=json] [--graph] [--author PAT] [--grep PAT] [--since DATE] [--until DATE] [--no-merges] [--first-parent] [-n N] [<rev> | <A>..<B> | <A>...<B>]
96                    Show commit history (default prints the full message
97                    body + a UTC date; --oneline/--abbrev-commit abbreviate
98                    the commit id, --abbrev[=N] sets the length (default 7);
99                    --format=json emits JSONL with the raw timestamp;
100                    --graph is accepted but currently a no-op). --author/
101                    --grep filter by substring match on the author identity
102                    / commit message; --since/--until bound the commit
103                    timestamp (@<unix>, now/today/yesterday, N-unit-ago,
104                    or YYYY-MM-DD[ HH:MM:SS]); --no-merges hides
105                    merge commits from the output; --first-parent walks
106                    only first parents, never entering a merged side
107                    branch. Filters apply before -n's limit. Optional
108                    <rev> starts the walk there; <A>..<B> shows commits in B
109                    not in A; <A>...<B> the symmetric difference (empty
110                    side = HEAD)
111  reflog [<ref>] [--format=json] [-n N]
112                    Show a branch's first-parent ancestry (read-only).
113                    Lists the branch's first-parent chain (newest first,
114                    addressed <ref>@{N}); defaults to HEAD's branch. With
115                    --features history-mmr, cross-checks each entry against
116                    the canonical ancestry snapshot. Not a full Git reflog:
117                    @{N} indexes the reachable chain, so superseded commits
118                    (after amend/reset) are not listed.
119  status [--porcelain[=v1|v2]] [-s|--short] [-z]
120                    Show staged and working tree changes (--porcelain, or
121                    its -s/--short alias, emits machine-readable XY lines;
122                    --porcelain=v2 emits git's richer per-path format with
123                    modes + object ids; special-byte paths are C-style
124                    quoted; -z NUL-terminates records with raw paths)
125  diff [--staged|--cached] [--name-only|--name-status|--stat] [--merge-base] [--exit-code|--quiet] [--color[=WHEN]|--no-color] [-w|-b] [-U N] [-z] [<rev> [<rev>] | <a>..<b> | <a>...<b>] [<path>...]
126                    Show changes as a unified patch (HEAD vs workdir,
127                    --staged for HEAD vs index, a single revision vs the
128                    worktree, two revisions, an A..B range, or an A...B
129                    symmetric range = merge-base(a,b) vs b; --merge-base
130                    diffs the merge base of the given revisions (the flag
131                    spelling of A...B); revisions
132                    are refs, commits, or short hashes). --name-only lists
133                    changed paths; --name-status prefixes each with an
134                    A/D/M (T = mode change) letter; --stat shows per-file
135                    change counts + a +/- graph and a summary line; -z
136                    NUL-terminates name-only/-status records with raw paths
137                    (else special-byte paths are C-style quoted); -w/
138                    --ignore-all-space and -b/--ignore-space-change control
139                    whitespace sensitivity (-w wins if both given); -U<n>/
140                    --unified=<n> sets the context-line count (default 3)
141  branch [-v|--verbose] [--format=json]
142                    List branches (* marks current; no commit id by
143                    default, like git; -v adds the abbreviated id +
144                    subject; JSONL with --format=json)
145  branch <name>     Create a branch at HEAD
146  branch -d <name>  Delete a branch (safe; refuses the current branch)
147  branch -D <name>  Force-delete a branch (errors on an absent branch,
148                    like git; still refuses the current branch)
149  branch -m [<old>] <new>  Rename a branch (current branch if <old> omitted)
150  branch --show-current  Print the current branch name
151  branch [--list] [--contains [<c>]] [--no-contains [<c>]] [--merged [<c>]] [--no-merged [<c>]] [<pattern>...]
152                    Filter the listing (like git): <pattern> are shell globs on
153                    branch names (enabled by --list or any filter; `*`/`?`/`[…]`,
154                    `*` spans `/`); --contains keeps branches whose tip has <c>
155                    as an ancestor; --merged keeps those merged into <c>; the
156                    --no-* forms invert. All four commit args default to HEAD
157                    when omitted
158  checkout <branch> Switch HEAD to a branch and restore files
159  checkout -b|-B <new> [<start>]  Create (or -B reset) a branch and switch to it
160  switch <branch>   Switch branches (git switch)
161  switch -c|-C <new> [<start>]    Create (or -C reset) a branch and switch to it
162  clean [-n] [-f] [-d] [-x|-X] [<path>...]
163                    Remove untracked files (refuses without -f; -n
164                    previews). -d also removes untracked dirs; -x includes
165                    ignored files, -X removes only ignored
166  tag [<name>] [<commit>] [--format=json]  List tags, or create a lightweight tag
167  tag -l [<pattern>]  List tags, optionally filtered by a shell glob
168                    (JSONL with --format=json)
169  tag -a <name> [-m <msg>] [--author <spec>] [<commit>]  Create an annotated tag
170  tag -s <name> [-m <msg>] [--author <spec>] [<commit>]  Create a signed tag
171  tag -d <name>     Delete a tag
172  config [--format=json]  Show all configuration values (JSON with --format=json)
173  config <key> [--format=json]  Show one value
174  config <key> <value>  Set a configuration value
175  config --unset <key>  Remove a configuration value (idempotent; unknown key errors)
176  config [--local|--global] <key> <value>|--unset <key>  Force repo/user scope
177  config user.identity <value>  Set author Identity
178                        (ed25519:<hex>, mid:<N>, or raw [kind][len][bytes] hex)
179  config user.name|user.email <value>  Git-compatibility aliases; stored and
180                        round-tripped but NON-authoritative — they never set
181                        the signed author (use user.identity for that)
182  config trusted_remote_endpoint <url>  Trust an HTTP/S3 remote for ambient env credentials
183  config ssh.strict_host_key_checking <yes|no|accept-new>  Override SSH host policy
184  config ssh.user_known_hosts_file <path>  Custom SSH known_hosts file
185  config ssh.identity_file <path>  SSH private key file
186  merge [--no-commit] [-m <msg>] [--format=json] <branch> | --continue | --abort
187                    Merge a branch into HEAD (--no-commit stages the merge and
188                    stops before committing; finish with `mkit commit` or
189                    `mkit merge --continue`; -m overrides the merge message;
190                    --format=json emits a JSON outcome object)
191  push [<remote>] [-u|--set-upstream] [--all] [-f|--force|--force-with-lease] [--dry-run] [--format=json]
192                    Push current branch to its upstream (--all mirrors every
193                    branch; -u records the upstream; prints git's `To <url>` +
194                    ref-update summary, or `Everything up-to-date`;
195                    --format=json emits a JSON outcome object)
196  pull [<remote>] [--all] [--format=json]  Pull changes from remote (fast-forward;
197                    prints `Updating <a>..<b>`/`Fast-forward`/diffstat, or
198                    `Already up to date.`); --all pulls every configured remote;
199                    --format=json emits a JSON outcome object
200  fetch [<remote>] [--all] [--format=json]  Download from remote without merging
201                    (prints `From <url>` + per-ref summary; silent when nothing
202                    changed); --all fetches every configured remote;
203                    --format=json emits a JSON outcome object
204  worktree add <path> [<commit-ish>]  Create a linked working tree (shares the
205                    object store and refs; own HEAD/index/stash). With no
206                    commit-ish, creates a branch named after the path
207  worktree list [--porcelain]  List the main and every linked working tree
208  worktree remove [--force] <path>     Remove a linked working tree
209  worktree prune [--dry-run]   Delete registry entries whose tree is gone
210  stash [--format=json]  Stash working dir changes (save WIP; -m for a message;
211                    --format=json emits a JSON outcome, or JSONL on `list`)
212  stash save -m <msg>  Stash with a message
213  stash list        List stash entries (printed as stash@{N})
214  stash pop [--index] [<n>|stash@{n}]    Apply and remove a stash entry (default
215                    0; --index also restores the staged state, like git)
216  stash apply [--index] [<n>|stash@{n}]  Apply a stash entry, keeping it (default
217                    0; --index also restores the staged state)
218  stash drop [<n>|stash@{n}]   Remove a stash entry without applying
219  stash clear       Remove all stash entries
220  stash show [<n>|stash@{n}]   Show the diff of a stash entry (default 0)
221  clone [--sparse ...] [-b <branch>] [-o <name>] <url>  Clone a repository
222                    (-b checks out <branch> instead of the remote default;
223                    -o names the cloned remote <name> instead of `default`)
224  remote [-v|--verbose] [--format=json]  List remotes (names only; -v adds
225                    `<name>\t<url> (fetch)`/`(push)`; JSON with --format=json)
226  remote add [<name>] <url>  Add a remote (mkit+file://, mkit+https://, mkit+s3://, mkit+ssh://)
227  remote set [<name>] <url>  Alias for 'remote add'
228  remote remove <name>  Remove a named remote (`default` clears the flat remote)
229  remote rename <old> <new>  Rename a named remote
230  remote get-url <name>  Print a remote's URL
231  remote set-url <name> <url>  Change a remote's URL
232  key generate|list|import|export|delete  Manage user-scoped keystore keys
233  grant create|add|list|revoke  Issue, import, list and revoke write and read grants
234                    (create --cap read|read,write|write --grantee <hex> --repo <name>|--all
235                    [--refs <pattern=cufd>] [--audience <origin>] [--ttl 7d] [--store];
236                    a push that moves an existing branch needs the f flag while servers
237                    are opaque: issue such grants as cuf;
238                    add [--remote <remote>|--offline] <file|->; list [--check] [--json];
239                    revoke <remote> [--prune])
240  epoch show|bump <remote>  Show or advance a namespace's grant epoch on a remote
241                    (bump [--by <n>] revokes grants issued at a lower epoch)
242  visibility set <remote> public|private [--statement]
243                    Switch a repository between public and private
244  keygen [--algorithm ed25519|secp256k1|p256] [--force] [--print-pubkey]
245                    Generate a new signing key (defaults to Ed25519)
246  cherry-pick [-n] [-m <parent-number>] [--format=json] <hash> | --continue | --abort
247                    Apply a commit to the current branch (-n stages the change
248                    without committing; -m/--mainline selects the mainline
249                    parent when replaying a merge commit, like git;
250                    --format=json emits a JSON outcome object)
251  revert [-n] [--no-edit] [--format=json] <commit> | --continue | --abort
252                    Create a new commit undoing <commit> (forward commit;
253                    conflict-aware; -n stages the revert without committing;
254                    --format=json emits a JSON outcome object)
255  rebase <branch>    Replay commits onto a different base
256  rebase -i <branch> Interactive: reorder/drop/reword/squash/fixup the todo
257  rebase --continue  Continue rebase after conflict resolution
258  rebase --abort     Abort rebase and restore original state
259                    (--format=json on any form emits a JSON outcome object)
260  bisect start       Begin binary search for a bug
261  bisect good [hash] Mark a commit as good
262  bisect bad [hash]  Mark a commit as bad
263  bisect run <cmd>...  Auto-bisect: run <cmd> per candidate, classify by
264                     exit status (0=good/125=skip/1-127=bad), print first bad
265  bisect reset       End bisect and restore original state
266  gc [-n] [--grace-secs SECS]
267                    Reclaim unreachable objects older than the grace
268                    window (default 14d); -n/--dry-run previews
269  sparse-checkout    Manage sparse checkout patterns
270  serve <path>       Start SSH transport server (internal)
271  mcp [--repository <path>]
272                    Start a Model Context Protocol server on stdio so LLM
273                    agents can drive this repository (status/diff/log/add/
274                    commit/branch + verify/attest); --repository confines
275                    tool calls to that path
276  pack-shard [--out <dir>] [--force] <hash>
277                    Encode a stored pack into Reed-Solomon shards (--out sets
278                    the output dir, default .mkit/pack-shards; --force encodes
279                    below the size threshold) (feature: pack-shards)
280  git export <dest>  Export refs to a git mirror, one-way; --passthrough
281                    publishes an imported repo as a true git fork (feature: git-bridge)
282  git import <url> [<dir>]  Import a git upstream as a signed downstream fork (feature: git-bridge)
283  git fetch|pull     Update refs/remotes/<name>/* and imported tags from the
284                    upstream (locally-moved tags are never clobbered);
285                    pull also fast-forwards the current branch (feature: git-bridge)
286  git verify         Verify bridge state against the local store
287                    (--fork-audit re-derives referenced content) (feature: git-bridge)
288  git status         Show bridge state dirs: direction, endpoints, key, refs (feature: git-bridge)
289  git format-patch <range>  Render native commits as `git am`-able patches (feature: git-bridge)
290  blame [--format=json|--porcelain|--line-porcelain] [-w] [-M[<num>]] [-C[<num>]] [--ignore-rev <rev>] [--ignore-rev-precise] [--first-parent] [--reverse] [-L <start>,<end>] [<rev>] <file>
291                    Show line-level commit attribution; -L limits to a line
292                    range, -w ignores whitespace, -M/-C detect moved/copied
293                    lines (inline -M<num>/-C<num> sets the threshold),
294                    --ignore-rev/--ignore-revs-file skip noise commits,
295                    --ignore-rev-precise refines that fall-through by content
296                    (mkit-only), --first-parent limits the merge-aware walk to
297                    first parents, --reverse <start>..<end> walks history
298                    forward, <rev> blames as of a revision (default HEAD; JSONL
299                    with --format=json, or git-shaped
300                    --porcelain/--line-porcelain)
301  prove <rev> [<path>] [--chunk N | --range OFFSET:LEN] [--with-offsets] [-o FILE] [--format=json]
302                    Build a disclosure bundle proving a path, chunk, or
303                    byte range belongs to a commit (or remix)
304  verify <rev> [--trusted] [--trust-roots <path>]
305                    Verify the signature on a commit, remix, or signed tag;
306                    --trusted (or --trust-roots) also cross-checks the
307                    signer against the trust-roots registry, failing
308                    closed on an unlisted signer
309  verify-proof <commit-id> <bundle|-> [--expect-path PATH] [--trusted] [--trust-roots PATH] [--format=json] [--payload-out FILE]
310                    Verify a disclosure bundle against a trusted 64-hex
311                    commit id (does not resolve refs)
312  closure export <rev> [--history] [-o DIR] [--force] [--format=json]
313                    Export a commit's object-set closure as MANIFEST.mkcl
314                    plus raw-only packs (not `mkit git export`)
315  closure verify <commit-id> [--from DIR] [--history] [--show-unreferenced] [--format=json]
316                    Verify a closure directory against a trusted id, or
317                    check the local store (revision allowed without --from);
318                    local mode streams reachable objects by default;
319                    --show-unreferenced enumerates every local object and
320                    reveals the unreferenced list
321  attest [--commit <hash>] [--algorithm <alg>] [--signer <kind>] [--predicate-type <URI>] [--predicate-file <path>]
322         [--additional-signer \"algorithm=<alg>,signer=<kind>[,path=<p>]\"]... [--external-signer-arg <V>]...
323                    Produce a signed DSSE attestation for a commit
324                    (--external-signer-arg is repeatable; the supplied list
325                    replaces attest.external_signer_args from config)
326  verify-attest [--commit <hash>] [--trust-roots <path>] [--algorithm <filter>] [--format=json]
327                    Verify every attestation attached to a commit (per-signature
328                    verdicts to stderr; --format=json also emits a JSON result)
329  trust add <keyid> <pubkey-hex> [--kind <kind>] [--trust-roots <path>] [--force]
330  trust list [--trust-roots <path>] [--json]
331  trust remove <keyid> [--trust-roots <path>] --yes
332                    Manage the trust-roots registry `verify --trusted` and
333                    `verify-attest` read from
334  self update [--version <tag>] [--check] [--allow-downgrade] [--format human|json]
335                    Update this binary in place from a GitHub Release.
336                    Use OS certificate trust and check the release
337                    checksum when present; ignore custom remote CAs.
338                    Only for installer-managed binaries (curl mkit.sh |
339                    sh); refuses with guidance under brew/cargo.
340                    --check only reports; `latest` never downgrades
341  version           Print version. Also available as the top-level
342                    `--version` / `-V` flags; all emit `mkit <X.Y.Z>`.
343
344global flags (before <command>):
345  -C <path>         Run as if started in <path> (repeatable, like git)
346  -c <key>=<value>  One-shot config override for this invocation (inert /
347                    allowlisted keys only; security-sensitive keys refused)
348  --no-pager|-P     Accepted no-op (mkit never paginates)
349";
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354
355    /// True iff `needle` occurs in `haystack` as a whole token: the
356    /// characters immediately before and after the match (if any) are
357    /// not alphanumeric/hyphen. Plain `.contains()` would let a short
358    /// command name like `"rm"` match inside an unrelated word (e.g.
359    /// "perform"), so this pins word-boundary coverage instead.
360    fn contains_word(haystack: &str, needle: &str) -> bool {
361        fn is_word_char(c: char) -> bool {
362            c.is_ascii_alphanumeric() || c == '-'
363        }
364        haystack.match_indices(needle).any(|(idx, m)| {
365            let before_ok = haystack[..idx]
366                .chars()
367                .next_back()
368                .is_none_or(|c| !is_word_char(c));
369            let after_ok = haystack[idx + m.len()..]
370                .chars()
371                .next()
372                .is_none_or(|c| !is_word_char(c));
373            before_ok && after_ok
374        })
375    }
376
377    #[test]
378    fn help_contains_every_documented_subcommand() {
379        // Every top-level subcommand enumerated in docs/CLI.md — this
380        // doubles as a reminder to refresh HELP_TEXT whenever CLI.md
381        // grows a new command.
382        let required = [
383            "init",
384            "add",
385            "rm",
386            "mv",
387            "restore",
388            "reset",
389            "hash",
390            "cat",
391            "cat-file",
392            "tree",
393            "ls-tree",
394            "ls-files",
395            "rev-parse",
396            "show",
397            "show-ref",
398            "for-each-ref",
399            "symbolic-ref",
400            "update-ref",
401            "ref",
402            "commit",
403            "log",
404            "reflog",
405            "status",
406            "diff",
407            "branch",
408            "checkout",
409            "clean",
410            "tag",
411            "config",
412            "merge",
413            "push",
414            "pull",
415            "fetch",
416            "stash",
417            "clone",
418            "remote",
419            "key",
420            "keygen",
421            "cherry-pick",
422            "rebase",
423            "bisect",
424            "sparse-checkout",
425            "self",
426            "serve",
427            "mcp",
428            "pack-shard",
429            "blame",
430            "prove",
431            "verify",
432            "verify-proof",
433            "closure",
434            "trust",
435            "version",
436            "worktree",
437        ];
438        for cmd in required {
439            assert!(
440                contains_word(HELP_TEXT, cmd),
441                "HELP_TEXT missing documented subcommand: {cmd}"
442            );
443        }
444    }
445}