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] [<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 [<object>...] Display objects (default HEAD): a commit/remix with its
48                    diff vs the first parent, a tag then its target, a tree
49                    listing, or a blob's contents
50  tree              Snapshot working directory as a tree object
51  ls-tree [-r] [-z] <tree-ish> [<path>...]
52                    List a tree's entries as `<mode> <type> <hash>\t<name>`
53                    (-r recurses; -z NUL-terminates with raw paths)
54  ls-files [-s] [-z] [--others] [--ignored] [--exclude-standard]
55                    List tracked files (-s adds stage info; --others lists
56                    untracked; --exclude-standard drops ignored)
57  rev-parse [--verify] [--short[=N]] [--abbrev-ref] [--show-toplevel] [<rev>...]
58                    Resolve revisions to object ids (--short abbreviates,
59                    --abbrev-ref HEAD prints the branch, --show-toplevel
60                    prints the repo root)
61  show-ref [--heads] [--tags]  List refs as `<hash> <refname>`
62  for-each-ref [--format=<fmt>] [<pattern>...]
63                    Iterate refs, optionally with a %(atom) format string
64  symbolic-ref [--short] <name> [<ref>]
65                    Read a symbolic ref, or (with <ref>) repoint it
66                    (e.g. symbolic-ref HEAD refs/heads/main)
67  update-ref [-d] <ref> [<newvalue> [<oldvalue>]]
68                    Create/update/delete refs/heads/* or refs/tags/*
69                    (<oldvalue> compare-and-swap; all-zero = must be absent,
70                    update mode only; -d's <oldvalue> must be concrete)
71  commit [-a] [--amend] [-m <msg>] Create a signed commit (opens $EDITOR if -m omitted)
72  commit --amend [-m <msg>]  Replace HEAD: re-commit on HEAD's parent, re-sign,
73                    move the branch. Reuses HEAD's message if -m omitted.
74                    The superseded commit becomes unreachable until `gc` ships.
75  log [--oneline] [--abbrev-commit] [--abbrev[=N]] [--format=json] [--graph] [-n N] [<rev> | <A>..<B> | <A>...<B>]
76                    Show commit history (default prints the full message
77                    body + a UTC date; --oneline/--abbrev-commit abbreviate
78                    the commit id, --abbrev[=N] sets the length (default 7);
79                    --format=json emits JSONL with the raw timestamp;
80                    --graph is accepted but currently a no-op). Optional
81                    <rev> starts the walk there; <A>..<B> shows commits in B
82                    not in A; <A>...<B> the symmetric difference (empty
83                    side = HEAD)
84  reflog [<ref>] [--format=json] [-n N]
85                    Show a branch's recorded movement history (read-only).
86                    Lists the branch's first-parent chain (newest first,
87                    addressed <ref>@{N}); defaults to HEAD's branch. With
88                    --features history-mmr, cross-checks each entry against
89                    the journaled ref-history MMR. Not a full Git reflog:
90                    @{N} indexes the reachable chain, so superseded commits
91                    (after amend/reset) are not listed.
92  status [--porcelain[=v1|v2]] [-s|--short] [-z]
93                    Show staged and working tree changes (--porcelain, or
94                    its -s/--short alias, emits machine-readable XY lines;
95                    --porcelain=v2 emits git's richer per-path format with
96                    modes + object ids; special-byte paths are C-style
97                    quoted; -z NUL-terminates records with raw paths)
98  diff [--staged|--cached] [--name-only|--name-status|--stat] [-z] [<rev> [<rev>] | <a>..<b> | <a>...<b>] [<path>...]
99                    Show changes as a unified patch (HEAD vs workdir,
100                    --staged for HEAD vs index, a single revision vs the
101                    worktree, two revisions, an A..B range, or an A...B
102                    symmetric range = merge-base(a,b) vs b; revisions
103                    are refs, commits, or short hashes). --name-only lists
104                    changed paths; --name-status prefixes each with an
105                    A/D/M (T = mode change) letter; --stat shows per-file
106                    change counts + a +/- graph and a summary line; -z
107                    NUL-terminates name-only/-status records with raw paths
108                    (else special-byte paths are C-style quoted)
109  branch [-v|--verbose] [--format=json]
110                    List branches (* marks current; no commit id by
111                    default, like git; -v adds the abbreviated id +
112                    subject; JSONL with --format=json)
113  branch <name>     Create a branch at HEAD
114  branch -d <name>  Delete a branch (safe; refuses the current branch)
115  branch -D <name>  Force-delete a branch (errors on an absent branch,
116                    like git; still refuses the current branch)
117  branch -m [<old>] <new>  Rename a branch (current branch if <old> omitted)
118  checkout <branch> Switch HEAD to a branch and restore files
119  clean [-n] [-f] [-d] [-x|-X] [<path>...]
120                    Remove untracked files (refuses without -f; -n
121                    previews). -d also removes untracked dirs; -x includes
122                    ignored files, -X removes only ignored
123  tag [<name>] [<commit>]  List tags, or create a lightweight tag
124  tag -a <name> [-m <msg>] [<commit>]  Create an annotated tag object
125  tag -s <name> [-m <msg>] [<commit>]  Create a signed (Ed25519) tag object
126  tag -d <name>     Delete a tag
127  config [--format=json]  Show all configuration values (JSON with --format=json)
128  config <key> [--format=json]  Show one value
129  config <key> <value>  Set a configuration value
130  config user.identity <value>  Set author Identity
131                        (ed25519:<hex>, mid:<N>, or raw [kind][len][bytes] hex)
132  config user.name|user.email <value>  Git-compatibility aliases; stored and
133                        round-tripped but NON-authoritative — they never set
134                        the signed author (use user.identity for that)
135  config trusted_remote_endpoint <url>  Trust an HTTP/S3 remote for ambient env credentials
136  config ssh.strict_host_key_checking <yes|no|accept-new>  Override SSH host policy
137  config ssh.user_known_hosts_file <path>  Custom SSH known_hosts file
138  config ssh.identity_file <path>  SSH private key file
139  merge <branch>    Merge a branch into HEAD
140  push [<remote>] [--all] [--force|--force-with-lease] [--dry-run]
141                    Push current branch to its upstream (--all mirrors every branch)
142  pull              Pull changes from remote
143  fetch             Download from remote without merging
144  stash             Stash working dir changes (save WIP)
145  stash save -m <msg>  Stash with a message
146  stash list        List stash entries
147  stash pop [N]     Apply and remove stash entry N (default 0)
148  stash apply [N]   Apply stash entry N without removing it (default 0)
149  stash drop [N]    Remove stash entry N without applying
150  stash clear       Remove all stash entries
151  stash show [N]    Show diff of stash entry N
152  clone [--depth N] [--sparse ...] <url>  Clone a repository
153  remote [--format=json]  Show remote configuration (JSON with --format=json)
154  remote add [<name>] <url>  Add a remote (mkit+file://, mkit+https://, mkit+s3://, mkit+ssh://)
155  remote set [<name>] <url>  Alias for 'remote add'
156  remote remove <name>  Remove a named remote (`default` clears the flat remote)
157  remote rename <old> <new>  Rename a named remote
158  key generate|list|import|export|delete  Manage user-scoped keystore keys
159  keygen [--algorithm ed25519|secp256k1|p256] [--force] [--print-pubkey]
160                    Generate a new signing key (defaults to Ed25519)
161  cherry-pick <hash> Apply a commit to the current branch
162  revert <commit> | --continue | --abort
163                    Create a new commit undoing <commit> (forward commit;
164                    conflict-aware)
165  rebase <branch>    Replay commits onto a different base
166  rebase -i <branch> Interactive: reorder/drop/reword/squash/fixup the todo
167  rebase --continue  Continue rebase after conflict resolution
168  rebase --abort     Abort rebase and restore original state
169  bisect start       Begin binary search for a bug
170  bisect good [hash] Mark a commit as good
171  bisect bad [hash]  Mark a commit as bad
172  bisect reset       End bisect and restore original state
173  gc [-n] [--grace-secs SECS]
174                    Reclaim unreachable objects older than the grace
175                    window (default 14d); -n/--dry-run previews
176  sparse-checkout    Manage sparse checkout patterns
177  serve <path>       Start SSH transport server (internal)
178  mcp [--repository <path>]
179                    Start a Model Context Protocol server on stdio so LLM
180                    agents can drive this repository (status/diff/log/add/
181                    commit/branch + verify/attest); --repository confines
182                    tool calls to that path
183  pack-shard <hash>  Encode a stored pack into Reed-Solomon shards (feature: pack-shards)
184  git export <dest>  Export refs to a git mirror, one-way; --passthrough
185                    publishes an imported repo as a true git fork (feature: git-bridge)
186  git import <url> [<dir>]  Import a git upstream as a signed downstream fork (feature: git-bridge)
187  git fetch|pull     Update refs/remotes/<name>/* and imported tags from the
188                    upstream (locally-moved tags are never clobbered);
189                    pull also fast-forwards the current branch (feature: git-bridge)
190  git verify         Verify bridge state against the local store
191                    (--fork-audit re-derives referenced content) (feature: git-bridge)
192  git status         Show bridge state dirs: direction, endpoints, key, refs (feature: git-bridge)
193  git format-patch <range>  Render native commits as `git am`-able patches (feature: git-bridge)
194  blame [--format=json] <file>
195                    Show line-level commit attribution (JSONL with --format=json)
196  verify <rev>      Verify the signature on a commit, remix, or signed tag
197  attest [--commit <hash>] [--algorithm <alg>] [--signer <kind>] [--predicate-type <URI>] [--predicate-file <path>]
198         [--additional-signer \"algorithm=<alg>,signer=<kind>[,path=<p>]\"]...
199                    Produce a signed DSSE attestation for a commit
200  verify-attest [--commit <hash>] [--trust-roots <path>] [--algorithm <filter>]
201                    Verify every attestation attached to a commit
202  version           Print version. Also available as the top-level
203                    `--version` / `-V` flags; all emit `mkit <X.Y.Z>`.
204";
205
206/// Strip lines beginning with `#` (after any leading whitespace) and
207/// trim surrounding whitespace. Used by `mkit commit` when
208/// `.mkit/COMMIT_EDITMSG` is edited via `$EDITOR`.
209#[must_use]
210pub fn strip_comments_and_trim(input: &str) -> String {
211    let mut out = String::with_capacity(input.len());
212    for line in input.split('\n') {
213        let leading = line.trim_start_matches([' ', '\t']);
214        if leading.starts_with('#') {
215            continue;
216        }
217        out.push_str(line);
218        out.push('\n');
219    }
220    out.trim_matches([' ', '\t', '\r', '\n']).to_owned()
221}
222
223/// Template written into `.mkit/COMMIT_EDITMSG` before spawning
224/// `$EDITOR`. Pinned by snapshot tests.
225pub const COMMIT_EDITMSG_TEMPLATE: &str = "\n# Please enter the commit message for your changes. Lines starting\n# with '#' will be ignored, and an empty message aborts the commit.\n";
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn version_matches_package_version() {
233        assert_eq!(CLI_VERSION, env!("CARGO_PKG_VERSION"));
234    }
235
236    #[test]
237    fn help_contains_every_documented_subcommand() {
238        // Every top-level subcommand enumerated in docs/CLI.md — this
239        // doubles as a reminder to refresh HELP_TEXT whenever CLI.md
240        // grows a new command.
241        let required = [
242            "init",
243            "add",
244            "rm",
245            "mv",
246            "restore",
247            "reset",
248            "hash",
249            "cat",
250            "cat-file",
251            "tree",
252            "ls-tree",
253            "ls-files",
254            "rev-parse",
255            "show",
256            "show-ref",
257            "for-each-ref",
258            "symbolic-ref",
259            "update-ref",
260            "commit",
261            "log",
262            "reflog",
263            "status",
264            "diff",
265            "branch",
266            "checkout",
267            "clean",
268            "tag",
269            "config",
270            "merge",
271            "push",
272            "pull",
273            "fetch",
274            "stash",
275            "clone",
276            "remote",
277            "key",
278            "keygen",
279            "cherry-pick",
280            "rebase",
281            "bisect",
282            "sparse-checkout",
283            "serve",
284            "mcp",
285            "pack-shard",
286            "blame",
287            "verify",
288            "version",
289        ];
290        for cmd in required {
291            assert!(
292                HELP_TEXT.contains(cmd),
293                "HELP_TEXT missing documented subcommand: {cmd}"
294            );
295        }
296    }
297
298    #[test]
299    fn strip_comments_drops_hash_lines_and_trims() {
300        let input = "\nhello\n# a comment\nworld\n   # indented\n\n";
301        assert_eq!(strip_comments_and_trim(input), "hello\nworld");
302    }
303
304    #[test]
305    fn strip_comments_all_comments_is_empty() {
306        assert_eq!(strip_comments_and_trim("# only\n# comments\n"), "");
307    }
308
309    #[test]
310    fn strip_comments_preserves_interior_blank_lines() {
311        assert_eq!(
312            strip_comments_and_trim("title\n\nbody\n# comment\n"),
313            "title\n\nbody"
314        );
315    }
316}