use std::io::{self, BufRead, Read, Write};
use std::path::PathBuf;
use std::process::ExitCode;
use color_eyre::eyre::{Result, WrapErr as _, bail, eyre};
use girt::{Commit, Object, ObjectId, ObjectKind, Objects, Repository, Tag, Tree};
use crate::Context;
use crate::common::{objects, read_limits, resolve, stdout, write_line};
use crate::tree::write_entry;
#[derive(Debug, clap::Subcommand)]
pub enum Command {
Id(RevArgs),
Hash(HashArgs),
Show(RevArgs),
Info(InfoArgs),
Cat(RevArgs),
Exists(RevArgs),
Batch(BatchArgs),
}
#[derive(Debug, clap::Args)]
pub struct HashArgs {
#[arg(required_unless_present = "stdin", conflicts_with = "stdin")]
file: Option<PathBuf>,
#[arg(long, short = 't', default_value = "blob", value_parser = parse_kind)]
kind: ObjectKind,
#[arg(long, short)]
write: bool,
#[arg(long)]
stdin: bool,
}
#[derive(Debug, clap::Args)]
pub struct RevArgs {
rev: String,
}
#[derive(Debug, clap::Args)]
pub struct InfoArgs {
rev: String,
#[arg(long, conflicts_with = "size")]
kind: bool,
#[arg(long)]
size: bool,
}
#[derive(Debug, clap::Args)]
pub struct BatchArgs {
#[arg(long)]
check: bool,
}
pub fn run(command: Command, context: &Context) -> Result<ExitCode> {
match command {
Command::Hash(args) => hash(args, context),
Command::Id(args) => id(args, context),
Command::Show(args) => show(args, context),
Command::Info(args) => info(args, context),
Command::Cat(args) => cat(args, context),
Command::Exists(args) => exists(args, context),
Command::Batch(args) => batch(args, context),
}
}
fn parse_kind(value: &str) -> Result<ObjectKind, String> {
match value {
"blob" => Ok(ObjectKind::Blob),
"tree" => Ok(ObjectKind::Tree),
"commit" => Ok(ObjectKind::Commit),
"tag" => Ok(ObjectKind::Tag),
_ => Err(format!("unknown object kind `{value}`")),
}
}
fn hash(args: HashArgs, context: &Context) -> Result<ExitCode> {
let bytes = match &args.file {
Some(path) => {
std::fs::read(path).wrap_err_with(|| format!("reading {}", path.display()))?
}
None => {
let mut bytes = Vec::new();
io::stdin()
.read_to_end(&mut bytes)
.wrap_err("reading stdin")?;
bytes
}
};
let repository = context.repository().ok();
let format = repository
.as_ref()
.map(Repository::object_format)
.unwrap_or(girt::ObjectFormat::Sha1);
if args.write && repository.is_none() {
bail!("--write needs a repository");
}
let id = match args.kind {
ObjectKind::Blob => {
if let Some(repository) = repository.as_ref().filter(|_| args.write) {
repository.loose_objects().write_blob(&bytes)?
} else {
ObjectId::for_blob(format, &bytes)
}
}
ObjectKind::Tree => {
let tree = Tree::parse(format, &bytes).wrap_err("parsing tree")?;
tree.validate().wrap_err("validating tree")?;
match repository.as_ref().filter(|_| args.write) {
Some(repository) => repository.loose_objects().write_tree(&tree)?,
None => tree.id(),
}
}
ObjectKind::Commit => {
let commit = Commit::parse(format, &bytes).wrap_err("parsing commit")?;
match repository.as_ref().filter(|_| args.write) {
Some(repository) => repository.loose_objects().write_commit(&commit)?,
None => commit.id(),
}
}
ObjectKind::Tag => {
let tag = Tag::parse(format, &bytes).wrap_err("parsing tag")?;
match repository.as_ref().filter(|_| args.write) {
Some(repository) => repository.loose_objects().write_tag(&tag)?,
None => tag.id(),
}
}
};
let mut out = stdout();
writeln!(out, "{id}")?;
out.flush()?;
Ok(ExitCode::SUCCESS)
}
fn read(context: &Context, rev: &str) -> Result<Object> {
let repository = context.repository()?;
let objects = objects(&repository)?;
let id = resolve(&repository, rev)?;
read_id(&objects, id)?.ok_or_else(|| eyre!("missing object {id}"))
}
fn read_id(objects: &Objects, id: ObjectId) -> Result<Option<Object>> {
objects
.read(id, read_limits())
.wrap_err_with(|| format!("reading {id}"))
}
fn show(args: RevArgs, context: &Context) -> Result<ExitCode> {
let object = read(context, &args.rev)?;
let mut out = stdout();
if object.kind() == ObjectKind::Tree {
let tree = Tree::parse(object.object_format(), object.data()).wrap_err("parsing tree")?;
for entry in tree.entries() {
write_entry(&mut out, entry, &entry.name, context.git)?;
}
} else {
out.write_all(object.data())?;
}
out.flush()?;
Ok(ExitCode::SUCCESS)
}
fn info(args: InfoArgs, context: &Context) -> Result<ExitCode> {
let object = read(context, &args.rev)?;
let mut out = stdout();
let kind = object.kind().as_str();
let size = object.data().len();
if args.kind {
writeln!(out, "{kind}")?;
} else if args.size {
writeln!(out, "{size}")?;
} else {
writeln!(out, "{} {kind} {size}", object.id())?;
}
out.flush()?;
Ok(ExitCode::SUCCESS)
}
fn cat(args: RevArgs, context: &Context) -> Result<ExitCode> {
let object = read(context, &args.rev)?;
let mut out = stdout();
out.write_all(object.data())?;
out.flush()?;
Ok(ExitCode::SUCCESS)
}
fn id(args: RevArgs, context: &Context) -> Result<ExitCode> {
let id = resolve(&context.repository()?, &args.rev)?;
let mut out = stdout();
writeln!(out, "{id}")?;
out.flush()?;
Ok(ExitCode::SUCCESS)
}
fn exists(args: RevArgs, context: &Context) -> Result<ExitCode> {
let repository = context.repository()?;
let objects = objects(&repository)?;
let Ok(id) = resolve(&repository, &args.rev) else {
return Ok(ExitCode::FAILURE);
};
Ok(match read_id(&objects, id)? {
Some(_) => ExitCode::SUCCESS,
None => ExitCode::FAILURE,
})
}
fn batch(args: BatchArgs, context: &Context) -> Result<ExitCode> {
let repository = context.repository()?;
let objects = objects(&repository)?;
let mut input = io::stdin().lock();
let mut out = stdout();
let mut line = String::new();
loop {
line.clear();
if input.read_line(&mut line).wrap_err("reading stdin")? == 0 {
break;
}
let rev = line.trim_end_matches(['\n', '\r']);
let object = match resolve(&repository, rev) {
Ok(id) => read_id(&objects, id)?,
Err(_) => None,
};
let Some(object) = object else {
writeln!(out, "{rev} missing")?;
continue;
};
writeln!(
out,
"{} {} {}",
object.id(),
object.kind().as_str(),
object.data().len()
)?;
if !args.check {
write_line(&mut out, object.data())?;
}
}
out.flush()?;
Ok(ExitCode::SUCCESS)
}