mkit-cli 0.4.1

The mkit command-line tool: a content-addressed VCS with native attestation support
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
//! CLI surface constants shared by the main dispatcher and the snapshot
//! tests.
//!
//! `CLI_VERSION` MUST equal `env!("CARGO_PKG_VERSION")` — `build.rs`
//! enforces this at compile time so cosmetic edits to `Cargo.toml` can
//! never desync the Homebrew / Scoop contract documented in
//! `docs/CLI.md`. `mkit version` MUST emit exactly `"mkit <X.Y.Z>\n"`.

/// Version string rendered by `mkit version`. Pinned to the package
/// version at compile time via `env!`.
pub const CLI_VERSION: &str = env!("CARGO_PKG_VERSION");

/// Full help text for `mkit --help` / `mkit help` / `mkit` (with no
/// args). Pinned by snapshot tests so downstream tooling that greps the
/// binary output sees a stable surface.
pub const HELP_TEXT: &str = "\
usage: mkit <command> [args]

commands:
  init              Create a new mkit repository
  add [-A|-u] [-f] <path>...  Stage files for the next commit
  add .             Stage all files under cwd (respects .gitignore/.mkitignore)
  add -A            Stage all changes incl. deletions (no path args)
  add -u            Restage only already-tracked files (no path args)
  add -f <path>     Stage an ignored path (overrides .gitignore/.mkitignore)
  add -p <path>...  Interactively choose hunks to stage (y/n/q/a/d per hunk)
  rm [--cached] [-r] [-f] <path>...  Remove path(s) and stage the deletion
  rm --cached       Stage the removal only; keep the worktree file(s)
  mv [-f] <source>... <dest>  Move/rename tracked path(s) and stage it
                    (into <dest> when it is an existing directory; -f
                    overwrites an existing destination)
  restore [--staged] [--worktree] [--source <rev>] [-f] <path>...
                    Discard worktree changes for path(s) (restore from the
                    index), or --staged to unstage (restore the index entry
                    from HEAD); -f overrides the un-staged-edit guard
  reset [--soft|--mixed|--hard] [-f] [-q] [<commit>]
                    Move HEAD/branch (--soft) or HEAD + reset the index to
                    the commit's tree (--mixed, default); worktree untouched.
                    --hard also resets the worktree (keeps untracked files);
                    refuses to discard dirty/staged content without -f
  hash <file>       Hash a file and store it as a blob
  cat <hash>        Display an object by its hash
  cat-file (-t|-s|-p) <object> | cat-file --batch
                    Show an object's type, size, or content
                    (-p: blob bytes, tree listing, or commit/tag summary;
                    --batch reads object names from stdin, takes no <object>)
  show [--stat] [<object>...] Display objects (default HEAD): a commit/remix
                    with its diff vs the first parent, a tag then its target, a
                    tree listing, or a blob's contents (--stat shows a diffstat
                    instead of the patch for commits/remixes)
  tree              Snapshot working directory as a tree object
  ls-tree [-r] [-z] <tree-ish> [<path>...]
                    List a tree's entries as `<mode> <type> <hash>\t<name>`
                    (-r recurses; -z NUL-terminates with raw paths)
  ls-files [-s] [-z] [--others] [--ignored] [--exclude-standard]
                    List tracked files (-s adds stage info; --others lists
                    untracked; --exclude-standard drops ignored)
  rev-parse [--verify] [--short[=N]] [--abbrev-ref] [--show-toplevel] [<rev>...]
                    Resolve revisions to object ids (--short abbreviates,
                    --abbrev-ref HEAD prints the branch, --show-toplevel
                    prints the repo root; --verify is accepted for git-script
                    compatibility — mkit always errors on a bad revision)
  merge-base [--is-ancestor] <a> <b>  Print the common ancestor of two
                    commits (--is-ancestor tests ancestry: exit 0/1, no output)
  rev-list [--count] <rev>  List commit ids reachable from <rev> (--count
                    prints the number)
  show-ref [--heads] [--tags]  List refs as `<hash> <refname>`
  for-each-ref [--format=<fmt>] [<pattern>...]
                    Iterate refs, optionally with a %(atom) format string
  symbolic-ref [--short] <name> [<ref>]
                    Read a symbolic ref, or (with <ref>) repoint it
                    (e.g. symbolic-ref HEAD refs/heads/main)
  update-ref [-d] <ref> [<newvalue> [<oldvalue>]]
                    Create/update/delete refs/heads/* or refs/tags/*
                    (<oldvalue> compare-and-swap; all-zero = must be absent,
                    update mode only; -d's <oldvalue> must be concrete)
  ref list [--pattern <glob>]  List every ref's full name + resolved hash
                    (`<refname> <hash>`, sorted; covers refs/heads/*,
                    refs/tags/*, refs/remotes/*/*; --pattern is a shell glob
                    on the full name, `*` spans `/`)
  ref cat <name>    Print the resolved hash for one ref (HEAD,
                    refs/heads/<b>, refs/tags/<t>, or refs/remotes/<r>/<b>),
                    following HEAD's symbolic indirection
  commit [-a] [-q] [--amend] [-m <msg> | -F <file>] [--author <spec>] [--format=json]
                    Create a signed commit (opens $EDITOR if -m/-F omitted).
                    -F reads the message from <file> (`-` = stdin); --author
                    overrides the commit author. After `merge --no-commit`
                    (or a resolved merge) this records a two-parent merge commit.
                    -q suppresses the summary; -S/--gpg-sign, --no-verify,
                    --no-edit are accepted no-ops (mkit always signs, no hooks);
                    --format=json emits a JSON result object on success
  commit --amend [-m <msg>]  Replace HEAD: re-commit on HEAD's parent, re-sign,
                    move the branch. Reuses HEAD's message if -m omitted.
                    The superseded commit becomes unreachable until `gc` ships.
  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>]
                    Show commit history (default prints the full message
                    body + a UTC date; --oneline/--abbrev-commit abbreviate
                    the commit id, --abbrev[=N] sets the length (default 7);
                    --format=json emits JSONL with the raw timestamp;
                    --graph is accepted but currently a no-op). --author/
                    --grep filter by substring match on the author identity
                    / commit message; --since/--until bound the commit
                    timestamp (@<unix>, now/today/yesterday, N-unit-ago,
                    or YYYY-MM-DD[ HH:MM:SS]); --no-merges hides
                    merge commits from the output; --first-parent walks
                    only first parents, never entering a merged side
                    branch. Filters apply before -n's limit. Optional
                    <rev> starts the walk there; <A>..<B> shows commits in B
                    not in A; <A>...<B> the symmetric difference (empty
                    side = HEAD)
  reflog [<ref>] [--format=json] [-n N]
                    Show a branch's recorded movement history (read-only).
                    Lists the branch's first-parent chain (newest first,
                    addressed <ref>@{N}); defaults to HEAD's branch. With
                    --features history-mmr, cross-checks each entry against
                    the journaled ref-history MMR. Not a full Git reflog:
                    @{N} indexes the reachable chain, so superseded commits
                    (after amend/reset) are not listed.
  status [--porcelain[=v1|v2]] [-s|--short] [-z]
                    Show staged and working tree changes (--porcelain, or
                    its -s/--short alias, emits machine-readable XY lines;
                    --porcelain=v2 emits git's richer per-path format with
                    modes + object ids; special-byte paths are C-style
                    quoted; -z NUL-terminates records with raw paths)
  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>...]
                    Show changes as a unified patch (HEAD vs workdir,
                    --staged for HEAD vs index, a single revision vs the
                    worktree, two revisions, an A..B range, or an A...B
                    symmetric range = merge-base(a,b) vs b; --merge-base
                    diffs the merge base of the given revisions (the flag
                    spelling of A...B); revisions
                    are refs, commits, or short hashes). --name-only lists
                    changed paths; --name-status prefixes each with an
                    A/D/M (T = mode change) letter; --stat shows per-file
                    change counts + a +/- graph and a summary line; -z
                    NUL-terminates name-only/-status records with raw paths
                    (else special-byte paths are C-style quoted); -w/
                    --ignore-all-space and -b/--ignore-space-change control
                    whitespace sensitivity (-w wins if both given); -U<n>/
                    --unified=<n> sets the context-line count (default 3)
  branch [-v|--verbose] [--format=json]
                    List branches (* marks current; no commit id by
                    default, like git; -v adds the abbreviated id +
                    subject; JSONL with --format=json)
  branch <name>     Create a branch at HEAD
  branch -d <name>  Delete a branch (safe; refuses the current branch)
  branch -D <name>  Force-delete a branch (errors on an absent branch,
                    like git; still refuses the current branch)
  branch -m [<old>] <new>  Rename a branch (current branch if <old> omitted)
  branch --show-current  Print the current branch name
  branch [--list] [--contains [<c>]] [--no-contains [<c>]] [--merged [<c>]] [--no-merged [<c>]] [<pattern>...]
                    Filter the listing (like git): <pattern> are shell globs on
                    branch names (enabled by --list or any filter; `*`/`?`/`[…]`,
                    `*` spans `/`); --contains keeps branches whose tip has <c>
                    as an ancestor; --merged keeps those merged into <c>; the
                    --no-* forms invert. All four commit args default to HEAD
                    when omitted
  checkout <branch> Switch HEAD to a branch and restore files
  checkout -b|-B <new> [<start>]  Create (or -B reset) a branch and switch to it
  switch <branch>   Switch branches (git switch)
  switch -c|-C <new> [<start>]    Create (or -C reset) a branch and switch to it
  clean [-n] [-f] [-d] [-x|-X] [<path>...]
                    Remove untracked files (refuses without -f; -n
                    previews). -d also removes untracked dirs; -x includes
                    ignored files, -X removes only ignored
  tag [<name>] [<commit>] [--format=json]  List tags, or create a lightweight tag
  tag -l [<pattern>]  List tags, optionally filtered by a shell glob
                    (JSONL with --format=json)
  tag -a <name> [-m <msg>] [--author <spec>] [<commit>]  Create an annotated tag
  tag -s <name> [-m <msg>] [--author <spec>] [<commit>]  Create a signed tag
  tag -d <name>     Delete a tag
  config [--format=json]  Show all configuration values (JSON with --format=json)
  config <key> [--format=json]  Show one value
  config <key> <value>  Set a configuration value
  config --unset <key>  Remove a configuration value (idempotent; unknown key errors)
  config [--local|--global] <key> <value>|--unset <key>  Force repo/user scope
  config user.identity <value>  Set author Identity
                        (ed25519:<hex>, mid:<N>, or raw [kind][len][bytes] hex)
  config user.name|user.email <value>  Git-compatibility aliases; stored and
                        round-tripped but NON-authoritative — they never set
                        the signed author (use user.identity for that)
  config trusted_remote_endpoint <url>  Trust an HTTP/S3 remote for ambient env credentials
  config ssh.strict_host_key_checking <yes|no|accept-new>  Override SSH host policy
  config ssh.user_known_hosts_file <path>  Custom SSH known_hosts file
  config ssh.identity_file <path>  SSH private key file
  merge [--no-commit] [-m <msg>] [--format=json] <branch> | --continue | --abort
                    Merge a branch into HEAD (--no-commit stages the merge and
                    stops before committing; finish with `mkit commit` or
                    `mkit merge --continue`; -m overrides the merge message;
                    --format=json emits a JSON outcome object)
  push [<remote>] [-u|--set-upstream] [--all] [-f|--force|--force-with-lease] [--dry-run] [--format=json]
                    Push current branch to its upstream (--all mirrors every
                    branch; -u records the upstream; prints git's `To <url>` +
                    ref-update summary, or `Everything up-to-date`;
                    --format=json emits a JSON outcome object)
  pull [<remote>] [--all] [--format=json]  Pull changes from remote (fast-forward;
                    prints `Updating <a>..<b>`/`Fast-forward`/diffstat, or
                    `Already up to date.`); --all pulls every configured remote;
                    --format=json emits a JSON outcome object
  fetch [<remote>] [--all] [--format=json]  Download from remote without merging
                    (prints `From <url>` + per-ref summary; silent when nothing
                    changed); --all fetches every configured remote;
                    --format=json emits a JSON outcome object
  worktree add <path> [<commit-ish>]  Create a linked working tree (shares the
                    object store and refs; own HEAD/index/stash). With no
                    commit-ish, creates a branch named after the path
  worktree list [--porcelain]  List the main and every linked working tree
  worktree remove [--force] <path>     Remove a linked working tree
  worktree prune [--dry-run]   Delete registry entries whose tree is gone
  stash [--format=json]  Stash working dir changes (save WIP; -m for a message;
                    --format=json emits a JSON outcome, or JSONL on `list`)
  stash save -m <msg>  Stash with a message
  stash list        List stash entries (printed as stash@{N})
  stash pop [--index] [<n>|stash@{n}]    Apply and remove a stash entry (default
                    0; --index also restores the staged state, like git)
  stash apply [--index] [<n>|stash@{n}]  Apply a stash entry, keeping it (default
                    0; --index also restores the staged state)
  stash drop [<n>|stash@{n}]   Remove a stash entry without applying
  stash clear       Remove all stash entries
  stash show [<n>|stash@{n}]   Show the diff of a stash entry (default 0)
  clone [--sparse ...] [-b <branch>] [-o <name>] <url>  Clone a repository
                    (-b checks out <branch> instead of the remote default;
                    -o names the cloned remote <name> instead of `default`)
  remote [-v|--verbose] [--format=json]  List remotes (names only; -v adds
                    `<name>\t<url> (fetch)`/`(push)`; JSON with --format=json)
  remote add [<name>] <url>  Add a remote (mkit+file://, mkit+https://, mkit+s3://, mkit+ssh://)
  remote set [<name>] <url>  Alias for 'remote add'
  remote remove <name>  Remove a named remote (`default` clears the flat remote)
  remote rename <old> <new>  Rename a named remote
  remote get-url <name>  Print a remote's URL
  remote set-url <name> <url>  Change a remote's URL
  key generate|list|import|export|delete  Manage user-scoped keystore keys
  keygen [--algorithm ed25519|secp256k1|p256] [--force] [--print-pubkey]
                    Generate a new signing key (defaults to Ed25519)
  cherry-pick [-n] [-m <parent-number>] [--format=json] <hash> | --continue | --abort
                    Apply a commit to the current branch (-n stages the change
                    without committing; -m/--mainline selects the mainline
                    parent when replaying a merge commit, like git;
                    --format=json emits a JSON outcome object)
  revert [-n] [--no-edit] [--format=json] <commit> | --continue | --abort
                    Create a new commit undoing <commit> (forward commit;
                    conflict-aware; -n stages the revert without committing;
                    --format=json emits a JSON outcome object)
  rebase <branch>    Replay commits onto a different base
  rebase -i <branch> Interactive: reorder/drop/reword/squash/fixup the todo
  rebase --continue  Continue rebase after conflict resolution
  rebase --abort     Abort rebase and restore original state
                    (--format=json on any form emits a JSON outcome object)
  bisect start       Begin binary search for a bug
  bisect good [hash] Mark a commit as good
  bisect bad [hash]  Mark a commit as bad
  bisect run <cmd>...  Auto-bisect: run <cmd> per candidate, classify by
                     exit status (0=good/125=skip/1-127=bad), print first bad
  bisect reset       End bisect and restore original state
  gc [-n] [--grace-secs SECS]
                    Reclaim unreachable objects older than the grace
                    window (default 14d); -n/--dry-run previews
  sparse-checkout    Manage sparse checkout patterns
  serve <path>       Start SSH transport server (internal)
  mcp [--repository <path>]
                    Start a Model Context Protocol server on stdio so LLM
                    agents can drive this repository (status/diff/log/add/
                    commit/branch + verify/attest); --repository confines
                    tool calls to that path
  pack-shard [--out <dir>] [--force] <hash>
                    Encode a stored pack into Reed-Solomon shards (--out sets
                    the output dir, default .mkit/pack-shards; --force encodes
                    below the size threshold) (feature: pack-shards)
  git export <dest>  Export refs to a git mirror, one-way; --passthrough
                    publishes an imported repo as a true git fork (feature: git-bridge)
  git import <url> [<dir>]  Import a git upstream as a signed downstream fork (feature: git-bridge)
  git fetch|pull     Update refs/remotes/<name>/* and imported tags from the
                    upstream (locally-moved tags are never clobbered);
                    pull also fast-forwards the current branch (feature: git-bridge)
  git verify         Verify bridge state against the local store
                    (--fork-audit re-derives referenced content) (feature: git-bridge)
  git status         Show bridge state dirs: direction, endpoints, key, refs (feature: git-bridge)
  git format-patch <range>  Render native commits as `git am`-able patches (feature: git-bridge)
  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>
                    Show line-level commit attribution; -L limits to a line
                    range, -w ignores whitespace, -M/-C detect moved/copied
                    lines (inline -M<num>/-C<num> sets the threshold),
                    --ignore-rev/--ignore-revs-file skip noise commits,
                    --ignore-rev-precise refines that fall-through by content
                    (mkit-only), --first-parent limits the merge-aware walk to
                    first parents, --reverse <start>..<end> walks history
                    forward, <rev> blames as of a revision (default HEAD; JSONL
                    with --format=json, or git-shaped
                    --porcelain/--line-porcelain)
  verify <rev> [--trusted] [--trust-roots <path>]
                    Verify the signature on a commit, remix, or signed tag;
                    --trusted (or --trust-roots) also cross-checks the
                    signer against the trust-roots registry, failing
                    closed on an unlisted signer
  attest [--commit <hash>] [--algorithm <alg>] [--signer <kind>] [--predicate-type <URI>] [--predicate-file <path>]
         [--additional-signer \"algorithm=<alg>,signer=<kind>[,path=<p>]\"]... [--external-signer-arg <V>]...
                    Produce a signed DSSE attestation for a commit
                    (--external-signer-arg is repeatable; the supplied list
                    replaces attest.external_signer_args from config)
  verify-attest [--commit <hash>] [--trust-roots <path>] [--algorithm <filter>] [--format=json]
                    Verify every attestation attached to a commit (per-signature
                    verdicts to stderr; --format=json also emits a JSON result)
  trust add <keyid> <pubkey-hex> [--kind <kind>] [--trust-roots <path>] [--force]
  trust list [--trust-roots <path>] [--json]
  trust remove <keyid> [--trust-roots <path>] --yes
                    Manage the trust-roots registry `verify --trusted` and
                    `verify-attest` read from
  self update [--version <tag>] [--check] [--allow-downgrade] [--format human|json]
                    Update this binary in place from a signed GitHub
                    Release, verifying the mkit-native release
                    attestation against keys embedded at build time.
                    Only for installer-managed binaries (curl mkit.sh |
                    sh); refuses with guidance under brew/cargo.
                    --check only reports; `latest` never downgrades
  version           Print version. Also available as the top-level
                    `--version` / `-V` flags; all emit `mkit <X.Y.Z>`.

global flags (before <command>):
  -C <path>         Run as if started in <path> (repeatable, like git)
  -c <key>=<value>  One-shot config override for this invocation (inert /
                    allowlisted keys only; security-sensitive keys refused)
  --no-pager|-P     Accepted no-op (mkit never paginates)
";

#[cfg(test)]
mod tests {
    use super::*;

    /// True iff `needle` occurs in `haystack` as a whole token: the
    /// characters immediately before and after the match (if any) are
    /// not alphanumeric/hyphen. Plain `.contains()` would let a short
    /// command name like `"rm"` match inside an unrelated word (e.g.
    /// "perform"), so this pins word-boundary coverage instead.
    fn contains_word(haystack: &str, needle: &str) -> bool {
        fn is_word_char(c: char) -> bool {
            c.is_ascii_alphanumeric() || c == '-'
        }
        haystack.match_indices(needle).any(|(idx, m)| {
            let before_ok = haystack[..idx]
                .chars()
                .next_back()
                .is_none_or(|c| !is_word_char(c));
            let after_ok = haystack[idx + m.len()..]
                .chars()
                .next()
                .is_none_or(|c| !is_word_char(c));
            before_ok && after_ok
        })
    }

    #[test]
    fn help_contains_every_documented_subcommand() {
        // Every top-level subcommand enumerated in docs/CLI.md — this
        // doubles as a reminder to refresh HELP_TEXT whenever CLI.md
        // grows a new command.
        let required = [
            "init",
            "add",
            "rm",
            "mv",
            "restore",
            "reset",
            "hash",
            "cat",
            "cat-file",
            "tree",
            "ls-tree",
            "ls-files",
            "rev-parse",
            "show",
            "show-ref",
            "for-each-ref",
            "symbolic-ref",
            "update-ref",
            "ref",
            "commit",
            "log",
            "reflog",
            "status",
            "diff",
            "branch",
            "checkout",
            "clean",
            "tag",
            "config",
            "merge",
            "push",
            "pull",
            "fetch",
            "stash",
            "clone",
            "remote",
            "key",
            "keygen",
            "cherry-pick",
            "rebase",
            "bisect",
            "sparse-checkout",
            "self",
            "serve",
            "mcp",
            "pack-shard",
            "blame",
            "verify",
            "trust",
            "version",
            "worktree",
        ];
        for cmd in required {
            assert!(
                contains_word(HELP_TEXT, cmd),
                "HELP_TEXT missing documented subcommand: {cmd}"
            );
        }
    }
}