Skip to main content

mkit_cli/commands/
ref_cmd.rs

1//! `mkit ref list [--pattern <glob>]` / `mkit ref cat <name>` — stable
2//! ref-inspection plumbing (#652).
3//!
4//! The epic (#634) settled on keeping refs as plain loose files rather
5//! than migrating them to a denser storage primitive, but that decision
6//! only holds up if "ls-able" is satisfied by a stable command surface
7//! rather than by shelling out to `ls`/`cat` on `.mkit/refs/`. These two
8//! commands are that surface, modeled on git's `for-each-ref`/`show-ref`
9//! plumbing:
10//!
11//! - `ref list` prints every ref's full name and resolved hash, one per
12//!   line as `<refname> <hash>`, sorted lexicographically by name.
13//!   Covers `refs/heads/*`, `refs/tags/*`, and `refs/remotes/*/*` — the
14//!   same read scope as `show-ref`/`for-each-ref`. An empty repo prints
15//!   nothing and still exits 0 (unlike `show-ref`, whose "nothing
16//!   matched" exit-1 convention is a git-inherited existence test, not
17//!   what a structured listing command should do). `--pattern <glob>`
18//!   filters to full ref names matching a shell glob (`*` spans `/`,
19//!   `?`/`[...]` supported — see `super::branch::glob_match`).
20//! - `ref cat <name>` prints the resolved hash for exactly one ref,
21//!   following `HEAD`'s symbolic indirection (`HEAD` is mkit's only
22//!   symbolic ref: it either names a branch or holds a detached hash).
23//!   `<name>` must be a fully-qualified ref name — `refs/heads/<b>`,
24//!   `refs/tags/<t>`, `refs/remotes/<r>/<b>` — or the literal `HEAD`;
25//!   these are exactly the names `ref list` prints, so the two commands
26//!   round-trip.
27//!
28//! Both commands are read-only and reuse `mkit-core::refs`'s existing
29//! read/list helpers (`read_ref`, `read_tag`, `read_remote_ref`,
30//! `resolve_head`, `list_refs`, `list_tags`, `list_remote_refs`,
31//! `list_remote_names`) — no new storage-layer code.
32
33use std::io::Write;
34
35use clap::{Parser, Subcommand};
36use mkit_core::hash::Hash;
37use mkit_core::layout::RepoLayout;
38use mkit_core::refs::{self, RefError};
39
40use crate::clap_shim;
41use crate::exit;
42use crate::format;
43
44#[derive(Debug, Parser)]
45#[command(name = "mkit ref", about = "Inspect refs: list them, or resolve one.")]
46struct RefOpts {
47    #[command(subcommand)]
48    sub: RefCmd,
49}
50
51#[derive(Debug, Subcommand)]
52enum RefCmd {
53    /// List every ref's full name and resolved hash, sorted by name.
54    List {
55        /// Shell-glob filter on the full ref name (`*` spans `/`).
56        #[arg(long)]
57        pattern: Option<String>,
58    },
59    /// Print the resolved hash for one ref, following HEAD's symbolic
60    /// indirection.
61    Cat {
62        /// Fully-qualified ref name (`refs/heads/<b>`, `refs/tags/<t>`,
63        /// `refs/remotes/<r>/<b>`), or the literal `HEAD`.
64        name: String,
65    },
66}
67
68#[must_use]
69pub fn run(args: &[String]) -> u8 {
70    let opts = match clap_shim::parse::<RefOpts>("mkit ref", args) {
71        Ok(o) => o,
72        Err(code) => return code,
73    };
74    let cwd = match std::env::current_dir() {
75        Ok(p) => p,
76        Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
77    };
78    let layout = match super::resolve_layout(&cwd) {
79        Ok(layout) => layout,
80        Err(code) => return code,
81    };
82
83    match opts.sub {
84        RefCmd::List { pattern } => run_list(&layout, pattern.as_deref()),
85        RefCmd::Cat { name } => run_cat(&layout, &name),
86    }
87}
88
89/// One resolved `ref list` row: full ref name and target hash.
90struct Row {
91    name: String,
92    hash: Hash,
93}
94
95fn run_list(layout: &RepoLayout, pattern: Option<&str>) -> u8 {
96    let mut rows: Vec<Row> = Vec::new();
97    match refs::list_refs(layout) {
98        Ok(rs) => push_rows(&mut rows, &rs, "refs/heads/"),
99        Err(e) => return emit_err(&format!("list refs: {e}"), exit::GENERAL_ERROR),
100    }
101    match refs::list_tags(layout) {
102        Ok(rs) => push_rows(&mut rows, &rs, "refs/tags/"),
103        Err(e) => return emit_err(&format!("list tags: {e}"), exit::GENERAL_ERROR),
104    }
105    match refs::list_remote_names(layout) {
106        Ok(remotes) => {
107            for remote in remotes {
108                match refs::list_remote_refs(layout, &remote) {
109                    Ok(rs) => push_rows(&mut rows, &rs, &format!("refs/remotes/{remote}/")),
110                    Err(e) => {
111                        return emit_err(&format!("list remote refs: {e}"), exit::GENERAL_ERROR);
112                    }
113                }
114            }
115        }
116        Err(e) => return emit_err(&format!("list remotes: {e}"), exit::GENERAL_ERROR),
117    }
118    rows.sort_by(|a, b| a.name.cmp(&b.name));
119
120    if let Some(pat) = pattern {
121        rows.retain(|r| super::branch::glob_match(pat, &r.name));
122    }
123
124    let mut stdout = std::io::stdout().lock();
125    for r in &rows {
126        let _ = writeln!(stdout, "{} {}", r.name, format::hex_hash(&r.hash));
127    }
128    exit::OK
129}
130
131/// Push `(<prefix><name>, hash)` for every ref with a readable hash
132/// (mirrors `show_ref::collect` / `for_each_ref::push_rows`).
133fn push_rows(out: &mut Vec<Row>, rs: &[refs::Ref], prefix: &str) {
134    for r in rs {
135        if let Some(h) = r.hash {
136            out.push(Row {
137                name: format!("{prefix}{}", r.name),
138                hash: h,
139            });
140        }
141    }
142}
143
144fn run_cat(layout: &RepoLayout, name: &str) -> u8 {
145    let resolved: Result<Option<Hash>, RefError> = if name == "HEAD" {
146        refs::resolve_head(layout)
147    } else if let Some(short) = name.strip_prefix("refs/heads/") {
148        refs::read_ref(layout, short)
149    } else if let Some(short) = name.strip_prefix("refs/tags/") {
150        refs::read_tag(layout, short)
151    } else if let Some(rest) = name.strip_prefix("refs/remotes/") {
152        match rest.split_once('/') {
153            Some((remote, branch)) => refs::read_remote_ref(layout, remote, branch),
154            None => {
155                return emit_err(
156                    &format!(
157                        "invalid remote ref '{name}': expected refs/remotes/<remote>/<branch>"
158                    ),
159                    exit::USAGE,
160                );
161            }
162        }
163    } else {
164        return emit_err(
165            &format!(
166                "unsupported ref '{name}': ref cat handles HEAD, refs/heads/<b>, refs/tags/<t>, \
167                 and refs/remotes/<r>/<b>"
168            ),
169            exit::USAGE,
170        );
171    };
172
173    match resolved {
174        Ok(Some(h)) => {
175            let mut stdout = std::io::stdout().lock();
176            let _ = writeln!(stdout, "{}", format::hex_hash(&h));
177            exit::OK
178        }
179        Ok(None) => emit_err(&format!("ref '{name}' not found"), exit::GENERAL_ERROR),
180        Err(e) => emit_err(&format!("ref cat {name}: {e}"), exit::GENERAL_ERROR),
181    }
182}
183
184use super::error as emit_err;