weavatrix-git 0.3.1

Fast, bounded, evidence-carrying Git reader with an optional read-only MCP server
Documentation
use std::fs;

use crate::{GitError, ObjectId, Repository, Result, Signature, error::invalid, refs};

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ReflogEntry {
    pub old_id: ObjectId,
    pub new_id: ObjectId,
    pub committer: Signature,
    pub message: Vec<u8>,
}

impl ReflogEntry {
    #[must_use]
    pub fn message_lossy(&self) -> String {
        String::from_utf8_lossy(&self.message).into_owned()
    }
}

impl Repository {
    pub fn reflog(&self, name: &str) -> Result<Vec<ReflogEntry>> {
        if name != "HEAD" {
            refs::validate_name(name)?;
        }
        let relative = std::path::Path::new("logs").join(name);
        let mut data = None;
        for root in [self.git_dir(), self.common_dir()] {
            match fs::read(root.join(&relative)) {
                Ok(value) => {
                    data = Some(value);
                    break;
                }
                Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
                Err(error) => return Err(error.into()),
            }
        }
        let data = data.ok_or_else(|| GitError::NotFound(format!("reflog {name}")))?;
        let mut entries = data
            .split(|byte| *byte == b'\n')
            .filter(|line| !line.is_empty())
            .map(|line| parse_entry(line, self.hash_kind()))
            .collect::<Result<Vec<_>>>()?;
        if entries.len() > self.limits().max_reflog_entries {
            return Err(GitError::LimitExceeded {
                resource: "reflog entries",
                limit: self.limits().max_reflog_entries,
            });
        }
        entries.reverse();
        Ok(entries)
    }
}

fn parse_entry(line: &[u8], hash: crate::HashKind) -> Result<ReflogEntry> {
    let hex = hash.hex_len();
    let old = line
        .get(..hex)
        .ok_or_else(|| invalid("truncated reflog old identifier"))?;
    if line.get(hex) != Some(&b' ') {
        return Err(invalid("reflog old identifier has no separator"));
    }
    let new_start = hex + 1;
    let new_end = new_start + hex;
    let new = line
        .get(new_start..new_end)
        .ok_or_else(|| invalid("truncated reflog new identifier"))?;
    if line.get(new_end) != Some(&b' ') {
        return Err(invalid("reflog new identifier has no separator"));
    }
    let remainder = &line[new_end + 1..];
    let tab = remainder
        .iter()
        .position(|byte| *byte == b'\t')
        .ok_or_else(|| invalid("reflog entry has no message separator"))?;
    let old_id = parse_id(old, hash)?;
    let new_id = parse_id(new, hash)?;
    let committer = crate::object::parse_signature(&remainder[..tab])
        .ok_or_else(|| invalid("invalid reflog committer"))?;
    Ok(ReflogEntry {
        old_id,
        new_id,
        committer,
        message: remainder[tab + 1..].to_vec(),
    })
}

fn parse_id(bytes: &[u8], hash: crate::HashKind) -> Result<ObjectId> {
    ObjectId::from_hex_for(
        std::str::from_utf8(bytes).map_err(|_| invalid("reflog identifier is not ASCII"))?,
        hash,
    )
}

#[cfg(test)]
mod tests {
    use super::parse_entry;
    use crate::HashKind;

    #[test]
    fn parses_reflog_line() {
        let line = b"0000000000000000000000000000000000000000 \
1111111111111111111111111111111111111111 Ada <ada@example.com> 42 +0230\tcommit: one";
        let entry = parse_entry(line, HashKind::Sha1).unwrap();
        assert_eq!(entry.new_id.to_string(), "1".repeat(40));
        assert_eq!(entry.committer.timezone_minutes, 150);
        assert_eq!(entry.message_lossy(), "commit: one");
    }
}