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 layout = match super::resolve_layout(&cwd) {
64 Ok(layout) => layout,
65 Err(code) => return code,
66 };
67 let store = match ObjectStore::open(&layout) {
68 Ok(s) => s,
69 Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
70 };
71
72 if opts.batch {
73 if opts.object.is_some() {
74 return super::usage_error("mkit cat-file --batch takes no object argument");
75 }
76 return run_batch(&store, &layout);
77 }
78 if !(opts.type_ || opts.size || opts.pretty) {
79 return super::usage_error("usage: mkit cat-file (-t | -s | -p) <object> | --batch");
80 }
81 let Some(object) = opts.object.as_deref() else {
82 return super::usage_error("usage: mkit cat-file (-t | -s | -p) <object>");
83 };
84
85 let h = match revspec::resolve_revision(&store, &layout, object) {
86 Ok(h) => h,
87 Err(e) => return emit_err(&format!("bad object '{object}': {e}"), exit::DATAERR),
88 };
89 let obj = match store.read_object(&h) {
90 Ok(o) => o,
91 Err(e) => return emit_err(&format!("read: {e}"), exit::NOINPUT),
92 };
93
94 let mut stdout = std::io::stdout().lock();
95 if opts.type_ {
96 let _ = writeln!(stdout, "{}", git_type(&obj));
97 return exit::OK;
98 }
99 if opts.size {
100 let size = match object_size(&store, &h, &obj) {
101 Ok(s) => s,
102 Err(msg) => return emit_err(&msg, exit::GENERAL_ERROR),
103 };
104 let _ = writeln!(stdout, "{size}");
105 return exit::OK;
106 }
107 match pretty_print(&store, &h, &obj, &mut stdout) {
109 Ok(()) => exit::OK,
110 Err(msg) => emit_err(&msg, exit::GENERAL_ERROR),
111 }
112}
113
114fn run_batch(store: &ObjectStore, layout: &mkit_core::layout::RepoLayout) -> u8 {
121 use std::io::BufRead;
122
123 let stdin = std::io::stdin();
124 let mut stdout = std::io::stdout().lock();
125 for line in stdin.lock().lines() {
126 let name = match line {
131 Ok(l) => l,
132 Err(e) => return emit_err(&format!("read stdin: {e}"), exit::NOINPUT),
133 };
134 let Ok(h) = revspec::resolve_revision(store, layout, &name) else {
135 let _ = writeln!(stdout, "{name} missing");
136 continue;
137 };
138 let Ok(obj) = store.read_object(&h) else {
139 let _ = writeln!(stdout, "{name} missing");
140 continue;
141 };
142 let mut buf: Vec<u8> = Vec::new();
145 if let Err(msg) = pretty_print(store, &h, &obj, &mut buf) {
146 return emit_err(&msg, exit::GENERAL_ERROR);
147 }
148 let _ = writeln!(
149 stdout,
150 "{} {} {}",
151 format::hex_hash(&h),
152 git_type(&obj),
153 buf.len()
154 );
155 let _ = stdout.write_all(&buf);
156 let _ = stdout.write_all(b"\n");
157 }
158 exit::OK
159}
160
161fn git_type(obj: &Object) -> &'static str {
163 match obj {
164 Object::Blob(_) | Object::ChunkedBlob(_) => "blob",
165 Object::Tree(_) => "tree",
166 Object::Commit(_) => "commit",
167 Object::Tag(_) => "tag",
168 Object::Remix(_) => "remix",
169 Object::Delta(_) => "delta",
170 }
171}
172
173fn object_size(
176 store: &ObjectStore,
177 h: &mkit_core::hash::Hash,
178 obj: &Object,
179) -> Result<u64, String> {
180 Ok(match obj {
181 Object::Blob(b) => b.data.len() as u64,
182 Object::ChunkedBlob(c) => c.total_size,
183 _ => store.read(h).map_err(|e| format!("read: {e}"))?.len() as u64,
184 })
185}
186
187fn pretty_print(
188 store: &ObjectStore,
189 h: &mkit_core::hash::Hash,
190 obj: &Object,
191 out: &mut impl Write,
192) -> Result<(), String> {
193 match obj {
194 Object::Blob(b) => {
195 let _ = out.write_all(&b.data);
196 }
197 Object::ChunkedBlob(_) => {
198 let data = worktree::read_blob(store, h).map_err(|e| format!("reassemble: {e}"))?;
199 let _ = out.write_all(&data);
200 }
201 Object::Tree(t) => {
202 for e in &t.entries {
203 let (mode, ty) = git_mode_and_type(e.mode);
204 let _ = writeln!(
205 out,
206 "{mode} {ty} {}\t{}",
207 format::hex_hash(&e.object_hash),
208 String::from_utf8_lossy(&e.name)
209 );
210 }
211 }
212 Object::Commit(c) => {
213 let _ = writeln!(out, "tree {}", format::hex_hash(&c.tree_hash));
214 for p in &c.parents {
215 let _ = writeln!(out, "parent {}", format::hex_hash(p));
216 }
217 let _ = writeln!(out, "author {}", format::full_identity(&c.author));
218 let _ = writeln!(out, "timestamp {}", c.timestamp);
219 let _ = writeln!(out);
220 let _ = out.write_all(&c.message);
221 let _ = writeln!(out);
222 }
223 Object::Tag(t) => {
224 let _ = writeln!(out, "object {}", format::hex_hash(&t.target));
225 let _ = writeln!(out, "type {}", t.target_type.name());
226 let _ = writeln!(out, "tag {}", String::from_utf8_lossy(&t.name));
227 let _ = writeln!(out, "tagger {}", format::full_identity(&t.tagger));
228 let _ = writeln!(out, "timestamp {}", t.timestamp);
229 let _ = writeln!(out);
230 let _ = out.write_all(&t.message);
231 let _ = writeln!(out);
232 }
233 other => {
234 let _ = writeln!(out, "{other}");
235 }
236 }
237 Ok(())
238}
239
240pub(super) fn git_mode_and_type(mode: EntryMode) -> (&'static str, &'static str) {
243 match mode {
244 EntryMode::Blob => ("100644", "blob"),
245 EntryMode::Executable => ("100755", "blob"),
246 EntryMode::Symlink => ("120000", "blob"),
247 EntryMode::Tree => ("040000", "tree"),
248 }
249}
250
251use super::error as emit_err;