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::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 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
81fn 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 Object::Delta(_) => Err(("cannot show a delta object".to_string(), exit::DATAERR)),
140 }
141}
142
143#[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 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
180fn 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 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
205fn 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}