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::config::ConfigInputs;
use girt::refs::{RefName, References};
use girt::revision::{Range, Resolver, Revision, Selection};
use girt::workflow::Environment;
use girt::{Commit, Config, ObjectId, ObjectKind, Objects, PackLimits, ReadLimits, Repository};
const MAX_SYMREF_DEPTH: usize = 5;
#[derive(Debug)]
pub struct Context {
directory: PathBuf,
pub git: bool,
pub color: bool,
pub progress: bool,
pub environment: Environment,
}
impl Context {
pub fn new(git: bool) -> Self {
Self {
directory: PathBuf::from("."),
git,
color: false,
progress: !git,
environment: Environment::from_process(),
}
}
pub fn directory(&self) -> &Path {
&self.directory
}
pub fn config_inputs(&self) -> Result<ConfigInputs> {
ConfigInputs::git_default(&self.environment).wrap_err("reading config environment")
}
pub fn repository(&self) -> Result<Repository> {
let no_repository = || format!("no repository at {}", self.directory.display());
let relative_selector = ["GIT_CONFIG_GLOBAL", "GIT_CONFIG_SYSTEM"]
.iter()
.any(|name| {
self.environment
.get(name)
.is_some_and(|path| !path.is_empty() && Path::new(path).is_relative())
});
if !relative_selector {
return Repository::discover_with_config(&self.directory, &self.config_inputs()?)
.wrap_err_with(no_repository);
}
let discovered = Repository::discover(&self.directory).wrap_err_with(no_repository)?;
let Some(top) = discovered.worktree() else {
return Repository::open_with_config(discovered.git_dir(), &self.config_inputs()?)
.wrap_err_with(no_repository);
};
let environment = self.environment.clone().with_current_dir(top);
let inputs =
ConfigInputs::git_default(&environment).wrap_err("reading config environment")?;
Repository::open_with_config(top, &inputs).wrap_err_with(no_repository)
}
pub fn user_config(&self) -> Result<Config> {
Config::resolve(&self.config_inputs()?).wrap_err("reading global config")
}
}
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> {
let objects = objects(repository)?;
resolve_with(repository, &objects, revision, None)
}
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 resolve_kind(
repository: &Repository,
objects: &Objects,
revision: &str,
kind: ObjectKind,
) -> Result<ObjectId> {
resolve_with(repository, objects, revision, Some(kind))
}
fn resolve_with(
repository: &Repository,
objects: &Objects,
revision: &str,
kind: Option<ObjectKind>,
) -> Result<ObjectId> {
let parsed: Revision = revision.parse()?;
let resolver = Resolver::new(repository, objects)?.with_read_limits(read_limits());
let id = match kind {
Some(kind) => resolver.resolve_to(&parsed, kind),
None => resolver.resolve(&parsed),
};
id.wrap_err_with(|| format!("resolving `{revision}`"))
}
pub fn select(repository: &Repository, objects: &Objects, ranges: &[String]) -> Result<Selection> {
let parsed = if ranges.is_empty() {
vec![Range::Include(Revision::head())]
} else {
ranges
.iter()
.map(|range| range.parse())
.collect::<Result<Vec<Range>, _>>()?
};
let resolver = Resolver::new(repository, objects)?.with_read_limits(read_limits());
Ok(resolver.select(&parsed)?)
}
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")
}
pub fn abbrev_len(repository: &Repository, objects: &Objects) -> Result<usize> {
let Some(value) = repository.config().value("core", None, "abbrev") else {
return Ok(objects.auto_abbrev_len());
};
let value = value.ok_or_else(|| eyre!("core.abbrev requires a value"))?;
if value.eq_ignore_ascii_case(b"auto") {
return Ok(objects.auto_abbrev_len());
}
if [b"false".as_slice(), b"no", b"off", b""]
.iter()
.any(|v| value.eq_ignore_ascii_case(v))
{
return Ok(64);
}
let length = repository
.config()
.integer("core", None, "abbrev")?
.unwrap_or(0);
if length < 4 {
bail!("invalid core.abbrev: {}", display_bytes(value));
}
Ok(usize::try_from(i32::try_from(length)?)?)
}