Skip to main content

mkit_cli/commands/
show.rs

1//! `mkit show [<object>...]` — display objects (default `HEAD`).
2//!
3//! Mirrors `git show`:
4//! - **commit** / **remix**: a header (`commit <hash>` / `Author` / `Date` /
5//!   indented message, matching `mkit log`) followed by the unified diff
6//!   against its first parent. The diff body is produced by the same code as
7//!   `mkit diff`, so `show <commit>` is byte-identical to
8//!   `diff <parent> <commit>` (modulo the abbreviated `index` ids).
9//! - **tag**: the tag header, then the peeled target object.
10//! - **tree**: an `ls-tree`-style listing.
11//! - **blob**: the raw contents.
12//!
13//! Like `mkit log`, the commit/tag headers carry mkit's signed `Identity`
14//! and 64-hex BLAKE3 ids, so the header lines diverge from git's
15//! `Author: Name <email>` / 40-hex form — the same documented divergence as
16//! `log`. The diff body, tree listing, and blob output match git.
17
18use std::io::Write;
19
20use clap::Parser;
21use mkit_core::hash::Hash;
22use mkit_core::object::{Identity, Object, Tag};
23use mkit_core::ops::diff_trees;
24use mkit_core::store::ObjectStore;
25use mkit_core::worktree;
26
27use super::revspec;
28use crate::clap_shim;
29use crate::exit;
30use crate::format;
31
32/// Bound on tag-of-tag recursion, mirroring `diff`/`log`'s peel depth.
33const MAX_TAG_DEPTH: usize = 16;
34
35#[derive(Debug, Parser)]
36#[command(
37    name = "mkit show",
38    about = "Display objects (default HEAD): commits with their diff, tags, trees, blobs."
39)]
40struct ShowOpts {
41    /// Objects to show — revisions, refs, or hashes (e.g. `HEAD`, `main`,
42    /// `HEAD~2`, `<hash>`, `<tag>`). Defaults to `HEAD`.
43    objects: Vec<String>,
44}
45
46#[must_use]
47pub fn run(args: &[String]) -> u8 {
48    let opts = match clap_shim::parse::<ShowOpts>("mkit show", args) {
49        Ok(o) => o,
50        Err(code) => return code,
51    };
52    let cwd = match std::env::current_dir() {
53        Ok(p) => p,
54        Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
55    };
56    let store = match ObjectStore::open(&cwd) {
57        Ok(s) => s,
58        Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
59    };
60    let mkit_dir = cwd.join(mkit_core::MKIT_DIR);
61
62    let specs: Vec<String> = if opts.objects.is_empty() {
63        vec!["HEAD".to_string()]
64    } else {
65        opts.objects.clone()
66    };
67
68    let mut stdout = std::io::stdout().lock();
69    for spec in &specs {
70        let h = match revspec::resolve_revision(&store, &mkit_dir, spec) {
71            Ok(h) => h,
72            Err(e) => return emit_err(&e.to_string(), exit::GENERAL_ERROR),
73        };
74        if let Err((msg, code)) = show_object(&mut stdout, &store, &h, 0) {
75            return emit_err(&msg, code);
76        }
77    }
78    exit::OK
79}
80
81/// Display one object. `tag_depth` bounds tag-of-tag recursion.
82fn show_object(
83    out: &mut impl Write,
84    store: &ObjectStore,
85    h: &Hash,
86    tag_depth: usize,
87) -> Result<(), (String, u8)> {
88    let obj = store
89        .read_object(h)
90        .map_err(|e| (format!("read object: {e}"), exit::GENERAL_ERROR))?;
91    match obj {
92        Object::Commit(c) => show_commit_like(
93            out,
94            store,
95            h,
96            "commit",
97            &c.author,
98            c.timestamp,
99            &c.message,
100            c.parents.first().copied(),
101            c.tree_hash,
102        ),
103        Object::Remix(r) => show_commit_like(
104            out,
105            store,
106            h,
107            "remix",
108            &r.author,
109            r.timestamp,
110            &r.message,
111            r.parents.first().copied(),
112            r.tree_hash,
113        ),
114        Object::Tag(t) => show_tag(out, store, &t, tag_depth),
115        Object::Tree(t) => {
116            for e in &t.entries {
117                let (mode, ty) = super::cat_file::git_mode_and_type(e.mode);
118                let _ = writeln!(
119                    out,
120                    "{mode} {ty} {}\t{}",
121                    format::hex_hash(&e.object_hash),
122                    String::from_utf8_lossy(&e.name)
123                );
124            }
125            Ok(())
126        }
127        Object::Blob(b) => {
128            let _ = out.write_all(&b.data);
129            Ok(())
130        }
131        Object::ChunkedBlob(_) => {
132            let data = worktree::read_blob(store, h)
133                .map_err(|e| (format!("reassemble: {e}"), exit::GENERAL_ERROR))?;
134            let _ = out.write_all(&data);
135            Ok(())
136        }
137        // `Delta` is a pack-only encoding and never the result of a plain
138        // `read_object`, but handle it explicitly rather than via a wildcard.
139        Object::Delta(_) => Err(("cannot show a delta object".to_string(), exit::DATAERR)),
140    }
141}
142
143/// Render a commit or remix: a `mkit log`-style header followed by the
144/// unified diff against the first parent (an empty tree for a root commit).
145#[allow(clippy::too_many_arguments)]
146fn show_commit_like(
147    out: &mut impl Write,
148    store: &ObjectStore,
149    hash: &Hash,
150    label: &str,
151    author: &Identity,
152    timestamp: u64,
153    message: &[u8],
154    parent: Option<Hash>,
155    tree: Hash,
156) -> Result<(), (String, u8)> {
157    let _ = writeln!(out, "{label} {}", format::hex_hash(hash));
158    let _ = writeln!(out, "Author: {}", format::short_identity(author));
159    let _ = writeln!(out, "Date:   {}", format::human_date_utc(timestamp));
160    let _ = writeln!(out);
161    write_indented_message(out, message);
162    let _ = writeln!(out);
163
164    // Diff the first parent's tree against this tree (None ⇒ empty, so a
165    // root commit shows every file as added), reusing `diff`'s renderer.
166    let parent_tree = match parent {
167        Some(p) => {
168            Some(super::diff::object_to_tree(store, &p).map_err(|e| (e, exit::GENERAL_ERROR))?)
169        }
170        None => None,
171    };
172    let result = diff_trees(store, parent_tree, Some(tree))
173        .map_err(|e| (format!("diff: {e}"), exit::GENERAL_ERROR))?;
174    for e in &result.entries {
175        super::diff::emit_entry_patch(out, store, e).map_err(|e| (e, exit::GENERAL_ERROR))?;
176    }
177    Ok(())
178}
179
180/// Render an annotated/signed tag header, then the peeled target object.
181fn show_tag(
182    out: &mut impl Write,
183    store: &ObjectStore,
184    t: &Tag,
185    tag_depth: usize,
186) -> Result<(), (String, u8)> {
187    let _ = writeln!(out, "tag {}", String::from_utf8_lossy(&t.name));
188    let _ = writeln!(out, "Tagger: {}", format::short_identity(&t.tagger));
189    let _ = writeln!(out, "Date:   {}", format::human_date_utc(t.timestamp));
190    let _ = writeln!(out);
191    // git prints the tag message un-indented, then a blank line, then the
192    // target object.
193    let msg = String::from_utf8_lossy(&t.message);
194    for line in msg.lines() {
195        let _ = writeln!(out, "{line}");
196    }
197    let _ = writeln!(out);
198
199    if tag_depth + 1 >= MAX_TAG_DEPTH {
200        return Err(("tag chain too deep".to_string(), exit::DATAERR));
201    }
202    show_object(out, store, &t.target, tag_depth + 1)
203}
204
205/// Write a commit message indented four spaces per line (blank lines stay
206/// blank), matching `mkit log`'s default format.
207fn write_indented_message(out: &mut impl Write, message: &[u8]) {
208    let text = String::from_utf8_lossy(message);
209    for line in text.lines() {
210        if line.is_empty() {
211            let _ = writeln!(out);
212        } else {
213            let _ = writeln!(out, "    {line}");
214        }
215    }
216}
217
218fn emit_err(msg: &str, code: u8) -> u8 {
219    let mut stderr = std::io::stderr().lock();
220    let _ = writeln!(stderr, "error: {msg}");
221    code
222}