mkit_cli/commands/
for_each_ref.rs1use 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 #[arg(long)]
32 format: Option<String>,
33 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 layout = match super::resolve_layout(&cwd) {
55 Ok(layout) => layout,
56 Err(code) => return code,
57 };
58 let store = match ObjectStore::open(&layout) {
59 Ok(s) => s,
60 Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
61 };
62
63 let mut rows: Vec<RefRow> = Vec::new();
64 let heads = match refs::list_refs(&layout) {
65 Ok(r) => r,
66 Err(e) => return emit_err(&format!("list refs: {e}"), exit::GENERAL_ERROR),
67 };
68 let tags = match refs::list_tags(&layout) {
69 Ok(r) => r,
70 Err(e) => return emit_err(&format!("list tags: {e}"), exit::GENERAL_ERROR),
71 };
72 push_rows(&store, &mut rows, &heads, "refs/heads/");
73 push_rows(&store, &mut rows, &tags, "refs/tags/");
74 match refs::list_remote_names(&layout) {
75 Ok(remotes) => {
76 for remote in remotes {
77 match refs::list_remote_refs(&layout, &remote) {
78 Ok(rs) => {
79 push_rows(&store, &mut rows, &rs, &format!("refs/remotes/{remote}/"));
80 }
81 Err(e) => {
82 return emit_err(&format!("list remote refs: {e}"), exit::GENERAL_ERROR);
83 }
84 }
85 }
86 }
87 Err(e) => return emit_err(&format!("list remotes: {e}"), exit::GENERAL_ERROR),
88 }
89 rows.sort_by(|a, b| a.refname.cmp(&b.refname));
90
91 if !opts.patterns.is_empty() {
92 rows.retain(|r| {
93 opts.patterns
94 .iter()
95 .any(|p| ref_matches_pattern(&r.refname, p))
96 });
97 }
98
99 let mut stdout = std::io::stdout().lock();
100 for r in &rows {
101 let line = match &opts.format {
102 Some(fmt) => match render_format(fmt, r) {
103 Ok(s) => s,
104 Err(msg) => return emit_err(&msg, exit::USAGE),
105 },
106 None => format!("{} {}\t{}", format::hex_hash(&r.hash), r.objtype, r.refname),
107 };
108 let _ = writeln!(stdout, "{line}");
109 }
110 exit::OK
111}
112
113fn ref_matches_pattern(refname: &str, pattern: &str) -> bool {
118 let p = pattern.trim_end_matches('/');
119 refname == p || refname.starts_with(&format!("{p}/"))
120}
121
122fn push_rows(store: &ObjectStore, out: &mut Vec<RefRow>, rs: &[refs::Ref], prefix: &str) {
123 for r in rs {
124 let Some(h) = r.hash else { continue };
125 out.push(RefRow {
126 refname: format!("{prefix}{}", r.name),
127 short: r.name.clone(),
128 hash: h,
129 objtype: object_type_name(store, &h),
130 });
131 }
132}
133
134fn object_type_name(store: &ObjectStore, h: &Hash) -> &'static str {
138 match store.read_object(h) {
139 Ok(Object::Tag(_)) => "tag",
140 Ok(Object::Tree(_)) => "tree",
141 Ok(Object::Blob(_) | Object::ChunkedBlob(_)) => "blob",
142 Ok(Object::Remix(_)) => "remix",
143 _ => "commit",
146 }
147}
148
149fn render_format(fmt: &str, r: &RefRow) -> Result<String, String> {
151 let mut out = String::with_capacity(fmt.len());
152 let mut chars = fmt.chars().peekable();
153 while let Some(c) = chars.next() {
154 if c != '%' {
155 out.push(c);
156 continue;
157 }
158 match chars.peek() {
159 Some('%') => {
160 chars.next();
161 out.push('%');
162 }
163 Some('(') => {
164 chars.next(); let mut atom = String::new();
166 let mut closed = false;
167 for ac in chars.by_ref() {
168 if ac == ')' {
169 closed = true;
170 break;
171 }
172 atom.push(ac);
173 }
174 if !closed {
175 return Err(format!("unterminated format atom in '{fmt}'"));
176 }
177 out.push_str(&atom_value(&atom, r)?);
178 }
179 _ => out.push('%'),
180 }
181 }
182 Ok(out)
183}
184
185fn atom_value(atom: &str, r: &RefRow) -> Result<String, String> {
186 Ok(match atom {
187 "refname" => r.refname.clone(),
188 "refname:short" => r.short.clone(),
189 "objectname" => format::hex_hash(&r.hash),
190 "objectname:short" => format::short_hash(&r.hash, DEFAULT_ABBREV),
191 "objecttype" => r.objtype.to_string(),
192 other => return Err(format!("unsupported format atom: %({other})")),
193 })
194}
195
196use super::error as emit_err;