mkit_cli/commands/
rev_parse.rs1use std::io::Write;
17use std::path::{Path, PathBuf};
18
19use clap::Parser;
20use mkit_core::refs::{self, Head};
21use mkit_core::store::ObjectStore;
22
23use super::revspec;
24use crate::clap_shim;
25use crate::exit;
26use crate::format;
27
28const DEFAULT_ABBREV: usize = 7;
29
30#[derive(Debug, Parser)]
31#[command(name = "mkit rev-parse", about = "Resolve revisions to object ids.")]
32struct RevParseOpts {
33 #[arg(long)]
35 verify: bool,
36 #[arg(long, num_args = 0..=1, require_equals = true, default_missing_value = "7")]
39 short: Option<usize>,
40 #[arg(long = "abbrev-ref")]
42 abbrev_ref: bool,
43 #[arg(long = "show-toplevel")]
45 show_toplevel: bool,
46 args: Vec<String>,
48}
49
50#[must_use]
51pub fn run(args: &[String]) -> u8 {
52 let opts = match clap_shim::parse::<RevParseOpts>("mkit rev-parse", args) {
53 Ok(o) => o,
54 Err(code) => return code,
55 };
56 let cwd = match std::env::current_dir() {
57 Ok(p) => p,
58 Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
59 };
60 let mut stdout = std::io::stdout().lock();
61
62 if opts.show_toplevel {
65 let Some(root) = find_repo_root(&cwd) else {
66 return emit_err("not inside a mkit repository", exit::GENERAL_ERROR);
67 };
68 let _ = writeln!(stdout, "{}", root.display());
69 return exit::OK;
70 }
71
72 let store = match ObjectStore::open(&cwd) {
73 Ok(s) => s,
74 Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
75 };
76 let mkit_dir = cwd.join(mkit_core::MKIT_DIR);
77
78 if opts.args.is_empty() {
79 return super::usage_error("usage: mkit rev-parse [opts] <rev>...");
80 }
81
82 for spec in &opts.args {
83 if opts.abbrev_ref {
84 match abbrev_ref(&mkit_dir, spec) {
85 Ok(name) => {
86 let _ = writeln!(stdout, "{name}");
87 }
88 Err(code) => return code,
89 }
90 continue;
91 }
92 let hash = match revspec::resolve_revision(&store, &mkit_dir, spec) {
93 Ok(h) => h,
94 Err(e) => {
95 let _ = opts.verify;
98 return emit_err(&format!("bad revision '{spec}': {e}"), exit::GENERAL_ERROR);
99 }
100 };
101 let rendered = match opts.short {
102 Some(n) => format::short_hash(&hash, if n == 0 { DEFAULT_ABBREV } else { n }),
103 None => format::hex_hash(&hash),
104 };
105 let _ = writeln!(stdout, "{rendered}");
106 }
107 exit::OK
108}
109
110fn abbrev_ref(mkit_dir: &Path, spec: &str) -> Result<String, u8> {
113 if spec == "HEAD" {
114 return match refs::read_head(mkit_dir) {
115 Ok(Head::Branch(name)) => Ok(name),
116 Ok(Head::Detached(_)) => Ok("HEAD".to_string()),
117 Err(e) => Err(emit_err(&format!("read HEAD: {e}"), exit::DATAERR)),
118 };
119 }
120 let short = spec
122 .strip_prefix("refs/heads/")
123 .or_else(|| spec.strip_prefix("refs/tags/"))
124 .or_else(|| spec.strip_prefix("refs/remotes/"))
125 .unwrap_or(spec);
126 Ok(short.to_string())
127}
128
129fn find_repo_root(start: &Path) -> Option<PathBuf> {
131 let mut cur = start;
132 loop {
133 if cur.join(mkit_core::MKIT_DIR).is_dir() {
134 return Some(cur.to_path_buf());
135 }
136 cur = cur.parent()?;
137 }
138}
139
140fn emit_err(msg: &str, code: u8) -> u8 {
141 let mut stderr = std::io::stderr().lock();
142 let _ = writeln!(stderr, "error: {msg}");
143 code
144}