use std::path::Path;
use std::process::Command;
#[derive(Debug, Clone)]
pub struct ReflogEntry {
pub selector: String,
pub short_hash: String,
pub full_hash: String,
pub op: String,
pub subject: String,
pub relative_time: String,
}
pub fn list(workspace: &Path, limit: usize) -> Vec<ReflogEntry> {
let limit = limit.clamp(1, 1000);
let fmt = "%H%x09%h%x09%gd%x09%gr%x09%gs";
let out = match Command::new("git")
.args([
"reflog",
&format!("-n{limit}"),
&format!("--pretty=format:{fmt}"),
])
.current_dir(workspace)
.output()
{
Ok(o) if o.status.success() => o,
_ => return Vec::new(),
};
String::from_utf8_lossy(&out.stdout)
.lines()
.filter_map(|line| {
let mut parts = line.split('\t');
let full_hash = parts.next()?.to_string();
let short_hash = parts.next()?.to_string();
let selector = parts.next()?.to_string();
let relative_time = parts.next()?.to_string();
let gs = parts.next()?.to_string();
let (op, subject) = match gs.split_once(": ") {
Some((o, s)) => (o.to_string(), s.to_string()),
None => (gs.clone(), String::new()),
};
Some(ReflogEntry {
selector,
short_hash,
full_hash,
op,
subject,
relative_time,
})
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
fn init_repo(d: &Path) {
for args in [
&["init", "-q", "-b", "main"][..],
&["config", "user.email", "t@example.com"][..],
&["config", "user.name", "Test"][..],
&["config", "commit.gpgsign", "false"][..],
] {
let _ = Command::new("git").args(args).current_dir(d).output();
}
std::fs::write(d.join("a.txt"), "hi").unwrap();
let _ = Command::new("git")
.args(["add", "-A"])
.current_dir(d)
.output();
let _ = Command::new("git")
.args(["commit", "-qm", "initial"])
.current_dir(d)
.output();
std::fs::write(d.join("a.txt"), "hi2").unwrap();
let _ = Command::new("git")
.args(["commit", "-aqm", "second"])
.current_dir(d)
.output();
}
#[test]
fn list_returns_reflog_entries_newest_first() {
let dir = tempfile::tempdir().unwrap();
init_repo(dir.path());
let entries = list(dir.path(), 10);
assert!(
entries.len() >= 2,
"expected ≥ 2 entries, got {}",
entries.len()
);
assert_eq!(entries[0].selector, "HEAD@{0}");
assert!(entries[0].subject.contains("second") || entries[0].op.contains("second"));
}
#[test]
fn list_returns_empty_for_non_repo() {
let dir = tempfile::tempdir().unwrap();
let entries = list(dir.path(), 10);
assert!(entries.is_empty());
}
}