use std::io::Write;
use std::process::ExitCode;
use clap_complete::{ArgValueCandidates, ArgValueCompleter};
use color_eyre::eyre::{Result, WrapErr as _, bail, eyre};
use girt::refs::{Dereference, Expected, RefEdit, RefName, References, Reflog, Target};
use girt::{ObjectId, ObjectKind, ReadLimits, Repository, Signature};
use crate::common::{self, display_bytes};
use crate::{Context, complete};
const MAX_DEPTH: usize = 5;
#[derive(Debug, clap::Subcommand)]
pub enum Command {
List {
prefixes: Vec<String>,
#[arg(long)]
symbolic: bool,
},
Show {
#[arg(add = ArgValueCandidates::new(complete::references))]
name: String,
},
Set {
#[arg(add = ArgValueCandidates::new(complete::references))]
name: String,
#[arg(add = ArgValueCompleter::new(complete::revisions))]
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 {
#[arg(add = ArgValueCandidates::new(complete::references))]
name: String,
#[arg(long, value_name = "OLD_ID")]
expect: Option<String>,
},
Log {
#[arg(add = ArgValueCandidates::new(complete::references))]
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(&repository, &old)?)),
(None, true) => Expected::Absent,
(None, false) => Expected::Any,
};
let destination = ref_name(&name)?;
let destination = references
.resolve(&destination)
.map_or(destination, |resolution| resolution.name);
let reflog = repository
.reflog_for(&destination, committer(&repository, context)?, message)
.wrap_err("reading core.logAllRefUpdates")?;
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(&repository, &old)?)),
None => Expected::Exists,
};
edit(&references, &name, None, expected, Reflog::Delete)
}
Command::Log { name } => log(&repository, &references, &name, context),
}
}
fn parse_id(repository: &Repository, hex: &str) -> Result<ObjectId> {
repository
.object_id_from_hex(hex)
.map_err(|_| eyre!("`{hex}` is not a full object ID"))
}
fn check_target(
repository: &Repository,
references: &References<'_>,
name: &str,
id: ObjectId,
) -> Result<()> {
let objects = repository.objects()?;
let object = objects
.header(id, ReadLimits::trusted())
.wrap_err_with(|| format!("reading {id}"))?
.ok_or_else(|| eyre!("object {id} does not exist"))?;
let name = ref_name(name)?;
let destination = references
.resolve(&name)
.map_or(name, |resolution| resolution.name);
if destination.is_branch() && object.kind() != ObjectKind::Commit {
bail!(
"{} must point at a commit, not a {:?}",
display_bytes(destination.as_bytes()),
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 = repository.objects()?;
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
.header(id, ReadLimits::trusted())
.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 = repository.objects()?;
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, context: &Context) -> Result<Signature> {
Signature::committer(repository.config(), &context.environment)
.wrap_err("building reflog committer: set user.name/user.email or GIT_COMMITTER_NAME/EMAIL; GIT_COMMITTER_DATE must be raw or RFC 3339")
}