Skip to main content

weavatrix_git/
reflog.rs

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}