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");
}
}