use std::path::Path;
use crate::error::Result;
use crate::hash::GitHash;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReflogEntry {
pub old: GitHash,
pub new: GitHash,
pub name: String,
pub email: String,
pub timestamp: i64,
pub tz_offset: String,
pub message: String,
}
#[must_use]
pub fn parse_reflog(bytes: &[u8]) -> Vec<ReflogEntry> {
let text = String::from_utf8_lossy(bytes);
text.lines().filter_map(parse_line).collect()
}
fn parse_line(line: &str) -> Option<ReflogEntry> {
let (prefix, message) = line.split_once('\t')?;
let email_start = prefix.find('<')?;
let email_end = prefix[email_start..].find('>')? + email_start;
let head = prefix.get(..email_start)?.trim_end();
let email = prefix.get(email_start + 1..email_end)?.to_string();
let tail = prefix.get(email_end + 1..)?.trim();
let mut head_parts = head.splitn(3, ' ');
let old = GitHash::from_hex(head_parts.next()?).ok()?;
let new = GitHash::from_hex(head_parts.next()?).ok()?;
let name = head_parts.next()?.trim().to_string();
let mut tail_parts = tail.split_whitespace();
let timestamp: i64 = tail_parts.next()?.parse().ok()?;
let tz_offset = tail_parts.next().unwrap_or("+0000").to_string();
Some(ReflogEntry {
old,
new,
name,
email,
timestamp,
tz_offset,
message: message.to_string(),
})
}
pub fn read_reflog(git_dir: &Path, refname: &str) -> Result<Vec<ReflogEntry>> {
let path = git_dir.join("logs").join(refname);
match std::fs::read(&path) {
Ok(bytes) => Ok(parse_reflog(&bytes)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
Err(e) => Err(e.into()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_a_basic_line() {
let e = parse_line(
"0000000000000000000000000000000000000000 \
3abc579ce97f2484371fbe6e52d1fa43699479b5 A <a@b.x> 100 +0000\tcommit: x",
)
.unwrap();
assert_eq!(e.name, "A");
assert_eq!(e.email, "a@b.x");
assert_eq!(e.timestamp, 100);
assert_eq!(e.message, "commit: x");
}
#[test]
fn rejects_a_line_without_a_tab() {
assert!(parse_line("no tab here").is_none());
}
#[test]
fn rejects_a_line_without_an_email() {
assert!(parse_line("aaa bbb name 100 +0000\tmsg").is_none());
}
#[test]
fn rejects_a_short_hash() {
assert!(parse_line("dead beef A <a@b.x> 100 +0000\tmsg").is_none());
}
#[test]
fn rejects_a_non_numeric_timestamp() {
assert!(parse_line(
"0000000000000000000000000000000000000000 \
3abc579ce97f2484371fbe6e52d1fa43699479b5 A <a@b.x> nope +0000\tmsg"
)
.is_none());
}
#[test]
fn defaults_missing_tz_to_utc() {
let e = parse_line(
"0000000000000000000000000000000000000000 \
3abc579ce97f2484371fbe6e52d1fa43699479b5 A <a@b.x> 100\tmsg",
)
.unwrap();
assert_eq!(e.tz_offset, "+0000");
}
}