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 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    // -p
108    match pretty_print(&store, &h, &obj, &mut stdout) {
109        Ok(()) => exit::OK,
110        Err(msg) => emit_err(&msg, exit::GENERAL_ERROR),
111    }
112}
113
114/// `--batch`: read object names (one per line) from stdin and emit, per
115/// object, a `<hash> <type> <size>` header line followed by the content and
116/// a trailing newline. Unknown objects print `<name> missing`, matching
117/// `git cat-file --batch`. `<size>` is the byte length of the content that
118/// follows, so blobs are byte-exact with git; commit/tree/tag content is
119/// mkit-shaped (and so is its size), as with `-p`.
120fn 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        // One output record per input line. The whole line is the object
127        // name (no trimming, no skipping) — mkit has no `%(rest)` format —
128        // so a blank or whitespace-bearing line simply fails to resolve and
129        // yields a `<name> missing` record, exactly like git.
130        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        // Render content to a buffer so the advertised size is exactly the
143        // byte length we emit (self-consistent for every object type).
144        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
161/// git-compatible type token. mkit's `remix` has no git equivalent.
162fn 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
173/// Object size: blob content length (git-compatible) / chunked total size,
174/// else mkit's serialized object size (differs from git).
175fn 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
240/// `(octal mode, type)` for a tree entry, in git's `ls-tree`/`cat-file -p`
241/// form. Shared with `mkit show` so its tree listing matches.
242pub(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;