1use std::path::Path;
12
13use crate::error::Result;
14use crate::hash::GitHash;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct ReflogEntry {
19 pub old: GitHash,
21 pub new: GitHash,
23 pub name: String,
25 pub email: String,
27 pub timestamp: i64,
29 pub tz_offset: String,
31 pub message: String,
33}
34
35#[must_use]
41pub fn parse_reflog(bytes: &[u8]) -> Vec<ReflogEntry> {
42 let text = String::from_utf8_lossy(bytes);
43 text.lines().filter_map(parse_line).collect()
44}
45
46fn parse_line(line: &str) -> Option<ReflogEntry> {
48 let (prefix, message) = line.split_once('\t')?;
50
51 let email_start = prefix.find('<')?;
55 let email_end = prefix[email_start..].find('>')? + email_start;
56
57 let head = prefix.get(..email_start)?.trim_end();
58 let email = prefix.get(email_start + 1..email_end)?.to_string();
59 let tail = prefix.get(email_end + 1..)?.trim();
60
61 let mut head_parts = head.splitn(3, ' ');
63 let old = GitHash::from_hex(head_parts.next()?).ok()?;
64 let new = GitHash::from_hex(head_parts.next()?).ok()?;
65 let name = head_parts.next()?.trim().to_string();
66
67 let mut tail_parts = tail.split_whitespace();
69 let timestamp: i64 = tail_parts.next()?.parse().ok()?;
70 let tz_offset = tail_parts.next().unwrap_or("+0000").to_string();
71
72 Some(ReflogEntry {
73 old,
74 new,
75 name,
76 email,
77 timestamp,
78 tz_offset,
79 message: message.to_string(),
80 })
81}
82
83pub fn read_reflog(git_dir: &Path, refname: &str) -> Result<Vec<ReflogEntry>> {
91 let path = git_dir.join("logs").join(refname);
92 match std::fs::read(&path) {
93 Ok(bytes) => Ok(parse_reflog(&bytes)),
94 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
95 Err(e) => Err(e.into()),
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn parses_a_basic_line() {
105 let e = parse_line(
106 "0000000000000000000000000000000000000000 \
107 3abc579ce97f2484371fbe6e52d1fa43699479b5 A <a@b.x> 100 +0000\tcommit: x",
108 )
109 .unwrap();
110 assert_eq!(e.name, "A");
111 assert_eq!(e.email, "a@b.x");
112 assert_eq!(e.timestamp, 100);
113 assert_eq!(e.message, "commit: x");
114 }
115
116 #[test]
117 fn rejects_a_line_without_a_tab() {
118 assert!(parse_line("no tab here").is_none());
119 }
120
121 #[test]
122 fn rejects_a_line_without_an_email() {
123 assert!(parse_line("aaa bbb name 100 +0000\tmsg").is_none());
124 }
125
126 #[test]
127 fn rejects_a_short_hash() {
128 assert!(parse_line("dead beef A <a@b.x> 100 +0000\tmsg").is_none());
129 }
130
131 #[test]
132 fn rejects_a_non_numeric_timestamp() {
133 assert!(parse_line(
134 "0000000000000000000000000000000000000000 \
135 3abc579ce97f2484371fbe6e52d1fa43699479b5 A <a@b.x> nope +0000\tmsg"
136 )
137 .is_none());
138 }
139
140 #[test]
141 fn defaults_missing_tz_to_utc() {
142 let e = parse_line(
143 "0000000000000000000000000000000000000000 \
144 3abc579ce97f2484371fbe6e52d1fa43699479b5 A <a@b.x> 100\tmsg",
145 )
146 .unwrap();
147 assert_eq!(e.tz_offset, "+0000");
148 }
149}