use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use color_eyre::eyre::{Result, WrapErr as _, bail, eyre};
use girt::refs::{RefName, References};
use girt::{Commit, ObjectId, ObjectKind, Objects, PackLimits, PeelLimits, ReadLimits, Repository};
const MAX_SYMREF_DEPTH: usize = 5;
#[derive(Debug)]
pub struct Context {
directory: PathBuf,
pub git: bool,
}
impl Context {
pub fn new(git: bool) -> Self {
Self {
directory: PathBuf::from("."),
git,
}
}
pub fn directory(&self) -> &Path {
&self.directory
}
pub fn repository(&self) -> Result<Repository> {
Repository::discover(&self.directory)
.wrap_err_with(|| format!("no repository at {}", self.directory.display()))
}
}
pub fn not_cancelled() -> &'static AtomicBool {
static FLAG: AtomicBool = AtomicBool::new(false);
&FLAG
}
pub fn objects(repository: &Repository) -> Result<Objects> {
repository
.objects(PackLimits::default())
.wrap_err("opening object storage")
}
pub fn read_limits() -> ReadLimits {
ReadLimits::default().with_max_object_bytes(1 << 30)
}
pub fn resolve(repository: &Repository, revision: &str) -> Result<ObjectId> {
if let Ok(id) = revision.parse::<ObjectId>() {
return Ok(id);
}
let references = repository.references().wrap_err("opening references")?;
for candidate in candidates(revision) {
let Ok(name) = RefName::new(&candidate) else {
continue;
};
if let Some(id) = resolve_ref(&references, &name)? {
return Ok(id);
}
}
bail!("unknown revision `{revision}`")
}
pub fn resolve_ref(references: &References<'_>, name: &RefName) -> Result<Option<ObjectId>> {
let resolution = references
.resolve(name, MAX_SYMREF_DEPTH)
.wrap_err_with(|| format!("resolving {}", display_bytes(name.as_bytes())))?;
Ok(resolution.id)
}
pub fn candidates(name: &str) -> Vec<String> {
if name == "HEAD" || name.starts_with("refs/") {
return vec![name.to_owned()];
}
vec![
format!("refs/{name}"),
format!("refs/tags/{name}"),
format!("refs/heads/{name}"),
format!("refs/remotes/{name}"),
format!("refs/remotes/{name}/HEAD"),
]
}
pub fn resolve_kind(
repository: &Repository,
objects: &Objects,
revision: &str,
kind: ObjectKind,
) -> Result<ObjectId> {
let id = resolve(repository, revision)?;
let peeled = objects
.peel(id, PeelLimits::default(), not_cancelled())
.wrap_err_with(|| format!("peeling {revision}"))?;
match (peeled.kind, kind) {
(found, wanted) if found == wanted => Ok(peeled.target),
(ObjectKind::Commit, ObjectKind::Tree) => Ok(read_commit(objects, peeled.target)?.tree()),
(found, wanted) => bail!("{revision} is a {found:?}, not a {wanted:?}"),
}
}
pub fn resolve_commit(
repository: &Repository,
objects: &Objects,
revision: &str,
) -> Result<ObjectId> {
resolve_kind(repository, objects, revision, ObjectKind::Commit)
}
pub fn resolve_tree(
repository: &Repository,
objects: &Objects,
revision: &str,
) -> Result<ObjectId> {
resolve_kind(repository, objects, revision, ObjectKind::Tree)
}
pub fn read_commit(objects: &Objects, id: ObjectId) -> Result<Commit> {
let object = objects
.read(id, read_limits())
.wrap_err_with(|| format!("reading {id}"))?
.ok_or_else(|| eyre!("missing object {id}"))?;
if object.kind() != ObjectKind::Commit {
bail!("{id} is a {:?}, not a commit", object.kind());
}
Commit::parse(objects.object_format(), object.data()).wrap_err_with(|| format!("parsing {id}"))
}
pub fn display_bytes(bytes: &[u8]) -> std::borrow::Cow<'_, str> {
String::from_utf8_lossy(bytes)
}
pub fn stdout() -> io::BufWriter<io::StdoutLock<'static>> {
io::BufWriter::new(io::stdout().lock())
}
pub fn write_line(out: &mut impl Write, bytes: &[u8]) -> io::Result<()> {
out.write_all(bytes)?;
out.write_all(b"\n")
}