1use std::fs;
2
3use crate::{GitError, ObjectId, Repository, Result, Signature, error::invalid, refs};
4
5#[derive(Clone, Debug, PartialEq, Eq)]
6pub struct ReflogEntry {
7 pub old_id: ObjectId,
8 pub new_id: ObjectId,
9 pub committer: Signature,
10 pub message: Vec<u8>,
11}
12
13impl ReflogEntry {
14 #[must_use]
15 pub fn message_lossy(&self) -> String {
16 String::from_utf8_lossy(&self.message).into_owned()
17 }
18}
19
20impl Repository {
21 pub fn reflog(&self, name: &str) -> Result<Vec<ReflogEntry>> {
22 if name != "HEAD" {
23 refs::validate_name(name)?;
24 }
25 let relative = std::path::Path::new("logs").join(name);
26 let mut data = None;
27 for root in [self.git_dir(), self.common_dir()] {
28 match fs::read(root.join(&relative)) {
29 Ok(value) => {
30 data = Some(value);
31 break;
32 }
33 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
34 Err(error) => return Err(error.into()),
35 }
36 }
37 let data = data.ok_or_else(|| GitError::NotFound(format!("reflog {name}")))?;
38 let mut entries = data
39 .split(|byte| *byte == b'\n')
40 .filter(|line| !line.is_empty())
41 .map(|line| parse_entry(line, self.hash_kind()))
42 .collect::<Result<Vec<_>>>()?;
43 if entries.len() > self.limits().max_reflog_entries {
44 return Err(GitError::LimitExceeded {
45 resource: "reflog entries",
46 limit: self.limits().max_reflog_entries,
47 });
48 }
49 entries.reverse();
50 Ok(entries)
51 }
52}
53
54fn parse_entry(line: &[u8], hash: crate::HashKind) -> Result<ReflogEntry> {
55 let hex = hash.hex_len();
56 let old = line
57 .get(..hex)
58 .ok_or_else(|| invalid("truncated reflog old identifier"))?;
59 if line.get(hex) != Some(&b' ') {
60 return Err(invalid("reflog old identifier has no separator"));
61 }
62 let new_start = hex + 1;
63 let new_end = new_start + hex;
64 let new = line
65 .get(new_start..new_end)
66 .ok_or_else(|| invalid("truncated reflog new identifier"))?;
67 if line.get(new_end) != Some(&b' ') {
68 return Err(invalid("reflog new identifier has no separator"));
69 }
70 let remainder = &line[new_end + 1..];
71 let tab = remainder
72 .iter()
73 .position(|byte| *byte == b'\t')
74 .ok_or_else(|| invalid("reflog entry has no message separator"))?;
75 let old_id = parse_id(old, hash)?;
76 let new_id = parse_id(new, hash)?;
77 let committer = crate::object::parse_signature(&remainder[..tab])
78 .ok_or_else(|| invalid("invalid reflog committer"))?;
79 Ok(ReflogEntry {
80 old_id,
81 new_id,
82 committer,
83 message: remainder[tab + 1..].to_vec(),
84 })
85}
86
87fn parse_id(bytes: &[u8], hash: crate::HashKind) -> Result<ObjectId> {
88 ObjectId::from_hex_for(
89 std::str::from_utf8(bytes).map_err(|_| invalid("reflog identifier is not ASCII"))?,
90 hash,
91 )
92}
93
94#[cfg(test)]
95mod tests {
96 use super::parse_entry;
97 use crate::HashKind;
98
99 #[test]
100 fn parses_reflog_line() {
101 let line = b"0000000000000000000000000000000000000000 \
1021111111111111111111111111111111111111111 Ada <ada@example.com> 42 +0230\tcommit: one";
103 let entry = parse_entry(line, HashKind::Sha1).unwrap();
104 assert_eq!(entry.new_id.to_string(), "1".repeat(40));
105 assert_eq!(entry.committer.timezone_minutes, 150);
106 assert_eq!(entry.message_lossy(), "commit: one");
107 }
108}