use std::io::{self, BufRead, Read, Write};
use std::path::PathBuf;
use std::process::ExitCode;
use clap_complete::ArgValueCompleter;
use color_eyre::eyre::{Result, WrapErr as _, bail, eyre};
use girt::{
Commit, ListLimits, Object, ObjectId, ObjectKind, ObjectStore, Objects, Repository, Tag, Tree,
};
use crate::common::{not_cancelled, objects, read_limits, resolve, stdout, write_line};
use crate::tree::write_entry;
use crate::{Context, complete};
#[derive(Debug, clap::Subcommand)]
pub enum Command {
Id(RevArgs),
Hash(HashArgs),
Show(RevArgs),
Info(InfoArgs),
Cat(RevArgs),
Exists(RevArgs),
Batch(BatchArgs),
List(ListArgs),
Count,
}
#[derive(Debug, clap::Args)]
pub struct ListArgs {
#[arg(long, conflicts_with = "loose")]
packed: bool,
#[arg(long)]
loose: bool,
}
#[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 {
#[arg(add = ArgValueCompleter::new(complete::revisions))]
rev: String,
}
#[derive(Debug, clap::Args)]
pub struct InfoArgs {
#[arg(add = ArgValueCompleter::new(complete::revisions))]
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),
Command::List(args) => list(&args, context),
Command::Count => count(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)
}
fn list(args: &ListArgs, context: &Context) -> Result<ExitCode> {
let repository = context.repository()?;
let objects = objects(&repository)?;
let mut ids = if args.packed {
objects.packs().flat_map(|pack| pack.ids()).collect()
} else if args.loose {
let mut ids = Vec::new();
for store in objects.stores() {
ids.extend(loose_ids(&store)?);
}
ids
} else {
objects
.object_ids_controlled(ListLimits::default(), not_cancelled())
.wrap_err("listing objects")?
};
ids.sort_unstable();
ids.dedup();
let mut headers = objects
.headers_controlled(ids, read_limits(), not_cancelled())
.wrap_err("reading object headers")?;
headers.sort_unstable_by_key(|(id, _)| *id);
let mut out = io::BufWriter::new(stdout());
for (id, header) in headers {
let header = header.ok_or_else(|| eyre!("object {id} vanished while listing"))?;
writeln!(out, "{id} {} {}", header.kind().as_str(), header.size())?;
}
out.flush()?;
Ok(ExitCode::SUCCESS)
}
pub(crate) fn loose_ids(store: &ObjectStore<'_>) -> Result<Vec<ObjectId>> {
store
.loose_objects()
.map(|object| Ok(object?.id()))
.collect::<Result<_, girt::ObjectReadError>>()
.wrap_err_with(|| format!("scanning {}", store.directory().display()))
}
fn count(context: &Context) -> Result<ExitCode> {
let repository = context.repository()?;
let objects = objects(&repository)?;
let mut stores = objects.stores();
let primary = stores
.next()
.ok_or_else(|| eyre!("object storage has no primary store"))?;
let (mut loose, mut loose_bytes) = (Vec::new(), 0);
for object in primary.loose_objects() {
let object =
object.wrap_err_with(|| format!("scanning {}", primary.directory().display()))?;
match std::fs::symlink_metadata(object.path()) {
Ok(metadata) => loose_bytes += disk_usage(&metadata),
Err(error) if error.kind() == io::ErrorKind::NotFound => continue,
Err(error) => {
return Err(error).wrap_err_with(|| format!("reading {}", object.path().display()));
}
}
loose.push(object.id());
}
let packs: Vec<_> = primary.packs().collect();
let in_pack: usize = packs.iter().map(|pack| pack.object_count()).sum();
let pack_bytes: u64 = packs
.iter()
.map(|pack| pack.pack_bytes() + pack.index_bytes())
.sum();
let mut prune_packable = 0;
for &id in &loose {
for pack in &packs {
if pack.contains(id)? {
prune_packable += 1;
break;
}
}
}
let mut out = stdout();
if context.git {
writeln!(out, "count: {}", loose.len())?;
writeln!(out, "size: {}", loose_bytes / 1024)?;
writeln!(out, "in-pack: {in_pack}")?;
writeln!(out, "packs: {}", packs.len())?;
writeln!(out, "size-pack: {}", pack_bytes / 1024)?;
writeln!(out, "prune-packable: {prune_packable}")?;
for store in stores {
writeln!(out, "alternate: {}", store.directory().display())?;
}
} else {
writeln!(out, "loose {}", loose.len())?;
writeln!(out, "loose-bytes {loose_bytes}")?;
writeln!(out, "packed {in_pack}")?;
writeln!(out, "packs {}", packs.len())?;
writeln!(out, "pack-bytes {pack_bytes}")?;
writeln!(out, "prune-packable {prune_packable}")?;
for store in stores {
writeln!(out, "alternate {}", store.directory().display())?;
}
}
out.flush()?;
Ok(ExitCode::SUCCESS)
}
fn disk_usage(metadata: &std::fs::Metadata) -> u64 {
#[cfg(unix)]
{
std::os::unix::fs::MetadataExt::blocks(metadata) * 512
}
#[cfg(not(unix))]
{
metadata.len()
}
}