1#![doc = include_str!("../README.md")]
2#![deny(unsafe_code)]
22
23mod admission_helper;
24pub mod clap_shim;
25pub mod cli;
26pub mod commands;
27pub mod config;
28pub mod editor;
29pub mod exit;
30mod fanout;
31pub mod format;
32pub mod grants;
33pub mod progress;
34pub mod remote_dispatch;
35mod restore_fanout;
36pub mod signal;
37#[cfg(feature = "sparse-checkout")]
38pub mod sparse_cache;
39pub mod term;
40
41use std::io::Write;
42
43#[must_use]
52#[allow(clippy::too_many_lines)] pub fn dispatch(argv: &[String]) -> u8 {
54 let (cmd_idx, overrides) = match parse_global_flags(argv) {
58 Ok(parsed) => parsed,
59 Err(code) => return code,
60 };
61 config::set_cli_overrides(overrides);
62
63 if cmd_idx >= argv.len() {
64 print_usage_stderr();
65 return exit::USAGE;
66 }
67 let cmd = &argv[cmd_idx];
68 let rest: Vec<String> = argv.iter().skip(cmd_idx + 1).cloned().collect();
69
70 match cmd.as_str() {
71 "-h" | "--help" | "help" => {
72 let mut stdout = std::io::stdout().lock();
73 let _ = stdout.write_all(cli::HELP_TEXT.as_bytes());
74 exit::OK
75 }
76 "version" | "--version" | "-V" => {
77 let mut stdout = std::io::stdout().lock();
78 let _ = writeln!(stdout, "mkit {}", cli::CLI_VERSION);
86 exit::OK
87 }
88 "init" => commands::init::run(&rest),
89 "key" => commands::key::run(&rest),
90 "grant" => commands::grant::run(&rest),
91 "epoch" => commands::epoch::run(&rest),
92 "visibility" => commands::visibility::run(&rest),
93 "keygen" => commands::keygen::run(&rest),
94 "hash" => commands::hash_cmd::run(&rest),
95 "cat" => commands::cat::run(&rest),
96 "cat-file" => commands::cat_file::run(&rest),
97 "ls-tree" => commands::ls_tree::run(&rest),
98 "ls-files" => commands::ls_files::run(&rest),
99 "rev-parse" => commands::rev_parse::run(&rest),
100 "show" => commands::show::run(&rest),
101 "show-ref" => commands::show_ref::run(&rest),
102 "for-each-ref" => commands::for_each_ref::run(&rest),
103 "symbolic-ref" => commands::symbolic_ref::run(&rest),
104 "update-ref" => commands::update_ref::run(&rest),
105 "ref" => commands::ref_cmd::run(&rest),
106 "tree" => commands::tree::run(&rest),
107 "add" => commands::add::run(&rest),
108 "rm" => commands::rm::run(&rest),
109 "mv" => commands::mv::run(&rest),
110 "restore" => commands::restore::run(&rest),
111 "reset" => commands::reset::run(&rest),
112 "status" => commands::status::run(&rest),
113 "commit" => commands::commit::run(&rest),
114 "log" => commands::log::run(&rest),
115 "reflog" => commands::reflog::run(&rest),
116 "branch" => commands::branch::run(&rest),
117 "tag" => commands::tag::run(&rest),
118 "checkout" => commands::checkout::run(&rest),
119 "switch" => commands::switch::run(&rest),
120 "merge-base" => commands::merge_base::run(&rest),
121 "rev-list" => commands::rev_list::run(&rest),
122 "clean" => commands::clean::run(&rest),
123 "diff" => commands::diff::run(&rest),
124 "prove" => commands::prove::run(&rest),
125 "verify" => commands::verify::run(&rest),
126 "verify-proof" => commands::verify_proof::run(&rest),
127 "attest" => commands::attest::run(&rest),
128 "verify-attest" => commands::verify_attest::run(&rest),
129 "closure" => commands::closure::run(&rest),
130 "trust" => commands::trust::run(&rest),
131 "config" => commands::config_cmd::run(&rest),
132 "remote" => commands::remote::run(&rest),
133 "push" => commands::push::run(&rest),
134 "pull" => commands::pull::run(&rest),
135 "fetch" => commands::fetch::run(&rest),
136 "clone" => commands::clone::run(&rest),
137 "mcp" => commands::mcp::run(&rest),
138 "merge" => commands::merge::run(&rest),
139 "cherry-pick" => commands::cherry_pick::run(&rest),
140 "revert" => commands::revert::run(&rest),
141 "rebase" => commands::rebase::run(&rest),
142 "bisect" => commands::bisect::run(&rest),
143 "gc" => commands::gc::run(&rest),
144 "stash" => commands::stash::run(&rest),
145 "worktree" => commands::worktree::run(&rest),
146 "blame" => commands::blame::run(&rest),
147 "self" => commands::self_update::run(&rest),
148 "serve" => commands::serve::run(&rest),
149 #[cfg(feature = "git-bridge")]
150 "git" => commands::git::run(&rest),
151 #[cfg(not(feature = "git-bridge"))]
152 "git" => {
153 let mut stderr = std::io::stderr().lock();
154 let _ = writeln!(
155 stderr,
156 "error: the git bridge is not compiled into this binary; \
157 rebuild with `--features git-bridge` (see docs/specs/SPEC-GIT-BRIDGE.md)"
158 );
159 exit::UNAVAILABLE
160 }
161 "sparse-checkout" => commands::sparse_checkout::run(&rest),
162 #[cfg(feature = "pack-shards")]
163 "pack-shard" => commands::pack_shard::run(&rest),
164 #[cfg(not(feature = "pack-shards"))]
165 "pack-shard" => {
166 let mut stderr = std::io::stderr().lock();
171 let _ = writeln!(
172 stderr,
173 "error: pack-shard is not compiled into this binary; \
174 rebuild with `--features pack-shards`"
175 );
176 exit::UNAVAILABLE
177 }
178 other => {
179 let mut stderr = std::io::stderr().lock();
180 let _ = writeln!(
181 stderr,
182 "error: unknown command '{other}' (run 'mkit --help' for a list of commands)"
183 );
184 exit::USAGE
185 }
186 }
187}
188
189fn parse_global_flags(argv: &[String]) -> Result<(usize, Vec<(String, String)>), u8> {
197 let mut i = 1; let mut overrides: Vec<(String, String)> = Vec::new();
199 while i < argv.len() {
200 let arg = argv[i].as_str();
201 if arg == "-C" {
202 let Some(path) = argv.get(i + 1) else {
203 return Err(global_flag_err("option `-C` requires a path"));
204 };
205 chdir(path)?;
206 i += 2;
207 } else if let Some(path) = arg.strip_prefix("-C").filter(|p| !p.is_empty()) {
208 chdir(path)?;
209 i += 1;
210 } else if arg == "-c" {
211 let Some(kv) = argv.get(i + 1) else {
212 return Err(global_flag_err("option `-c` requires <key>=<value>"));
213 };
214 overrides.push(split_config_override(kv)?);
215 i += 2;
216 } else if let Some(kv) = arg.strip_prefix("-c").filter(|kv| !kv.is_empty()) {
217 overrides.push(split_config_override(kv)?);
218 i += 1;
219 } else if matches!(arg, "--no-pager" | "-P" | "--paginate") {
220 i += 1;
223 } else {
224 break;
225 }
226 }
227 Ok((i, overrides))
228}
229
230fn chdir(path: &str) -> Result<(), u8> {
233 std::env::set_current_dir(path).map_err(|e| {
234 let mut stderr = std::io::stderr().lock();
235 let _ = writeln!(stderr, "error: cannot change to '{path}': {e}");
236 exit::NOINPUT
237 })
238}
239
240fn split_config_override(kv: &str) -> Result<(String, String), u8> {
242 match kv.split_once('=') {
243 Some((k, v)) if !k.is_empty() => Ok((k.to_string(), v.to_string())),
244 _ => Err(global_flag_err(
245 "option `-c` expects <key>=<value> (e.g. -c user.email=ci@example.com)",
246 )),
247 }
248}
249
250fn global_flag_err(msg: &str) -> u8 {
251 let mut stderr = std::io::stderr().lock();
252 let _ = writeln!(stderr, "error: {msg}");
253 exit::USAGE
254}
255
256fn print_usage_stderr() {
257 let mut stderr = std::io::stderr().lock();
258 let _ = stderr.write_all(cli::HELP_TEXT.as_bytes());
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn dispatch_version_returns_ok() {
267 let argv = vec!["mkit".to_string(), "version".to_string()];
269 assert_eq!(dispatch(&argv), exit::OK);
270 }
271
272 #[test]
273 fn dispatch_unknown_command_returns_usage() {
274 let argv = vec!["mkit".to_string(), "definitely-not-a-command".to_string()];
275 assert_eq!(dispatch(&argv), exit::USAGE);
276 }
277
278 #[test]
279 fn dispatch_bare_binary_returns_usage() {
280 let argv = vec!["mkit".to_string()];
281 assert_eq!(dispatch(&argv), exit::USAGE);
282 }
283}