Skip to main content

mkit_cli/commands/
for_each_ref.rs

1//! `mkit for-each-ref [--format=<fmt>] [<pattern>...]` — iterate refs with
2//! an optional format string, like `git for-each-ref`.
3//!
4//! Default line: `<objectname> <objecttype>\t<refname>`. `--format`
5//! substitutes the common atoms `%(refname)`, `%(refname:short)`,
6//! `%(objectname)`, `%(objectname:short)`, `%(objecttype)` (and `%%` for a
7//! literal `%`). The object id is 64-hex BLAKE3. Optional patterns filter
8//! to refs whose full name equals or is under the pattern.
9
10use std::io::Write;
11
12use clap::Parser;
13use mkit_core::hash::Hash;
14use mkit_core::object::Object;
15use mkit_core::refs;
16use mkit_core::store::ObjectStore;
17
18use crate::clap_shim;
19use crate::exit;
20use crate::format;
21
22const DEFAULT_ABBREV: usize = 7;
23
24#[derive(Debug, Parser)]
25#[command(
26    name = "mkit for-each-ref",
27    about = "Iterate refs with an optional format."
28)]
29struct ForEachRefOpts {
30    /// Output format with `%(atom)` substitutions.
31    #[arg(long)]
32    format: Option<String>,
33    /// Optional ref-name patterns (prefix match) to filter the listing.
34    patterns: Vec<String>,
35}
36
37struct RefRow {
38    refname: String,
39    short: String,
40    hash: Hash,
41    objtype: &'static str,
42}
43
44#[must_use]
45pub fn run(args: &[String]) -> u8 {
46    let opts = match clap_shim::parse::<ForEachRefOpts>("mkit for-each-ref", args) {
47        Ok(o) => o,
48        Err(code) => return code,
49    };
50    let cwd = match std::env::current_dir() {
51        Ok(p) => p,
52        Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
53    };
54    let store = match ObjectStore::open(&cwd) {
55        Ok(s) => s,
56        Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
57    };
58    let mkit_dir = cwd.join(mkit_core::MKIT_DIR);
59
60    let mut rows: Vec<RefRow> = Vec::new();
61    let heads = match refs::list_refs(&mkit_dir) {
62        Ok(r) => r,
63        Err(e) => return emit_err(&format!("list refs: {e}"), exit::GENERAL_ERROR),
64    };
65    let tags = match refs::list_tags(&mkit_dir) {
66        Ok(r) => r,
67        Err(e) => return emit_err(&format!("list tags: {e}"), exit::GENERAL_ERROR),
68    };
69    push_rows(&store, &mut rows, &heads, "refs/heads/");
70    push_rows(&store, &mut rows, &tags, "refs/tags/");
71    match refs::list_remote_names(&mkit_dir) {
72        Ok(remotes) => {
73            for remote in remotes {
74                match refs::list_remote_refs(&mkit_dir, &remote) {
75                    Ok(rs) => {
76                        push_rows(&store, &mut rows, &rs, &format!("refs/remotes/{remote}/"));
77                    }
78                    Err(e) => {
79                        return emit_err(&format!("list remote refs: {e}"), exit::GENERAL_ERROR);
80                    }
81                }
82            }
83        }
84        Err(e) => return emit_err(&format!("list remotes: {e}"), exit::GENERAL_ERROR),
85    }
86    rows.sort_by(|a, b| a.refname.cmp(&b.refname));
87
88    if !opts.patterns.is_empty() {
89        rows.retain(|r| {
90            opts.patterns
91                .iter()
92                .any(|p| ref_matches_pattern(&r.refname, p))
93        });
94    }
95
96    let mut stdout = std::io::stdout().lock();
97    for r in &rows {
98        let line = match &opts.format {
99            Some(fmt) => match render_format(fmt, r) {
100                Ok(s) => s,
101                Err(msg) => return emit_err(&msg, exit::USAGE),
102            },
103            None => format!("{} {}\t{}", format::hex_hash(&r.hash), r.objtype, r.refname),
104        };
105        let _ = writeln!(stdout, "{line}");
106    }
107    exit::OK
108}
109
110/// git matches a ref against a literal pattern either completely or up to a
111/// `/` boundary, so `refs/heads` and `refs/heads/` both select every branch
112/// ref. Trim a trailing `/` from the pattern so it doesn't become an empty
113/// (always-failing) `refs/heads//` component.
114fn ref_matches_pattern(refname: &str, pattern: &str) -> bool {
115    let p = pattern.trim_end_matches('/');
116    refname == p || refname.starts_with(&format!("{p}/"))
117}
118
119fn push_rows(store: &ObjectStore, out: &mut Vec<RefRow>, rs: &[refs::Ref], prefix: &str) {
120    for r in rs {
121        let Some(h) = r.hash else { continue };
122        out.push(RefRow {
123            refname: format!("{prefix}{}", r.name),
124            short: r.name.clone(),
125            hash: h,
126            objtype: object_type_name(store, &h),
127        });
128    }
129}
130
131/// git-compatible object type of the ref target (`commit`/`tag`/…; mkit's
132/// `remix` is the one non-git type). Unreadable target → `commit` (the
133/// dominant case) rather than failing the whole listing.
134fn object_type_name(store: &ObjectStore, h: &Hash) -> &'static str {
135    match store.read_object(h) {
136        Ok(Object::Tag(_)) => "tag",
137        Ok(Object::Tree(_)) => "tree",
138        Ok(Object::Blob(_) | Object::ChunkedBlob(_)) => "blob",
139        Ok(Object::Remix(_)) => "remix",
140        // Commit is the dominant case; an unreadable target also defaults
141        // here rather than failing the whole listing.
142        _ => "commit",
143    }
144}
145
146/// Substitute `%(atom)` tokens (and `%%`) in `fmt` for one ref row.
147fn render_format(fmt: &str, r: &RefRow) -> Result<String, String> {
148    let mut out = String::with_capacity(fmt.len());
149    let mut chars = fmt.chars().peekable();
150    while let Some(c) = chars.next() {
151        if c != '%' {
152            out.push(c);
153            continue;
154        }
155        match chars.peek() {
156            Some('%') => {
157                chars.next();
158                out.push('%');
159            }
160            Some('(') => {
161                chars.next(); // consume '('
162                let mut atom = String::new();
163                let mut closed = false;
164                for ac in chars.by_ref() {
165                    if ac == ')' {
166                        closed = true;
167                        break;
168                    }
169                    atom.push(ac);
170                }
171                if !closed {
172                    return Err(format!("unterminated format atom in '{fmt}'"));
173                }
174                out.push_str(&atom_value(&atom, r)?);
175            }
176            _ => out.push('%'),
177        }
178    }
179    Ok(out)
180}
181
182fn atom_value(atom: &str, r: &RefRow) -> Result<String, String> {
183    Ok(match atom {
184        "refname" => r.refname.clone(),
185        "refname:short" => r.short.clone(),
186        "objectname" => format::hex_hash(&r.hash),
187        "objectname:short" => format::short_hash(&r.hash, DEFAULT_ABBREV),
188        "objecttype" => r.objtype.to_string(),
189        other => return Err(format!("unsupported format atom: %({other})")),
190    })
191}
192
193fn emit_err(msg: &str, code: u8) -> u8 {
194    let mut stderr = std::io::stderr().lock();
195    let _ = writeln!(stderr, "error: {msg}");
196    code
197}