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