use std::io::Write;
use clap::Parser;
use mkit_core::object::{EntryMode, Object};
use mkit_core::store::ObjectStore;
use mkit_core::worktree;
use super::revspec;
use crate::clap_shim;
use crate::exit;
use crate::format;
#[derive(Debug, Parser)]
#[command(name = "mkit cat-file", about = "Inspect a stored object.")]
#[allow(clippy::struct_excessive_bools)] struct CatFileOpts {
#[arg(short = 't', conflicts_with_all = ["size", "pretty", "batch"])]
type_: bool,
#[arg(short = 's', conflicts_with_all = ["pretty", "batch"])]
size: bool,
#[arg(short = 'p', conflicts_with = "batch")]
pretty: bool,
#[arg(long)]
batch: bool,
object: Option<String>,
}
#[must_use]
pub fn run(args: &[String]) -> u8 {
let opts = match clap_shim::parse::<CatFileOpts>("mkit cat-file", args) {
Ok(o) => o,
Err(code) => return code,
};
let cwd = match std::env::current_dir() {
Ok(p) => p,
Err(e) => return emit_err(&format!("cwd: {e}"), exit::NOINPUT),
};
let layout = match super::resolve_layout(&cwd) {
Ok(layout) => layout,
Err(code) => return code,
};
let store = match ObjectStore::open(&layout) {
Ok(s) => s,
Err(e) => return emit_err(&format!("not a mkit repo: {e}"), exit::GENERAL_ERROR),
};
if opts.batch {
if opts.object.is_some() {
return super::usage_error("mkit cat-file --batch takes no object argument");
}
return run_batch(&store, &layout);
}
if !(opts.type_ || opts.size || opts.pretty) {
return super::usage_error("usage: mkit cat-file (-t | -s | -p) <object> | --batch");
}
let Some(object) = opts.object.as_deref() else {
return super::usage_error("usage: mkit cat-file (-t | -s | -p) <object>");
};
let h = match revspec::resolve_revision(&store, &layout, object) {
Ok(h) => h,
Err(e) => return emit_err(&format!("bad object '{object}': {e}"), exit::DATAERR),
};
let obj = match store.read_object(&h) {
Ok(o) => o,
Err(e) => return emit_err(&format!("read: {e}"), exit::NOINPUT),
};
let mut stdout = std::io::stdout().lock();
if opts.type_ {
let _ = writeln!(stdout, "{}", git_type(&obj));
return exit::OK;
}
if opts.size {
let size = match object_size(&store, &h, &obj) {
Ok(s) => s,
Err(msg) => return emit_err(&msg, exit::GENERAL_ERROR),
};
let _ = writeln!(stdout, "{size}");
return exit::OK;
}
match pretty_print(&store, &h, &obj, &mut stdout) {
Ok(()) => exit::OK,
Err(msg) => emit_err(&msg, exit::GENERAL_ERROR),
}
}
fn run_batch(store: &ObjectStore, layout: &mkit_core::layout::RepoLayout) -> u8 {
use std::io::BufRead;
let stdin = std::io::stdin();
let mut stdout = std::io::stdout().lock();
for line in stdin.lock().lines() {
let name = match line {
Ok(l) => l,
Err(e) => return emit_err(&format!("read stdin: {e}"), exit::NOINPUT),
};
let Ok(h) = revspec::resolve_revision(store, layout, &name) else {
let _ = writeln!(stdout, "{name} missing");
continue;
};
let Ok(obj) = store.read_object(&h) else {
let _ = writeln!(stdout, "{name} missing");
continue;
};
let mut buf: Vec<u8> = Vec::new();
if let Err(msg) = pretty_print(store, &h, &obj, &mut buf) {
return emit_err(&msg, exit::GENERAL_ERROR);
}
let _ = writeln!(
stdout,
"{} {} {}",
format::hex_hash(&h),
git_type(&obj),
buf.len()
);
let _ = stdout.write_all(&buf);
let _ = stdout.write_all(b"\n");
}
exit::OK
}
fn git_type(obj: &Object) -> &'static str {
match obj {
Object::Blob(_) | Object::ChunkedBlob(_) => "blob",
Object::Tree(_) => "tree",
Object::Commit(_) => "commit",
Object::Tag(_) => "tag",
Object::Remix(_) => "remix",
Object::Delta(_) => "delta",
}
}
fn object_size(
store: &ObjectStore,
h: &mkit_core::hash::Hash,
obj: &Object,
) -> Result<u64, String> {
Ok(match obj {
Object::Blob(b) => b.data.len() as u64,
Object::ChunkedBlob(c) => c.total_size,
_ => store.read(h).map_err(|e| format!("read: {e}"))?.len() as u64,
})
}
fn pretty_print(
store: &ObjectStore,
h: &mkit_core::hash::Hash,
obj: &Object,
out: &mut impl Write,
) -> Result<(), String> {
match obj {
Object::Blob(b) => {
let _ = out.write_all(&b.data);
}
Object::ChunkedBlob(_) => {
let data = worktree::read_blob(store, h).map_err(|e| format!("reassemble: {e}"))?;
let _ = out.write_all(&data);
}
Object::Tree(t) => {
for e in &t.entries {
let (mode, ty) = git_mode_and_type(e.mode);
let _ = writeln!(
out,
"{mode} {ty} {}\t{}",
format::hex_hash(&e.object_hash),
String::from_utf8_lossy(&e.name)
);
}
}
Object::Commit(c) => {
let _ = writeln!(out, "tree {}", format::hex_hash(&c.tree_hash));
for p in &c.parents {
let _ = writeln!(out, "parent {}", format::hex_hash(p));
}
let _ = writeln!(out, "author {}", format::full_identity(&c.author));
let _ = writeln!(out, "timestamp {}", c.timestamp);
let _ = writeln!(out);
let _ = out.write_all(&c.message);
let _ = writeln!(out);
}
Object::Tag(t) => {
let _ = writeln!(out, "object {}", format::hex_hash(&t.target));
let _ = writeln!(out, "type {}", t.target_type.name());
let _ = writeln!(out, "tag {}", String::from_utf8_lossy(&t.name));
let _ = writeln!(out, "tagger {}", format::full_identity(&t.tagger));
let _ = writeln!(out, "timestamp {}", t.timestamp);
let _ = writeln!(out);
let _ = out.write_all(&t.message);
let _ = writeln!(out);
}
other => {
let _ = writeln!(out, "{other}");
}
}
Ok(())
}
pub(super) fn git_mode_and_type(mode: EntryMode) -> (&'static str, &'static str) {
match mode {
EntryMode::Blob => ("100644", "blob"),
EntryMode::Executable => ("100755", "blob"),
EntryMode::Symlink => ("120000", "blob"),
EntryMode::Tree => ("040000", "tree"),
}
}
use super::error as emit_err;