Skip to main content

mkit_cli/
lib.rs

1//! `mkit` CLI crate, exposed as a library so integration tests can
2//! drive commands in-process.
3//!
4//! The binary is `src/main.rs`; everything else is a module here so
5//! unit tests and integration tests can link without shelling out.
6//! `mkit-cli` is explicitly `publish = false` — it is monorepo-internal
7//! plumbing, not a stable API.
8
9// `deny` rather than `forbid` so the (currently single) `getpwuid_r`
10// home-dir lookup in `config::home_dir_for_euid` can call libc. That
11// function defeats the `HOME=/` parent-process trick when validating
12// an absolute `signing_key` path: env-derived home would admit every
13// path; passwd-derived home is bound to the same uid the file-mode
14// checks use. All other modules remain effectively `forbid`'d via
15// review; new `unsafe` sites need both an `#[allow]` opt-in and a
16// SAFETY comment on the block.
17#![deny(unsafe_code)]
18
19pub mod clap_shim;
20pub mod cli;
21pub mod commands;
22pub mod config;
23pub mod editor;
24pub mod exit;
25pub mod format;
26pub mod remote_dispatch;
27pub mod signal;
28#[cfg(feature = "sparse-checkout")]
29pub mod sparse_cache;
30pub mod term;
31
32use std::io::Write;
33
34/// Dispatch a single argv invocation. Takes the full argv including
35/// `argv[0]`. Returns the exit code the binary should pass to
36/// `std::process::exit`.
37///
38/// All I/O goes through stdout/stderr so integration tests either
39/// spawn the binary (full end-to-end) or drive this entry point
40/// directly (in-process, faster). We keep this function small and
41/// dispatch-only so the command modules remain easy to snapshot.
42#[must_use]
43pub fn dispatch(argv: &[String]) -> u8 {
44    if argv.len() < 2 {
45        print_usage_stderr();
46        return exit::USAGE;
47    }
48    let cmd = &argv[1];
49    let rest: Vec<String> = argv.iter().skip(2).cloned().collect();
50
51    match cmd.as_str() {
52        "-h" | "--help" | "help" => {
53            let mut stdout = std::io::stdout().lock();
54            let _ = stdout.write_all(cli::HELP_TEXT.as_bytes());
55            exit::OK
56        }
57        "version" | "--version" | "-V" => {
58            let mut stdout = std::io::stdout().lock();
59            // Byte-exact `"mkit <X.Y.Z>\n"` — pinned by the snapshot
60            // test in tests/version_snapshot.rs AND by Homebrew /
61            // Scoop shell asserts. Any refactor that widens this must
62            // update docs/CLI.md and ship a 1.0 major bump. The
63            // top-level `--version`/`-V` flags are aliases of the
64            // `version` subcommand (git-parity, #248) and emit the same
65            // canonical string.
66            let _ = writeln!(stdout, "mkit {}", cli::CLI_VERSION);
67            exit::OK
68        }
69        "init" => commands::init::run(&rest),
70        "key" => commands::key::run(&rest),
71        "keygen" => commands::keygen::run(&rest),
72        "hash" => commands::hash_cmd::run(&rest),
73        "cat" => commands::cat::run(&rest),
74        "cat-file" => commands::cat_file::run(&rest),
75        "ls-tree" => commands::ls_tree::run(&rest),
76        "ls-files" => commands::ls_files::run(&rest),
77        "rev-parse" => commands::rev_parse::run(&rest),
78        "show" => commands::show::run(&rest),
79        "show-ref" => commands::show_ref::run(&rest),
80        "for-each-ref" => commands::for_each_ref::run(&rest),
81        "symbolic-ref" => commands::symbolic_ref::run(&rest),
82        "update-ref" => commands::update_ref::run(&rest),
83        "tree" => commands::tree::run(&rest),
84        "add" => commands::add::run(&rest),
85        "rm" => commands::rm::run(&rest),
86        "mv" => commands::mv::run(&rest),
87        "restore" => commands::restore::run(&rest),
88        "reset" => commands::reset::run(&rest),
89        "status" => commands::status::run(&rest),
90        "commit" => commands::commit::run(&rest),
91        "log" => commands::log::run(&rest),
92        "reflog" => commands::reflog::run(&rest),
93        "branch" => commands::branch::run(&rest),
94        "tag" => commands::tag::run(&rest),
95        "checkout" => commands::checkout::run(&rest),
96        "clean" => commands::clean::run(&rest),
97        "diff" => commands::diff::run(&rest),
98        "verify" => commands::verify::run(&rest),
99        "attest" => commands::attest::run(&rest),
100        "verify-attest" => commands::verify_attest::run(&rest),
101        "config" => commands::config_cmd::run(&rest),
102        "remote" => commands::remote::run(&rest),
103        "push" => commands::push::run(&rest),
104        "pull" => commands::pull::run(&rest),
105        "fetch" => commands::fetch::run(&rest),
106        "clone" => commands::clone::run(&rest),
107        "mcp" => commands::mcp::run(&rest),
108        "merge" => commands::merge::run(&rest),
109        "cherry-pick" => commands::cherry_pick::run(&rest),
110        "revert" => commands::revert::run(&rest),
111        "rebase" => commands::rebase::run(&rest),
112        "bisect" => commands::bisect::run(&rest),
113        "gc" => commands::gc::run(&rest),
114        "stash" => commands::stash::run(&rest),
115        "blame" => commands::blame::run(&rest),
116        "serve" => commands::serve::run(&rest),
117        #[cfg(feature = "git-bridge")]
118        "git" => commands::git::run(&rest),
119        #[cfg(not(feature = "git-bridge"))]
120        "git" => {
121            let mut stderr = std::io::stderr().lock();
122            let _ = writeln!(
123                stderr,
124                "error: the git bridge is not compiled into this binary; \
125                 rebuild with `--features git-bridge` (see docs/SPEC-GIT-BRIDGE.md)"
126            );
127            exit::UNAVAILABLE
128        }
129        "sparse-checkout" => commands::sparse_checkout::run(&rest),
130        #[cfg(feature = "pack-shards")]
131        "pack-shard" => commands::pack_shard::run(&rest),
132        other => {
133            let mut stderr = std::io::stderr().lock();
134            let _ = writeln!(
135                stderr,
136                "error: unknown command '{other}' (run 'mkit --help' for a list of commands)"
137            );
138            exit::USAGE
139        }
140    }
141}
142
143fn print_usage_stderr() {
144    let mut stderr = std::io::stderr().lock();
145    let _ = stderr.write_all(cli::HELP_TEXT.as_bytes());
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn dispatch_version_returns_ok() {
154        // Even without a repo, `version` should succeed.
155        let argv = vec!["mkit".to_string(), "version".to_string()];
156        assert_eq!(dispatch(&argv), exit::OK);
157    }
158
159    #[test]
160    fn dispatch_unknown_command_returns_usage() {
161        let argv = vec!["mkit".to_string(), "definitely-not-a-command".to_string()];
162        assert_eq!(dispatch(&argv), exit::USAGE);
163    }
164
165    #[test]
166    fn dispatch_bare_binary_returns_usage() {
167        let argv = vec!["mkit".to_string()];
168        assert_eq!(dispatch(&argv), exit::USAGE);
169    }
170}