#![doc = include_str!("../README.md")]
#![deny(unsafe_code)]
pub mod clap_shim;
pub mod cli;
pub mod commands;
pub mod config;
pub mod editor;
pub mod exit;
pub mod format;
pub mod progress;
pub mod remote_dispatch;
pub mod signal;
#[cfg(feature = "sparse-checkout")]
pub mod sparse_cache;
pub mod term;
use std::io::Write;
#[must_use]
#[allow(clippy::too_many_lines)] pub fn dispatch(argv: &[String]) -> u8 {
let (cmd_idx, overrides) = match parse_global_flags(argv) {
Ok(parsed) => parsed,
Err(code) => return code,
};
config::set_cli_overrides(overrides);
if cmd_idx >= argv.len() {
print_usage_stderr();
return exit::USAGE;
}
let cmd = &argv[cmd_idx];
let rest: Vec<String> = argv.iter().skip(cmd_idx + 1).cloned().collect();
match cmd.as_str() {
"-h" | "--help" | "help" => {
let mut stdout = std::io::stdout().lock();
let _ = stdout.write_all(cli::HELP_TEXT.as_bytes());
exit::OK
}
"version" | "--version" | "-V" => {
let mut stdout = std::io::stdout().lock();
let _ = writeln!(stdout, "mkit {}", cli::CLI_VERSION);
exit::OK
}
"init" => commands::init::run(&rest),
"key" => commands::key::run(&rest),
"keygen" => commands::keygen::run(&rest),
"hash" => commands::hash_cmd::run(&rest),
"cat" => commands::cat::run(&rest),
"cat-file" => commands::cat_file::run(&rest),
"ls-tree" => commands::ls_tree::run(&rest),
"ls-files" => commands::ls_files::run(&rest),
"rev-parse" => commands::rev_parse::run(&rest),
"show" => commands::show::run(&rest),
"show-ref" => commands::show_ref::run(&rest),
"for-each-ref" => commands::for_each_ref::run(&rest),
"symbolic-ref" => commands::symbolic_ref::run(&rest),
"update-ref" => commands::update_ref::run(&rest),
"ref" => commands::ref_cmd::run(&rest),
"tree" => commands::tree::run(&rest),
"add" => commands::add::run(&rest),
"rm" => commands::rm::run(&rest),
"mv" => commands::mv::run(&rest),
"restore" => commands::restore::run(&rest),
"reset" => commands::reset::run(&rest),
"status" => commands::status::run(&rest),
"commit" => commands::commit::run(&rest),
"log" => commands::log::run(&rest),
"reflog" => commands::reflog::run(&rest),
"branch" => commands::branch::run(&rest),
"tag" => commands::tag::run(&rest),
"checkout" => commands::checkout::run(&rest),
"switch" => commands::switch::run(&rest),
"merge-base" => commands::merge_base::run(&rest),
"rev-list" => commands::rev_list::run(&rest),
"clean" => commands::clean::run(&rest),
"diff" => commands::diff::run(&rest),
"verify" => commands::verify::run(&rest),
"attest" => commands::attest::run(&rest),
"verify-attest" => commands::verify_attest::run(&rest),
"trust" => commands::trust::run(&rest),
"config" => commands::config_cmd::run(&rest),
"remote" => commands::remote::run(&rest),
"push" => commands::push::run(&rest),
"pull" => commands::pull::run(&rest),
"fetch" => commands::fetch::run(&rest),
"clone" => commands::clone::run(&rest),
"mcp" => commands::mcp::run(&rest),
"merge" => commands::merge::run(&rest),
"cherry-pick" => commands::cherry_pick::run(&rest),
"revert" => commands::revert::run(&rest),
"rebase" => commands::rebase::run(&rest),
"bisect" => commands::bisect::run(&rest),
"gc" => commands::gc::run(&rest),
"stash" => commands::stash::run(&rest),
"worktree" => commands::worktree::run(&rest),
"blame" => commands::blame::run(&rest),
"self" => commands::self_update::run(&rest),
"serve" => commands::serve::run(&rest),
#[cfg(feature = "git-bridge")]
"git" => commands::git::run(&rest),
#[cfg(not(feature = "git-bridge"))]
"git" => {
let mut stderr = std::io::stderr().lock();
let _ = writeln!(
stderr,
"error: the git bridge is not compiled into this binary; \
rebuild with `--features git-bridge` (see docs/specs/SPEC-GIT-BRIDGE.md)"
);
exit::UNAVAILABLE
}
"sparse-checkout" => commands::sparse_checkout::run(&rest),
#[cfg(feature = "pack-shards")]
"pack-shard" => commands::pack_shard::run(&rest),
#[cfg(not(feature = "pack-shards"))]
"pack-shard" => {
let mut stderr = std::io::stderr().lock();
let _ = writeln!(
stderr,
"error: pack-shard is not compiled into this binary; \
rebuild with `--features pack-shards`"
);
exit::UNAVAILABLE
}
other => {
let mut stderr = std::io::stderr().lock();
let _ = writeln!(
stderr,
"error: unknown command '{other}' (run 'mkit --help' for a list of commands)"
);
exit::USAGE
}
}
}
fn parse_global_flags(argv: &[String]) -> Result<(usize, Vec<(String, String)>), u8> {
let mut i = 1; let mut overrides: Vec<(String, String)> = Vec::new();
while i < argv.len() {
let arg = argv[i].as_str();
if arg == "-C" {
let Some(path) = argv.get(i + 1) else {
return Err(global_flag_err("option `-C` requires a path"));
};
chdir(path)?;
i += 2;
} else if let Some(path) = arg.strip_prefix("-C").filter(|p| !p.is_empty()) {
chdir(path)?;
i += 1;
} else if arg == "-c" {
let Some(kv) = argv.get(i + 1) else {
return Err(global_flag_err("option `-c` requires <key>=<value>"));
};
overrides.push(split_config_override(kv)?);
i += 2;
} else if let Some(kv) = arg.strip_prefix("-c").filter(|kv| !kv.is_empty()) {
overrides.push(split_config_override(kv)?);
i += 1;
} else if matches!(arg, "--no-pager" | "-P" | "--paginate") {
i += 1;
} else {
break;
}
}
Ok((i, overrides))
}
fn chdir(path: &str) -> Result<(), u8> {
std::env::set_current_dir(path).map_err(|e| {
let mut stderr = std::io::stderr().lock();
let _ = writeln!(stderr, "error: cannot change to '{path}': {e}");
exit::NOINPUT
})
}
fn split_config_override(kv: &str) -> Result<(String, String), u8> {
match kv.split_once('=') {
Some((k, v)) if !k.is_empty() => Ok((k.to_string(), v.to_string())),
_ => Err(global_flag_err(
"option `-c` expects <key>=<value> (e.g. -c user.email=ci@example.com)",
)),
}
}
fn global_flag_err(msg: &str) -> u8 {
let mut stderr = std::io::stderr().lock();
let _ = writeln!(stderr, "error: {msg}");
exit::USAGE
}
fn print_usage_stderr() {
let mut stderr = std::io::stderr().lock();
let _ = stderr.write_all(cli::HELP_TEXT.as_bytes());
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dispatch_version_returns_ok() {
let argv = vec!["mkit".to_string(), "version".to_string()];
assert_eq!(dispatch(&argv), exit::OK);
}
#[test]
fn dispatch_unknown_command_returns_usage() {
let argv = vec!["mkit".to_string(), "definitely-not-a-command".to_string()];
assert_eq!(dispatch(&argv), exit::USAGE);
}
#[test]
fn dispatch_bare_binary_returns_usage() {
let argv = vec!["mkit".to_string()];
assert_eq!(dispatch(&argv), exit::USAGE);
}
}