use std::ops::ControlFlow;
use std::path::Path;
use crate::error::{Error, Result};
use crate::reflog::{
delete_reflog_entries, delete_reflog_entries_rechain, expire_reflog_git, list_reflog_refs,
load_gc_reflog_expire_config, parse_reflog_expire_spec, reflog_exists, ReflogEntry,
ReflogExpireParams, ReflogExpireResult,
};
use crate::refs::resolve_ref_dwim;
use crate::refs::store::RefName;
use crate::repo::Repository;
#[derive(Debug, Clone)]
pub struct ExpireOptions {
pub now: i64,
pub expire: Option<String>,
pub expire_unreachable: Option<String>,
pub all: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReflogExpireOutcome {
pub entries_pruned: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndexedReflogEntry {
pub index: usize,
pub entry: ReflogEntry,
}
#[derive(Debug, Copy, Clone)]
pub struct RepositoryReflogs<'repo> {
repo: &'repo Repository,
}
impl<'repo> RepositoryReflogs<'repo> {
pub(crate) fn new(repo: &'repo Repository) -> Self {
Self { repo }
}
#[must_use]
pub fn repository(&self) -> &'repo Repository {
self.repo
}
pub fn refs(&self) -> Result<Vec<RefName>> {
let git_dir = self.repo.git_dir();
let mut out = Vec::new();
for name in list_reflog_refs(git_dir)? {
if name == "HEAD" {
out.push(
RefName::try_from("HEAD")
.map_err(|e| Error::Message(format!("invalid HEAD ref name: {e}")))?,
);
continue;
}
if let Ok(parsed) = RefName::try_from(name.as_str()) {
out.push(parsed);
}
}
out.sort_by(|a, b| a.as_str().cmp(b.as_str()));
Ok(out)
}
pub fn entries(&self, ref_name: &RefName) -> Result<Vec<IndexedReflogEntry>> {
let (canonical, raw) = self.load_reflog(ref_name.as_str())?;
let _ = canonical;
Ok(index_entries_newest_first(&raw))
}
pub fn delete(&self, ref_name: &RefName, indices: &[usize], rewrite: bool) -> Result<()> {
if indices.is_empty() {
return Ok(());
}
let (canonical, entries) = self.load_reflog(ref_name.as_str())?;
let len = entries.len();
for &idx in indices {
if idx >= len {
return Err(Error::ReflogIndexOutOfRange {
refname: canonical,
index: idx,
});
}
}
let git_dir = self.repo.git_dir();
if rewrite {
delete_reflog_entries_rechain(git_dir, &canonical, indices)?;
} else {
delete_reflog_entries(git_dir, &canonical, indices)?;
}
Ok(())
}
pub fn expire(&self, options: &ExpireOptions) -> Result<ReflogExpireOutcome> {
let cfg = self.repo.config()?;
let gc = load_gc_reflog_expire_config(&cfg, options.now);
let explicit_total = options
.expire
.as_deref()
.map(|s| parse_reflog_expire_spec(s, options.now))
.transpose()?;
let explicit_unreachable = options
.expire_unreachable
.as_deref()
.map(|s| parse_reflog_expire_spec(s, options.now))
.transpose()?;
let params = ReflogExpireParams {
stale_fix: false,
dry_run: false,
verbose: false,
};
let git_dir = self.repo.git_dir();
let refnames = expire_target_refs(git_dir, options.all)?;
let mut total = 0usize;
for refname in refnames {
let ReflogExpireResult { pruned, .. } = expire_reflog_git(
self.repo,
git_dir,
&refname,
¶ms,
explicit_total,
explicit_unreachable,
&gc.patterns,
gc.global_total,
gc.global_unreachable,
options.now,
)?;
total += pruned;
}
Ok(ReflogExpireOutcome {
entries_pruned: total,
})
}
fn load_reflog(&self, user_ref: &str) -> Result<(String, Vec<ReflogEntry>)> {
let git_dir = self.repo.git_dir();
for candidate in dwim_reflog_candidates(user_ref) {
let entries = read_reflog_via_store(self.repo, &candidate)?;
if !entries.is_empty() {
return Ok((candidate, entries));
}
}
if !reflog_ref_known(git_dir, user_ref)? {
return Err(Error::ReflogUnknownRef {
refname: user_ref.to_owned(),
});
}
let canonical = canonical_reflog_ref(git_dir, user_ref)?;
Ok((canonical, Vec::new()))
}
}
fn read_reflog_via_store(repo: &Repository, refname: &str) -> Result<Vec<ReflogEntry>> {
let mut entries = Vec::new();
repo.refs()
.for_each_reflog_entry(refname, false, &mut |entry| {
entries.push(entry.clone());
ControlFlow::Continue(())
})?;
Ok(entries)
}
fn index_entries_newest_first(entries: &[ReflogEntry]) -> Vec<IndexedReflogEntry> {
entries
.iter()
.rev()
.enumerate()
.map(|(index, entry)| IndexedReflogEntry {
index,
entry: entry.clone(),
})
.collect()
}
fn dwim_reflog_candidates(user_ref: &str) -> Vec<String> {
let mut out = vec![user_ref.to_owned()];
if user_ref != "HEAD" && !user_ref.starts_with("refs/") {
out.push(format!("refs/{user_ref}"));
out.push(format!("refs/heads/{user_ref}"));
}
out
}
fn canonical_reflog_ref(git_dir: &Path, user_ref: &str) -> Result<String> {
for candidate in dwim_reflog_candidates(user_ref) {
if reflog_exists(git_dir, &candidate) {
return Ok(candidate);
}
}
if reflog_ref_known(git_dir, user_ref)? {
if user_ref.starts_with("refs/") || user_ref == "HEAD" {
return Ok(user_ref.to_owned());
}
return Ok(format!("refs/heads/{user_ref}"));
}
Err(Error::ReflogUnknownRef {
refname: user_ref.to_owned(),
})
}
fn reflog_ref_known(git_dir: &Path, user_ref: &str) -> Result<bool> {
if user_ref == "HEAD" {
return Ok(true);
}
for candidate in dwim_reflog_candidates(user_ref) {
if reflog_exists(git_dir, &candidate) {
return Ok(true);
}
let (_, oid) = resolve_ref_dwim(git_dir, &candidate);
if oid.is_some() {
return Ok(true);
}
}
let (_, oid) = resolve_ref_dwim(git_dir, user_ref);
Ok(oid.is_some())
}
fn expire_target_refs(git_dir: &Path, all: bool) -> Result<Vec<String>> {
if !all {
return list_reflog_refs(git_dir);
}
let mut names: Vec<String> = crate::refs::list_refs(git_dir, "refs/")?
.into_iter()
.map(|(name, _)| name)
.collect();
if reflog_exists(git_dir, "HEAD") {
names.push("HEAD".to_owned());
}
names.sort();
names.dedup();
Ok(names)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::refs::append_reflog;
use crate::repo::{InitOptions, Repository};
fn init_repo() -> (tempfile::TempDir, Repository) {
let dir = tempfile::tempdir().expect("tempdir");
let repo = Repository::init(dir.path(), &InitOptions::default()).expect("init");
(dir, repo)
}
#[test]
fn delete_out_of_range_returns_typed_error() {
let (_dir, repo) = init_repo();
let git_dir = repo.git_dir().to_path_buf();
let refname = "refs/heads/main";
append_reflog(
&git_dir,
refname,
&crate::objects::ObjectId::zero(),
&crate::objects::ObjectId::zero(),
"T <t@example.com> 1000000000 +0000",
"only",
true,
)
.expect("append");
let name = RefName::try_from(refname).expect("valid");
let err = repo
.reflogs()
.delete(&name, &[1], false)
.expect_err("out of range");
match err {
Error::ReflogIndexOutOfRange { refname, index } => {
assert_eq!(refname, "refs/heads/main");
assert_eq!(index, 1);
}
other => panic!("expected ReflogIndexOutOfRange, got {other:?}"),
}
}
}