1use std::io::Write;
20
21use clap::Parser;
22use mkit_core::object::{EntryMode, Object};
23use mkit_core::store::ObjectStore;
24use mkit_core::worktree;
25
26use super::revspec;
27use crate::clap_shim;
28use crate::exit;
29use crate::format;
30
31#[derive(Debug, Parser)]
32#[command(name = "mkit cat-file", about = "Inspect a stored object.")]
33#[allow(clippy::struct_excessive_bools)] struct CatFileOpts {
35 #[arg(short = 't', conflicts_with_all = ["size", "pretty", "batch"])]
37 type_: bool,
38 #[arg(short = 's', conflicts_with_all = ["pretty", "batch"])]
40 size: bool,
41 #[arg(short = 'p', conflicts_with = "batch")]
43 pretty: bool,
44 #[arg(long)]
48 batch: bool,
49 object: Option<String>,
51}
52
53#[must_use]
54pub fn run(args: &[String]) -> u8 {
55 let opts = match clap_shim::parse::<CatFileOpts>("mkit cat-file", args) {
56 Ok(o) => o,
57 Err(code) => return code,
58 };
59 let cwd = match std::env::current_dir() {
60 Ok(p) => p,
61 Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
62 };
63 let store = match ObjectStore::open(&cwd) {
64 Ok(s) => s,
65 Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
66 };
67 let mkit_dir = cwd.join(mkit_core::MKIT_DIR);
68
69 if opts.batch {
70 if opts.object.is_some() {
71 return super::usage_error("mkit cat-file --batch takes no object argument");
72 }
73 return run_batch(&store, &mkit_dir);
74 }
75 if !(opts.type_ || opts.size || opts.pretty) {
76 return super::usage_error("usage: mkit cat-file (-t | -s | -p) <object> | --batch");
77 }
78 let Some(object) = opts.object.as_deref() else {
79 return super::usage_error("usage: mkit cat-file (-t | -s | -p) <object>");
80 };
81
82 let h = match revspec::resolve_revision(&store, &mkit_dir, object) {
83 Ok(h) => h,
84 Err(e) => return emit_err(&format!("bad object '{object}': {e}"), exit::DATAERR),
85 };
86 let obj = match store.read_object(&h) {
87 Ok(o) => o,
88 Err(e) => return emit_err(&format!("read: {e}"), exit::NOINPUT),
89 };
90
91 let mut stdout = std::io::stdout().lock();
92 if opts.type_ {
93 let _ = writeln!(stdout, "{}", git_type(&obj));
94 return exit::OK;
95 }
96 if opts.size {
97 let size = match object_size(&store, &h, &obj) {
98 Ok(s) => s,
99 Err(msg) => return emit_err(&msg, exit::GENERAL_ERROR),
100 };
101 let _ = writeln!(stdout, "{size}");
102 return exit::OK;
103 }
104 match pretty_print(&store, &h, &obj, &mut stdout) {
106 Ok(()) => exit::OK,
107 Err(msg) => emit_err(&msg, exit::GENERAL_ERROR),
108 }
109}
110
111fn run_batch(store: &ObjectStore, mkit_dir: &std::path::Path) -> u8 {
118 use std::io::BufRead;
119
120 let stdin = std::io::stdin();
121 let mut stdout = std::io::stdout().lock();
122 for line in stdin.lock().lines() {
123 let name = match line {
128 Ok(l) => l,
129 Err(e) => return emit_err(&format!("read stdin: {e}"), exit::NOINPUT),
130 };
131 let Ok(h) = revspec::resolve_revision(store, mkit_dir, &name) else {
132 let _ = writeln!(stdout, "{name} missing");
133 continue;
134 };
135 let Ok(obj) = store.read_object(&h) else {
136 let _ = writeln!(stdout, "{name} missing");
137 continue;
138 };
139 let mut buf: Vec<u8> = Vec::new();
142 if let Err(msg) = pretty_print(store, &h, &obj, &mut buf) {
143 return emit_err(&msg, exit::GENERAL_ERROR);
144 }
145 let _ = writeln!(
146 stdout,
147 "{} {} {}",
148 format::hex_hash(&h),
149 git_type(&obj),
150 buf.len()
151 );
152 let _ = stdout.write_all(&buf);
153 let _ = stdout.write_all(b"\n");
154 }
155 exit::OK
156}
157
158fn git_type(obj: &Object) -> &'static str {
160 match obj {
161 Object::Blob(_) | Object::ChunkedBlob(_) => "blob",
162 Object::Tree(_) => "tree",
163 Object::Commit(_) => "commit",
164 Object::Tag(_) => "tag",
165 Object::Remix(_) => "remix",
166 Object::Delta(_) => "delta",
167 }
168}
169
170fn object_size(
173 store: &ObjectStore,
174 h: &mkit_core::hash::Hash,
175 obj: &Object,
176) -> Result<u64, String> {
177 Ok(match obj {
178 Object::Blob(b) => b.data.len() as u64,
179 Object::ChunkedBlob(c) => c.total_size,
180 _ => store.read(h).map_err(|e| format!("read: {e}"))?.len() as u64,
181 })
182}
183
184fn pretty_print(
185 store: &ObjectStore,
186 h: &mkit_core::hash::Hash,
187 obj: &Object,
188 out: &mut impl Write,
189) -> Result<(), String> {
190 match obj {
191 Object::Blob(b) => {
192 let _ = out.write_all(&b.data);
193 }
194 Object::ChunkedBlob(_) => {
195 let data = worktree::read_blob(store, h).map_err(|e| format!("reassemble: {e}"))?;
196 let _ = out.write_all(&data);
197 }
198 Object::Tree(t) => {
199 for e in &t.entries {
200 let (mode, ty) = git_mode_and_type(e.mode);
201 let _ = writeln!(
202 out,
203 "{mode} {ty} {}\t{}",
204 format::hex_hash(&e.object_hash),
205 String::from_utf8_lossy(&e.name)
206 );
207 }
208 }
209 Object::Commit(c) => {
210 let _ = writeln!(out, "tree {}", format::hex_hash(&c.tree_hash));
211 for p in &c.parents {
212 let _ = writeln!(out, "parent {}", format::hex_hash(p));
213 }
214 let _ = writeln!(out, "author {}", format::full_identity(&c.author));
215 let _ = writeln!(out, "timestamp {}", c.timestamp);
216 let _ = writeln!(out);
217 let _ = out.write_all(&c.message);
218 let _ = writeln!(out);
219 }
220 Object::Tag(t) => {
221 let _ = writeln!(out, "object {}", format::hex_hash(&t.target));
222 let _ = writeln!(out, "type {}", t.target_type.name());
223 let _ = writeln!(out, "tag {}", String::from_utf8_lossy(&t.name));
224 let _ = writeln!(out, "tagger {}", format::full_identity(&t.tagger));
225 let _ = writeln!(out, "timestamp {}", t.timestamp);
226 let _ = writeln!(out);
227 let _ = out.write_all(&t.message);
228 let _ = writeln!(out);
229 }
230 other => {
231 let _ = writeln!(out, "{other}");
232 }
233 }
234 Ok(())
235}
236
237pub(super) fn git_mode_and_type(mode: EntryMode) -> (&'static str, &'static str) {
240 match mode {
241 EntryMode::Blob => ("100644", "blob"),
242 EntryMode::Executable => ("100755", "blob"),
243 EntryMode::Symlink => ("120000", "blob"),
244 EntryMode::Tree => ("040000", "tree"),
245 }
246}
247
248fn emit_err(msg: &str, code: u8) -> u8 {
249 let mut stderr = std::io::stderr().lock();
250 let _ = writeln!(stderr, "error: {msg}");
251 code
252}