use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use crate::error::{Error, Result};
use crate::objects::ObjectId;
#[derive(Debug, Clone)]
pub struct ReflogEntry {
pub old_oid: ObjectId,
pub new_oid: ObjectId,
pub identity: String,
pub message: String,
}
pub fn reflog_path(git_dir: &Path, refname: &str) -> PathBuf {
git_dir.join("logs").join(refname)
}
pub fn reflog_exists(git_dir: &Path, refname: &str) -> bool {
let path = reflog_path(git_dir, refname);
path.is_file()
}
pub fn read_reflog(git_dir: &Path, refname: &str) -> Result<Vec<ReflogEntry>> {
let path = reflog_path(git_dir, refname);
let content = match fs::read_to_string(&path) {
Ok(c) => c,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(Error::Io(e)),
};
let mut entries = Vec::new();
for line in content.lines() {
if line.is_empty() {
continue;
}
if let Some(entry) = parse_reflog_line(line) {
entries.push(entry);
}
}
Ok(entries)
}
fn parse_reflog_line(line: &str) -> Option<ReflogEntry> {
let (before_tab, message) = if let Some(pos) = line.find('\t') {
(&line[..pos], line[pos + 1..].to_string())
} else {
(line, String::new())
};
if before_tab.len() < 83 {
return None;
}
let old_hex = &before_tab[..40];
let new_hex = &before_tab[41..81];
let identity = before_tab[82..].to_string();
let old_oid = old_hex.parse::<ObjectId>().ok()?;
let new_oid = new_hex.parse::<ObjectId>().ok()?;
Some(ReflogEntry {
old_oid,
new_oid,
identity,
message,
})
}
pub fn delete_reflog_entries(
git_dir: &Path,
refname: &str,
indices: &[usize],
) -> Result<()> {
let mut entries = read_reflog(git_dir, refname)?;
if entries.is_empty() {
return Ok(());
}
entries.reverse();
let indices_set: std::collections::HashSet<usize> =
indices.iter().copied().collect();
let path = reflog_path(git_dir, refname);
let remaining: Vec<&ReflogEntry> = entries
.iter()
.enumerate()
.filter(|(i, _)| !indices_set.contains(i))
.map(|(_, e)| e)
.collect();
let mut lines = Vec::new();
for entry in remaining.iter().rev() {
lines.push(format_reflog_entry(entry));
}
fs::write(&path, lines.join(""))?;
Ok(())
}
pub fn expire_reflog(
git_dir: &Path,
refname: &str,
expire_time: Option<i64>,
) -> Result<usize> {
let entries = read_reflog(git_dir, refname)?;
if entries.is_empty() {
return Ok(0);
}
let path = reflog_path(git_dir, refname);
let mut kept = Vec::new();
let mut pruned = 0usize;
for entry in &entries {
let ts = parse_timestamp_from_identity(&entry.identity);
let dominated = match (expire_time, ts) {
(Some(cutoff), Some(t)) => t < cutoff,
(None, _) => true, (Some(_), None) => false, };
if dominated {
pruned += 1;
} else {
kept.push(format_reflog_entry(entry));
}
}
fs::write(&path, kept.join(""))?;
Ok(pruned)
}
fn format_reflog_entry(entry: &ReflogEntry) -> String {
format!(
"{} {} {}\t{}\n",
entry.old_oid, entry.new_oid, entry.identity, entry.message
)
}
fn parse_timestamp_from_identity(identity: &str) -> Option<i64> {
let parts: Vec<&str> = identity.rsplitn(3, ' ').collect();
if parts.len() >= 2 {
parts[1].parse::<i64>().ok()
} else {
None
}
}
pub fn list_reflog_refs(git_dir: &Path) -> Result<Vec<String>> {
let logs_dir = git_dir.join("logs");
let mut refs = Vec::new();
if logs_dir.join("HEAD").is_file() {
refs.push("HEAD".to_string());
}
let refs_logs = logs_dir.join("refs");
if refs_logs.is_dir() {
collect_reflog_refs(&refs_logs, "refs", &mut refs)?;
}
Ok(refs)
}
fn collect_reflog_refs(dir: &Path, prefix: &str, out: &mut Vec<String>) -> Result<()> {
let read_dir = match fs::read_dir(dir) {
Ok(rd) => rd,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(Error::Io(e)),
};
for entry in read_dir {
let entry = entry.map_err(Error::Io)?;
let name = entry.file_name().to_string_lossy().to_string();
let full_name = format!("{prefix}/{name}");
let ft = entry.file_type().map_err(Error::Io)?;
if ft.is_dir() {
collect_reflog_refs(&entry.path(), &full_name, out)?;
} else if ft.is_file() {
out.push(full_name);
}
}
Ok(())
}