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 layout = match super::resolve_layout(&cwd) {
50 Ok(layout) => layout,
51 Err(code) => return code,
52 };
53 let store = match ObjectStore::open(&layout) {
54 Ok(s) => s,
55 Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
56 };
57
58 let tree_hash = match resolve_tree(&store, &layout, spec) {
59 Ok(h) => h,
60 Err(msg) => return emit_err(&msg, exit::GENERAL_ERROR),
61 };
62
63 let specs: Vec<(String, bool)> = pathspecs.iter().map(|p| normalize(p)).collect();
66 let mut stdout = std::io::stdout().lock();
67 if let Err(msg) = list(
68 &store,
69 &tree_hash,
70 "",
71 opts.recursive,
72 opts.z,
73 &specs,
74 &mut stdout,
75 ) {
76 return emit_err(&msg, exit::GENERAL_ERROR);
77 }
78 exit::OK
79}
80
81fn list(
91 store: &ObjectStore,
92 tree_hash: &Hash,
93 prefix: &str,
94 recursive: bool,
95 z: bool,
96 pathspecs: &[(String, bool)],
97 out: &mut impl Write,
98) -> Result<(), String> {
99 let Object::Tree(tree) = store
100 .read_object(tree_hash)
101 .map_err(|e| format!("read tree: {e}"))?
102 else {
103 return Err(format!("{} is not a tree", format::hex_hash(tree_hash)));
104 };
105 for e in &tree.entries {
106 let Ok(name) = std::str::from_utf8(&e.name) else {
107 return Err("tree entry name is not valid UTF-8".to_string());
108 };
109 let path = if prefix.is_empty() {
110 name.to_string()
111 } else {
112 format!("{prefix}/{name}")
113 };
114 let is_tree = e.mode == EntryMode::Tree;
115
116 if pathspecs.is_empty() {
117 if is_tree && recursive {
118 list(store, &e.object_hash, &path, recursive, z, pathspecs, out)?;
119 } else {
120 emit_entry(e, &path, z, out);
121 }
122 continue;
123 }
124
125 let matched = pathspecs
127 .iter()
128 .any(|(s, _)| super::index_path_matches_or_descends(&path, s));
129 let ancestor = pathspecs
132 .iter()
133 .any(|(s, _)| super::index_path_descends_from(s, &path));
134 let list_contents = pathspecs.iter().any(|(s, slash)| *slash && &path == s);
136
137 if is_tree {
138 if ancestor || list_contents || (matched && recursive) {
139 list(store, &e.object_hash, &path, recursive, z, pathspecs, out)?;
140 } else if matched {
141 emit_entry(e, &path, z, out);
142 }
143 } else if matched {
144 emit_entry(e, &path, z, out);
145 }
146 }
147 Ok(())
148}
149
150fn emit_entry(e: &mkit_core::object::TreeEntry, path: &str, z: bool, out: &mut impl Write) {
153 let (mode, ty) = git_mode_and_type(e.mode);
154 let hash = format::hex_hash(&e.object_hash);
155 if z {
156 let _ = write!(out, "{mode} {ty} {hash}\t{path}\0");
157 } else {
158 let shown = super::c_quote_path(path);
159 let shown = shown.as_deref().unwrap_or(path);
160 let _ = writeln!(out, "{mode} {ty} {hash}\t{shown}");
161 }
162}
163
164fn git_mode_and_type(mode: EntryMode) -> (&'static str, &'static str) {
166 match mode {
167 EntryMode::Blob => ("100644", "blob"),
168 EntryMode::Executable => ("100755", "blob"),
169 EntryMode::Symlink => ("120000", "blob"),
170 EntryMode::Tree => ("040000", "tree"),
171 }
172}
173
174fn resolve_tree(
177 store: &ObjectStore,
178 layout: &mkit_core::layout::RepoLayout,
179 spec: &str,
180) -> Result<Hash, String> {
181 let h = revspec::resolve_revision(store, layout, spec)
182 .map_err(|e| format!("bad revision '{spec}': {e}"))?;
183 object_to_tree(store, &h)
184}
185
186fn object_to_tree(store: &ObjectStore, h: &Hash) -> Result<Hash, String> {
187 match store
188 .read_object(h)
189 .map_err(|e| format!("read object: {e}"))?
190 {
191 Object::Commit(c) => Ok(c.tree_hash),
192 Object::Remix(r) => Ok(r.tree_hash),
193 Object::Tree(_) => Ok(*h),
194 Object::Tag(t) => object_to_tree(store, &t.target),
195 _ => Err(format!("{} is not a tree-ish", format::hex_hash(h))),
196 }
197}
198
199fn normalize(spec: &str) -> (String, bool) {
201 let s = spec.replace('\\', "/");
202 let s = s.strip_prefix("./").unwrap_or(&s);
203 let dir_slash = s.ends_with('/');
204 let s = s.strip_suffix('/').unwrap_or(s);
205 (s.to_string(), dir_slash)
206}
207
208use super::error as emit_err;