Skip to main content

mkit_cli/commands/
cat_file.rs

1//! `mkit cat-file (-t | -s | -p) <object>` — inspect an object, like
2//! `git cat-file`.
3//!
4//! - `-t` — print the object type (`blob`/`tree`/`commit`/`tag`; mkit's
5//!   `remix` is the one non-git type);
6//! - `-s` — print the object size. For blobs this is the content byte
7//!   length (matches git); for trees/commits it is mkit's serialized size,
8//!   which differs from git's (different object format);
9//! - `-p` — pretty-print: a blob's raw bytes, a tree as
10//!   `<mode> <type> <hash>\t<name>` lines (git-shaped, modulo hash length),
11//!   or a readable commit/tag/remix summary;
12//! - `--batch` — read object names from stdin (one per line) and emit, per
13//!   object, a `<hash> <type> <size>` header then the content (or
14//!   `<name> missing` for unknown objects).
15//!
16//! `<object>` is resolved through the shared revspec grammar (full/short
17//! hash, ref, `HEAD`, `HEAD~n`/`^`).
18
19use 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)] // clap option flags, not a state machine
34struct CatFileOpts {
35    /// Print the object type.
36    #[arg(short = 't', conflicts_with_all = ["size", "pretty", "batch"])]
37    type_: bool,
38    /// Print the object size.
39    #[arg(short = 's', conflicts_with_all = ["pretty", "batch"])]
40    size: bool,
41    /// Pretty-print the object content.
42    #[arg(short = 'p', conflicts_with = "batch")]
43    pretty: bool,
44    /// Batch mode: read object names from stdin, emitting
45    /// `<hash> <type> <size>` then content for each (`<name> missing` for
46    /// unknown objects).
47    #[arg(long)]
48    batch: bool,
49    /// Object to inspect (hash, ref, HEAD, …). Omitted in `--batch` mode.
50    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    // -p
105    match pretty_print(&store, &h, &obj, &mut stdout) {
106        Ok(()) => exit::OK,
107        Err(msg) => emit_err(&msg, exit::GENERAL_ERROR),
108    }
109}
110
111/// `--batch`: read object names (one per line) from stdin and emit, per
112/// object, a `<hash> <type> <size>` header line followed by the content and
113/// a trailing newline. Unknown objects print `<name> missing`, matching
114/// `git cat-file --batch`. `<size>` is the byte length of the content that
115/// follows, so blobs are byte-exact with git; commit/tree/tag content is
116/// mkit-shaped (and so is its size), as with `-p`.
117fn 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        // One output record per input line. The whole line is the object
124        // name (no trimming, no skipping) — mkit has no `%(rest)` format —
125        // so a blank or whitespace-bearing line simply fails to resolve and
126        // yields a `<name> missing` record, exactly like git.
127        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        // Render content to a buffer so the advertised size is exactly the
140        // byte length we emit (self-consistent for every object type).
141        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
158/// git-compatible type token. mkit's `remix` has no git equivalent.
159fn 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
170/// Object size: blob content length (git-compatible) / chunked total size,
171/// else mkit's serialized object size (differs from git).
172fn 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
237/// `(octal mode, type)` for a tree entry, in git's `ls-tree`/`cat-file -p`
238/// form. Shared with `mkit show` so its tree listing matches.
239pub(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}