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::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    /// Error if a revision does not resolve.
35    #[arg(long)]
36    verify: bool,
37    /// Abbreviate the id to N chars (default 7). Value must be attached
38    /// (`--short` or `--short=N`) so it does not swallow a following rev.
39    #[arg(long, num_args = 0..=1, require_equals = true, default_missing_value = "7")]
40    short: Option<usize>,
41    /// Print the short symbolic ref name (`HEAD` → the current branch).
42    #[arg(long = "abbrev-ref")]
43    abbrev_ref: bool,
44    /// Print the repository root and exit.
45    #[arg(long = "show-toplevel")]
46    show_toplevel: bool,
47    /// Revisions to resolve.
48    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    // `--show-toplevel` only needs to find the repo root (walking up from a
64    // subdirectory), so handle it before opening the object store.
65    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                // `--verify` or not, a bad revision is an error (matching
100                // git, which refuses rather than echoing the bad token).
101                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
114/// `--abbrev-ref` rendering: `HEAD` → the current branch (or `HEAD` when
115/// detached); any other token is echoed as the already-short name.
116fn 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    // Strip a fully-qualified ref prefix if present; else echo as-is.
125    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
133/// Walk up from `start` to the directory that contains `.mkit`.
134fn 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;