1use 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::{DisplaySource, ObjectStore};
25use mkit_core::worktree;
26
27use super::revspec;
28use crate::clap_shim;
29use crate::exit;
30use crate::format;
31
32const 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 #[arg(long)]
45 stat: bool,
46 objects: Vec<String>,
49}
50
51#[must_use]
52pub fn run(args: &[String]) -> u8 {
53 let opts = match clap_shim::parse::<ShowOpts>("mkit show", args) {
54 Ok(o) => o,
55 Err(code) => return code,
56 };
57 let cwd = match std::env::current_dir() {
58 Ok(p) => p,
59 Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
60 };
61 let layout = match super::resolve_layout(&cwd) {
62 Ok(layout) => layout,
63 Err(code) => return code,
64 };
65 let store = match ObjectStore::open(&layout) {
66 Ok(s) => s,
67 Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
68 };
69
70 let specs: Vec<String> = if opts.objects.is_empty() {
71 vec!["HEAD".to_string()]
72 } else {
73 opts.objects.clone()
74 };
75
76 let mut stdout = std::io::stdout().lock();
77 for spec in &specs {
78 let h = match revspec::resolve_revision(&store, &layout, spec) {
79 Ok(h) => h,
80 Err(e) => return emit_err(&e.to_string(), exit::GENERAL_ERROR),
81 };
82 if let Err((msg, code)) = show_object(&mut stdout, &store, &h, 0, opts.stat) {
83 return emit_err(&msg, code);
84 }
85 }
86 exit::OK
87}
88
89fn show_object(
91 out: &mut impl Write,
92 store: &ObjectStore,
93 h: &Hash,
94 tag_depth: usize,
95 stat: bool,
96) -> Result<(), (String, u8)> {
97 let obj = store
98 .read_object(h)
99 .map_err(|e| (format!("read object: {e}"), exit::GENERAL_ERROR))?;
100 match obj {
101 Object::Commit(c) => show_commit_like(
102 out,
103 store,
104 h,
105 "commit",
106 &c.author,
107 c.timestamp,
108 &c.message,
109 c.parents.first().copied(),
110 c.tree_hash,
111 stat,
112 ),
113 Object::Remix(r) => show_commit_like(
114 out,
115 store,
116 h,
117 "remix",
118 &r.author,
119 r.timestamp,
120 &r.message,
121 r.parents.first().copied(),
122 r.tree_hash,
123 stat,
124 ),
125 Object::Tag(t) => show_tag(out, store, &t, tag_depth, stat),
126 Object::Tree(t) => {
127 for e in &t.entries {
128 let (mode, ty) = super::cat_file::git_mode_and_type(e.mode);
129 let _ = writeln!(
130 out,
131 "{mode} {ty} {}\t{}",
132 format::hex_hash(&e.object_hash),
133 String::from_utf8_lossy(&e.name)
134 );
135 }
136 Ok(())
137 }
138 Object::Blob(b) => {
139 let _ = out.write_all(&b.data);
140 Ok(())
141 }
142 Object::ChunkedBlob(_) => {
143 let data = worktree::read_blob(store, h)
144 .map_err(|e| (format!("reassemble: {e}"), exit::GENERAL_ERROR))?;
145 let _ = out.write_all(&data);
146 Ok(())
147 }
148 Object::Delta(_) => Err(("cannot show a delta object".to_string(), exit::DATAERR)),
151 }
152}
153
154#[allow(clippy::too_many_arguments)]
157fn show_commit_like(
158 out: &mut impl Write,
159 store: &ObjectStore,
160 hash: &Hash,
161 label: &str,
162 author: &Identity,
163 timestamp: u64,
164 message: &[u8],
165 parent: Option<Hash>,
166 tree: Hash,
167 stat: bool,
168) -> Result<(), (String, u8)> {
169 let _ = writeln!(out, "{label} {}", format::hex_hash(hash));
170 let _ = writeln!(out, "Author: {}", format::short_identity(author));
171 let _ = writeln!(out, "Date: {}", format::human_date_utc(timestamp));
172 let _ = writeln!(out);
173 write_indented_message(out, message);
174 let _ = writeln!(out);
175
176 let parent_tree = match parent {
179 Some(p) => {
180 Some(super::diff::object_to_tree(store, &p).map_err(|e| (e, exit::GENERAL_ERROR))?)
181 }
182 None => None,
183 };
184 let result = diff_trees(store, parent_tree, Some(tree))
185 .map_err(|e| (format!("diff: {e}"), exit::GENERAL_ERROR))?;
186 if stat {
190 return super::diff::render_stat(out, store, result.entries.iter())
191 .map_err(|e| (e, exit::GENERAL_ERROR));
192 }
193 let display = DisplaySource::new(store);
197 for e in &result.entries {
198 super::diff::emit_entry_patch(
199 out,
200 &display,
201 e,
202 mkit_core::ops::DEFAULT_CONTEXT_LINES,
203 mkit_core::ops::WhitespaceMode::Exact,
204 )
205 .map_err(|e| (e, exit::GENERAL_ERROR))?;
206 }
207 Ok(())
208}
209
210fn show_tag(
212 out: &mut impl Write,
213 store: &ObjectStore,
214 t: &Tag,
215 tag_depth: usize,
216 stat: bool,
217) -> Result<(), (String, u8)> {
218 let _ = writeln!(out, "tag {}", String::from_utf8_lossy(&t.name));
219 let _ = writeln!(out, "Tagger: {}", format::short_identity(&t.tagger));
220 let _ = writeln!(out, "Date: {}", format::human_date_utc(t.timestamp));
221 let _ = writeln!(out);
222 let msg = String::from_utf8_lossy(&t.message);
225 for line in msg.lines() {
226 let _ = writeln!(out, "{line}");
227 }
228 let _ = writeln!(out);
229
230 if tag_depth + 1 >= MAX_TAG_DEPTH {
231 return Err(("tag chain too deep".to_string(), exit::DATAERR));
232 }
233 show_object(out, store, &t.target, tag_depth + 1, stat)
234}
235
236fn write_indented_message(out: &mut impl Write, message: &[u8]) {
239 let text = String::from_utf8_lossy(message);
240 for line in text.lines() {
241 if line.is_empty() {
242 let _ = writeln!(out);
243 } else {
244 let _ = writeln!(out, " {line}");
245 }
246 }
247}
248
249use super::error as emit_err;