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 / Scoop contract documented in
7//! `docs/CLI.md`. `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 recorded movement history (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 journaled ref-history MMR. 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  keygen [--algorithm ed25519|secp256k1|p256] [--force] [--print-pubkey]
234                    Generate a new signing key (defaults to Ed25519)
235  cherry-pick [-n] [-m <parent-number>] [--format=json] <hash> | --continue | --abort
236                    Apply a commit to the current branch (-n stages the change
237                    without committing; -m/--mainline selects the mainline
238                    parent when replaying a merge commit, like git;
239                    --format=json emits a JSON outcome object)
240  revert [-n] [--no-edit] [--format=json] <commit> | --continue | --abort
241                    Create a new commit undoing <commit> (forward commit;
242                    conflict-aware; -n stages the revert without committing;
243                    --format=json emits a JSON outcome object)
244  rebase <branch>    Replay commits onto a different base
245  rebase -i <branch> Interactive: reorder/drop/reword/squash/fixup the todo
246  rebase --continue  Continue rebase after conflict resolution
247  rebase --abort     Abort rebase and restore original state
248                    (--format=json on any form emits a JSON outcome object)
249  bisect start       Begin binary search for a bug
250  bisect good [hash] Mark a commit as good
251  bisect bad [hash]  Mark a commit as bad
252  bisect run <cmd>...  Auto-bisect: run <cmd> per candidate, classify by
253                     exit status (0=good/125=skip/1-127=bad), print first bad
254  bisect reset       End bisect and restore original state
255  gc [-n] [--grace-secs SECS]
256                    Reclaim unreachable objects older than the grace
257                    window (default 14d); -n/--dry-run previews
258  sparse-checkout    Manage sparse checkout patterns
259  serve <path>       Start SSH transport server (internal)
260  mcp [--repository <path>]
261                    Start a Model Context Protocol server on stdio so LLM
262                    agents can drive this repository (status/diff/log/add/
263                    commit/branch + verify/attest); --repository confines
264                    tool calls to that path
265  pack-shard [--out <dir>] [--force] <hash>
266                    Encode a stored pack into Reed-Solomon shards (--out sets
267                    the output dir, default .mkit/pack-shards; --force encodes
268                    below the size threshold) (feature: pack-shards)
269  git export <dest>  Export refs to a git mirror, one-way; --passthrough
270                    publishes an imported repo as a true git fork (feature: git-bridge)
271  git import <url> [<dir>]  Import a git upstream as a signed downstream fork (feature: git-bridge)
272  git fetch|pull     Update refs/remotes/<name>/* and imported tags from the
273                    upstream (locally-moved tags are never clobbered);
274                    pull also fast-forwards the current branch (feature: git-bridge)
275  git verify         Verify bridge state against the local store
276                    (--fork-audit re-derives referenced content) (feature: git-bridge)
277  git status         Show bridge state dirs: direction, endpoints, key, refs (feature: git-bridge)
278  git format-patch <range>  Render native commits as `git am`-able patches (feature: git-bridge)
279  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>
280                    Show line-level commit attribution; -L limits to a line
281                    range, -w ignores whitespace, -M/-C detect moved/copied
282                    lines (inline -M<num>/-C<num> sets the threshold),
283                    --ignore-rev/--ignore-revs-file skip noise commits,
284                    --ignore-rev-precise refines that fall-through by content
285                    (mkit-only), --first-parent limits the merge-aware walk to
286                    first parents, --reverse <start>..<end> walks history
287                    forward, <rev> blames as of a revision (default HEAD; JSONL
288                    with --format=json, or git-shaped
289                    --porcelain/--line-porcelain)
290  verify <rev> [--trusted] [--trust-roots <path>]
291                    Verify the signature on a commit, remix, or signed tag;
292                    --trusted (or --trust-roots) also cross-checks the
293                    signer against the trust-roots registry, failing
294                    closed on an unlisted signer
295  attest [--commit <hash>] [--algorithm <alg>] [--signer <kind>] [--predicate-type <URI>] [--predicate-file <path>]
296         [--additional-signer \"algorithm=<alg>,signer=<kind>[,path=<p>]\"]... [--external-signer-arg <V>]...
297                    Produce a signed DSSE attestation for a commit
298                    (--external-signer-arg is repeatable; the supplied list
299                    replaces attest.external_signer_args from config)
300  verify-attest [--commit <hash>] [--trust-roots <path>] [--algorithm <filter>] [--format=json]
301                    Verify every attestation attached to a commit (per-signature
302                    verdicts to stderr; --format=json also emits a JSON result)
303  trust add <keyid> <pubkey-hex> [--kind <kind>] [--trust-roots <path>] [--force]
304  trust list [--trust-roots <path>] [--json]
305  trust remove <keyid> [--trust-roots <path>] --yes
306                    Manage the trust-roots registry `verify --trusted` and
307                    `verify-attest` read from
308  self update [--version <tag>] [--check] [--allow-downgrade] [--format human|json]
309                    Update this binary in place from a signed GitHub
310                    Release, verifying the mkit-native release
311                    attestation against keys embedded at build time.
312                    Only for installer-managed binaries (curl mkit.sh |
313                    sh); refuses with guidance under brew/cargo.
314                    --check only reports; `latest` never downgrades
315  version           Print version. Also available as the top-level
316                    `--version` / `-V` flags; all emit `mkit <X.Y.Z>`.
317
318global flags (before <command>):
319  -C <path>         Run as if started in <path> (repeatable, like git)
320  -c <key>=<value>  One-shot config override for this invocation (inert /
321                    allowlisted keys only; security-sensitive keys refused)
322  --no-pager|-P     Accepted no-op (mkit never paginates)
323";
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328
329    /// True iff `needle` occurs in `haystack` as a whole token: the
330    /// characters immediately before and after the match (if any) are
331    /// not alphanumeric/hyphen. Plain `.contains()` would let a short
332    /// command name like `"rm"` match inside an unrelated word (e.g.
333    /// "perform"), so this pins word-boundary coverage instead.
334    fn contains_word(haystack: &str, needle: &str) -> bool {
335        fn is_word_char(c: char) -> bool {
336            c.is_ascii_alphanumeric() || c == '-'
337        }
338        haystack.match_indices(needle).any(|(idx, m)| {
339            let before_ok = haystack[..idx]
340                .chars()
341                .next_back()
342                .is_none_or(|c| !is_word_char(c));
343            let after_ok = haystack[idx + m.len()..]
344                .chars()
345                .next()
346                .is_none_or(|c| !is_word_char(c));
347            before_ok && after_ok
348        })
349    }
350
351    #[test]
352    fn help_contains_every_documented_subcommand() {
353        // Every top-level subcommand enumerated in docs/CLI.md — this
354        // doubles as a reminder to refresh HELP_TEXT whenever CLI.md
355        // grows a new command.
356        let required = [
357            "init",
358            "add",
359            "rm",
360            "mv",
361            "restore",
362            "reset",
363            "hash",
364            "cat",
365            "cat-file",
366            "tree",
367            "ls-tree",
368            "ls-files",
369            "rev-parse",
370            "show",
371            "show-ref",
372            "for-each-ref",
373            "symbolic-ref",
374            "update-ref",
375            "ref",
376            "commit",
377            "log",
378            "reflog",
379            "status",
380            "diff",
381            "branch",
382            "checkout",
383            "clean",
384            "tag",
385            "config",
386            "merge",
387            "push",
388            "pull",
389            "fetch",
390            "stash",
391            "clone",
392            "remote",
393            "key",
394            "keygen",
395            "cherry-pick",
396            "rebase",
397            "bisect",
398            "sparse-checkout",
399            "self",
400            "serve",
401            "mcp",
402            "pack-shard",
403            "blame",
404            "verify",
405            "trust",
406            "version",
407            "worktree",
408        ];
409        for cmd in required {
410            assert!(
411                contains_word(HELP_TEXT, cmd),
412                "HELP_TEXT missing documented subcommand: {cmd}"
413            );
414        }
415    }
416}