Skip to main content

mkit_cli/commands/
rev_parse.rs

1//! `mkit rev-parse [--verify] [--short[=N]] [--abbrev-ref] [--show-toplevel] [<rev>...]`
2//! — resolve revisions to object ids, like `git rev-parse`.
3//!
4//! - bare `<rev>...` — print each resolved 64-hex id, one per line;
5//! - `--short[=N]` — abbreviate to N chars (default 7);
6//! - `--abbrev-ref <ref>` — print the short symbolic name (`HEAD` → the
7//!   current branch);
8//! - `--verify` — error if a revision does not resolve (the default error
9//!   behavior already matches, but the flag is accepted for parity);
10//! - `--show-toplevel` — print the repository root (the dir holding
11//!   `.mkit`).
12//!
13//! Resolution reuses the shared revspec grammar (refs, full/short hashes,
14//! `HEAD~n`/`^`). The abbreviated id is a BLAKE3 prefix, not a SHA-1 one.
15
16use 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    /// Error if a revision does not resolve.
34    #[arg(long)]
35    verify: bool,
36    /// Abbreviate the id to N chars (default 7). Value must be attached
37    /// (`--short` or `--short=N`) so it does not swallow a following rev.
38    #[arg(long, num_args = 0..=1, require_equals = true, default_missing_value = "7")]
39    short: Option<usize>,
40    /// Print the short symbolic ref name (`HEAD` → the current branch).
41    #[arg(long = "abbrev-ref")]
42    abbrev_ref: bool,
43    /// Print the repository root and exit.
44    #[arg(long = "show-toplevel")]
45    show_toplevel: bool,
46    /// Revisions to resolve.
47    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    // `--show-toplevel` only needs to find the repo root (walking up from a
63    // subdirectory), so handle it before opening the object store.
64    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                // `--verify` or not, a bad revision is an error (matching
96                // git, which refuses rather than echoing the bad token).
97                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
110/// `--abbrev-ref` rendering: `HEAD` → the current branch (or `HEAD` when
111/// detached); any other token is echoed as the already-short name.
112fn 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    // Strip a fully-qualified ref prefix if present; else echo as-is.
121    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
129/// Walk up from `start` to the directory that contains `.mkit`.
130fn 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}