use std::io::Write;
use std::path::PathBuf;
use std::process::ExitCode;
use std::time::{SystemTime, UNIX_EPOCH};
use color_eyre::eyre::{Result, WrapErr as _, bail, eyre};
use girt::config::{ConfigFile, ConfigInputs, ConfigScope};
use girt::refs::{Dereference, Expected, RefEdit, RefName, References, Reflog, Target};
use girt::{Config, ObjectId, ObjectKind, Repository, Signature};
use crate::Context;
use crate::common::{self, display_bytes};
const MAX_DEPTH: usize = 5;
#[derive(Debug, clap::Subcommand)]
pub enum Command {
List {
prefixes: Vec<String>,
#[arg(long)]
symbolic: bool,
},
Show {
name: String,
},
Set {
name: String,
value: String,
#[arg(long, value_name = "OLD_ID", conflicts_with = "create_only")]
expect: Option<String>,
#[arg(long)]
create_only: bool,
#[arg(short, long, default_value = "")]
message: String,
},
Delete {
name: String,
#[arg(long, value_name = "OLD_ID")]
expect: Option<String>,
},
Log {
name: String,
},
}
pub fn run(command: Command, context: &Context) -> Result<ExitCode> {
let repository = context.repository()?;
let references = repository.references().wrap_err("opening references")?;
match command {
Command::List { prefixes, symbolic } => {
list(&repository, &references, &prefixes, symbolic, context)
}
Command::Show { name } => show(&references, &name, context),
Command::Set {
name,
value,
expect,
create_only,
message,
} => {
let id = common::resolve(&repository, &value)?;
check_target(&repository, &references, &name, id)?;
let expected = match (expect, create_only) {
(Some(old), _) => Expected::Value(Target::Direct(parse_id(&old)?)),
(None, true) => Expected::Absent,
(None, false) => Expected::Any,
};
let reflog = Reflog::Append {
committer: committer(&repository)?,
message: message.into_bytes(),
};
edit(
&references,
&name,
Some(Target::Direct(id)),
expected,
reflog,
)
}
Command::Delete { name, expect } => {
let expected = match expect {
Some(old) => Expected::Value(Target::Direct(parse_id(&old)?)),
None => Expected::Exists,
};
edit(&references, &name, None, expected, Reflog::Delete)
}
Command::Log { name } => log(&repository, &references, &name, context),
}
}
fn parse_id(hex: &str) -> Result<ObjectId> {
hex.parse()
.map_err(|_| eyre!("`{hex}` is not a full object ID"))
}
fn check_target(
repository: &Repository,
references: &References<'_>,
name: &str,
id: ObjectId,
) -> Result<()> {
let objects = common::objects(repository)?;
let object = objects
.read(id, common::read_limits())
.wrap_err_with(|| format!("reading {id}"))?
.ok_or_else(|| eyre!("object {id} does not exist"))?;
let destination = references.resolve(&ref_name(name)?, 5).map_or_else(
|_| name.as_bytes().to_vec(),
|resolution| resolution.name.as_bytes().to_vec(),
);
if destination.starts_with(b"refs/heads/") && object.kind() != ObjectKind::Commit {
bail!(
"{} must point at a commit, not a {:?}",
display_bytes(&destination),
object.kind()
);
}
Ok(())
}
fn ref_name(name: &str) -> Result<RefName> {
RefName::new(name).map_err(|_| eyre!("invalid reference name `{name}`"))
}
fn find(references: &References<'_>, name: &str) -> Result<(RefName, Target)> {
for full in girt::revision::ref_candidates(name) {
if let Some(target) = references
.read(&full)
.wrap_err_with(|| format!("reading {}", display_bytes(full.as_bytes())))?
{
return Ok((full, target));
}
}
bail!("unknown reference `{name}`")
}
fn list(
repository: &Repository,
references: &References<'_>,
prefixes: &[String],
symbolic: bool,
context: &Context,
) -> Result<ExitCode> {
let objects = common::objects(repository)?;
let mut all = references.list().wrap_err("listing references")?;
all.sort_by(|a, b| a.name.as_bytes().cmp(b.name.as_bytes()));
let mut out = common::stdout();
for reference in all {
let name = reference.name.as_bytes();
let matches = prefixes.is_empty()
|| prefixes.iter().any(|prefix| {
let prefix = prefix.trim_end_matches('/').as_bytes();
name == prefix
|| (name.starts_with(prefix) && name.get(prefix.len()) == Some(&b'/'))
});
let is_symbolic = matches!(reference.target, Target::Symbolic(_));
if !matches || (symbolic && !is_symbolic) {
continue;
}
let id = match &reference.target {
Target::Direct(id) => *id,
_ => match common::resolve_ref(references, &reference.name)? {
Some(id) => id,
None => continue,
},
};
let name = display_bytes(name);
if context.git {
let object = objects
.read(id, common::read_limits())
.wrap_err_with(|| format!("reading {id}"))?
.ok_or_else(|| eyre!("missing object {id} for {name}"))?;
writeln!(out, "{id} {}\t{name}", object.kind().as_str())?;
} else if let Target::Symbolic(target) = &reference.target {
writeln!(out, "{id} {name} -> {}", display_bytes(target.as_bytes()))?;
} else {
writeln!(out, "{id} {name}")?;
}
}
out.flush()?;
Ok(ExitCode::SUCCESS)
}
fn show(references: &References<'_>, name: &str, context: &Context) -> Result<ExitCode> {
let (mut current, mut target) = find(references, name)?;
let mut out = common::stdout();
for _ in 0..=MAX_DEPTH {
match target {
Target::Direct(id) => {
if !context.git {
write!(out, "{} -> ", display_bytes(current.as_bytes()))?;
}
writeln!(out, "{id}")?;
out.flush()?;
return Ok(ExitCode::SUCCESS);
}
Target::Symbolic(next) => {
if !context.git {
writeln!(
out,
"{} -> {}",
display_bytes(current.as_bytes()),
display_bytes(next.as_bytes())
)?;
}
target = references
.read(&next)
.wrap_err_with(|| format!("reading {}", display_bytes(next.as_bytes())))?
.ok_or_else(|| eyre!("{} does not exist", display_bytes(next.as_bytes())))?;
current = next;
}
_ => bail!("girt does not support this reference target yet"),
}
}
bail!("symbolic reference chain for `{name}` is too deep")
}
fn edit(
references: &References<'_>,
name: &str,
target: Option<Target>,
expected: Expected,
reflog: Reflog,
) -> Result<ExitCode> {
let edit = RefEdit::new(ref_name(name)?, target, expected, reflog)
.with_dereference(Dereference::Follow);
references
.transaction(&[edit])
.wrap_err_with(|| format!("updating {name}"))?;
Ok(ExitCode::SUCCESS)
}
fn log(
repository: &Repository,
references: &References<'_>,
name: &str,
context: &Context,
) -> Result<ExitCode> {
let (full, _) = find(references, name)?;
let entries = references
.reflog(&full)
.wrap_err_with(|| format!("reading reflog of {name}"))?
.unwrap_or_default();
let objects = common::objects(repository)?;
let width = common::abbrev_len(repository, &objects)?;
let mut out = common::stdout();
for (index, entry) in entries.iter().rev().enumerate() {
let message = display_bytes(&entry.message);
if context.git {
let hex = objects.abbreviate(entry.new, width)?.to_string();
writeln!(out, "{} {name}@{{{index}}}: {message}", hex)?;
} else {
writeln!(out, "{} {} {message}", entry.new, entry.old)?;
}
}
out.flush()?;
Ok(ExitCode::SUCCESS)
}
fn committer(repository: &Repository) -> Result<Signature> {
let global = global_config();
let lookup = |env: &str, key: &str| -> Option<Vec<u8>> {
std::env::var(env).ok().map(String::into_bytes).or_else(|| {
[Some(repository.config()), global.as_ref()]
.into_iter()
.flatten()
.find_map(|config| config.string("user", None, key).map(<[u8]>::to_vec))
})
};
let (Some(name), Some(email)) = (
lookup("GIT_COMMITTER_NAME", "name"),
lookup("GIT_COMMITTER_EMAIL", "email"),
) else {
bail!("no committer identity for the reflog: set user.name and user.email in Git config");
};
let seconds = SystemTime::now()
.duration_since(UNIX_EPOCH)
.wrap_err("system clock is before 1970")?
.as_secs();
Ok(Signature {
name,
email,
seconds: i64::try_from(seconds)?,
offset_minutes: 0,
})
}
fn global_config() -> Option<Config> {
let home = std::env::var_os("HOME").map(PathBuf::from);
let xdg = std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.or_else(|| home.as_ref().map(|home| home.join(".config")));
let files = xdg
.map(|xdg| xdg.join("git/config"))
.into_iter()
.chain(home.map(|home| home.join(".gitconfig")))
.filter(|path| path.exists())
.map(|path| ConfigFile::new(path, ConfigScope::Global))
.collect();
Config::resolve(&ConfigInputs::default().with_files(files)).ok()
}