1use std::io::Write;
11
12use clap::Parser;
13use mkit_core::hash::Hash;
14use mkit_core::object::{EntryMode, Object};
15use mkit_core::store::ObjectStore;
16
17use super::revspec;
18use crate::clap_shim;
19use crate::exit;
20use crate::format;
21
22#[derive(Debug, Parser)]
23#[command(name = "mkit ls-tree", about = "List the contents of a tree object.")]
24struct LsTreeOpts {
25 #[arg(short = 'r')]
27 recursive: bool,
28 #[arg(short = 'z')]
30 z: bool,
31 args: Vec<String>,
34}
35
36#[must_use]
37pub fn run(args: &[String]) -> u8 {
38 let opts = match clap_shim::parse::<LsTreeOpts>("mkit ls-tree", args) {
39 Ok(o) => o,
40 Err(code) => return code,
41 };
42 let Some((spec, pathspecs)) = opts.args.split_first() else {
43 return super::usage_error("usage: mkit ls-tree [-r] [-z] <tree-ish> [<path>...]");
44 };
45 let cwd = match std::env::current_dir() {
46 Ok(p) => p,
47 Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
48 };
49 let store = match ObjectStore::open(&cwd) {
50 Ok(s) => s,
51 Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
52 };
53 let mkit_dir = cwd.join(mkit_core::MKIT_DIR);
54
55 let tree_hash = match resolve_tree(&store, &mkit_dir, spec) {
56 Ok(h) => h,
57 Err(msg) => return emit_err(&msg, exit::GENERAL_ERROR),
58 };
59
60 let specs: Vec<(String, bool)> = pathspecs.iter().map(|p| normalize(p)).collect();
63 let mut stdout = std::io::stdout().lock();
64 if let Err(msg) = list(
65 &store,
66 &tree_hash,
67 "",
68 opts.recursive,
69 opts.z,
70 &specs,
71 &mut stdout,
72 ) {
73 return emit_err(&msg, exit::GENERAL_ERROR);
74 }
75 exit::OK
76}
77
78fn list(
88 store: &ObjectStore,
89 tree_hash: &Hash,
90 prefix: &str,
91 recursive: bool,
92 z: bool,
93 pathspecs: &[(String, bool)],
94 out: &mut impl Write,
95) -> Result<(), String> {
96 let Object::Tree(tree) = store
97 .read_object(tree_hash)
98 .map_err(|e| format!("read tree: {e}"))?
99 else {
100 return Err(format!("{} is not a tree", format::hex_hash(tree_hash)));
101 };
102 for e in &tree.entries {
103 let Ok(name) = std::str::from_utf8(&e.name) else {
104 return Err("tree entry name is not valid UTF-8".to_string());
105 };
106 let path = if prefix.is_empty() {
107 name.to_string()
108 } else {
109 format!("{prefix}/{name}")
110 };
111 let is_tree = e.mode == EntryMode::Tree;
112
113 if pathspecs.is_empty() {
114 if is_tree && recursive {
115 list(store, &e.object_hash, &path, recursive, z, pathspecs, out)?;
116 } else {
117 emit_entry(e, &path, z, out);
118 }
119 continue;
120 }
121
122 let matched = pathspecs
124 .iter()
125 .any(|(s, _)| super::index_path_matches_or_descends(&path, s));
126 let ancestor = pathspecs
129 .iter()
130 .any(|(s, _)| super::index_path_descends_from(s, &path));
131 let list_contents = pathspecs.iter().any(|(s, slash)| *slash && &path == s);
133
134 if is_tree {
135 if ancestor || list_contents || (matched && recursive) {
136 list(store, &e.object_hash, &path, recursive, z, pathspecs, out)?;
137 } else if matched {
138 emit_entry(e, &path, z, out);
139 }
140 } else if matched {
141 emit_entry(e, &path, z, out);
142 }
143 }
144 Ok(())
145}
146
147fn emit_entry(e: &mkit_core::object::TreeEntry, path: &str, z: bool, out: &mut impl Write) {
150 let (mode, ty) = git_mode_and_type(e.mode);
151 let hash = format::hex_hash(&e.object_hash);
152 if z {
153 let _ = write!(out, "{mode} {ty} {hash}\t{path}\0");
154 } else {
155 let shown = super::c_quote_path(path);
156 let shown = shown.as_deref().unwrap_or(path);
157 let _ = writeln!(out, "{mode} {ty} {hash}\t{shown}");
158 }
159}
160
161fn git_mode_and_type(mode: EntryMode) -> (&'static str, &'static str) {
163 match mode {
164 EntryMode::Blob => ("100644", "blob"),
165 EntryMode::Executable => ("100755", "blob"),
166 EntryMode::Symlink => ("120000", "blob"),
167 EntryMode::Tree => ("040000", "tree"),
168 }
169}
170
171fn resolve_tree(
174 store: &ObjectStore,
175 mkit_dir: &std::path::Path,
176 spec: &str,
177) -> Result<Hash, String> {
178 let h = revspec::resolve_revision(store, mkit_dir, spec)
179 .map_err(|e| format!("bad revision '{spec}': {e}"))?;
180 object_to_tree(store, &h)
181}
182
183fn object_to_tree(store: &ObjectStore, h: &Hash) -> Result<Hash, String> {
184 match store
185 .read_object(h)
186 .map_err(|e| format!("read object: {e}"))?
187 {
188 Object::Commit(c) => Ok(c.tree_hash),
189 Object::Remix(r) => Ok(r.tree_hash),
190 Object::Tree(_) => Ok(*h),
191 Object::Tag(t) => object_to_tree(store, &t.target),
192 _ => Err(format!("{} is not a tree-ish", format::hex_hash(h))),
193 }
194}
195
196fn normalize(spec: &str) -> (String, bool) {
198 let s = spec.replace('\\', "/");
199 let s = s.strip_prefix("./").unwrap_or(&s);
200 let dir_slash = s.ends_with('/');
201 let s = s.strip_suffix('/').unwrap_or(s);
202 (s.to_string(), dir_slash)
203}
204
205fn emit_err(msg: &str, code: u8) -> u8 {
206 let mut stderr = std::io::stderr().lock();
207 let _ = writeln!(stderr, "error: {msg}");
208 code
209}