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 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
110fn 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
131fn 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",
143 }
144}
145
146fn 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(); 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}