use crate::error::{GitError, Result};
use crate::hash::GitHash;
#[derive(Debug, Clone)]
pub struct Signature {
pub name: String,
pub email: String,
pub timestamp: i64,
pub tz_offset_secs: i32,
}
#[derive(Debug, Clone)]
pub struct CommitObject {
pub hash: GitHash,
pub tree: GitHash,
pub parents: Vec<GitHash>,
pub author: Signature,
pub committer: Signature,
pub message: String,
pub is_signed: bool,
}
impl CommitObject {
pub fn parse(hash: GitHash, data: &[u8]) -> Result<Self> {
let text = std::str::from_utf8(data)
.map_err(|e| GitError::InvalidObject(format!("commit not valid UTF-8: {e}")))?;
let mut tree = None;
let mut parents = Vec::new();
let mut author = None;
let mut committer = None;
let mut is_signed = false;
let mut message_start = text.len();
for (i, line) in text.lines().enumerate() {
if line.is_empty() {
let byte_pos = text
.char_indices()
.filter(|(_, c)| *c == '\n')
.nth(i)
.map_or(text.len(), |(pos, _)| pos + 1);
message_start = byte_pos;
break;
}
if let Some(rest) = line.strip_prefix("tree ") {
tree = Some(GitHash::from_hex(rest.trim())?);
} else if let Some(rest) = line.strip_prefix("parent ") {
parents.push(GitHash::from_hex(rest.trim())?);
} else if let Some(rest) = line.strip_prefix("author ") {
author = Some(parse_signature(rest)?);
} else if let Some(rest) = line.strip_prefix("committer ") {
committer = Some(parse_signature(rest)?);
} else if line.strip_prefix("gpgsig ").is_some() {
is_signed = true;
}
}
Ok(Self {
hash,
tree: tree.ok_or_else(|| GitError::InvalidObject("commit missing tree".into()))?,
parents,
author: author
.ok_or_else(|| GitError::InvalidObject("commit missing author".into()))?,
committer: committer
.ok_or_else(|| GitError::InvalidObject("commit missing committer".into()))?,
message: text[message_start..].to_string(),
is_signed,
})
}
}
fn parse_signature(s: &str) -> Result<Signature> {
let err = || GitError::InvalidObject(format!("invalid signature: {s:?}"));
let email_end = s.rfind('>').ok_or_else(err)?;
let email_start = s.rfind('<').ok_or_else(err)?;
if email_start >= email_end {
return Err(err());
}
let name = s[..email_start].trim().to_string();
let email = s[email_start + 1..email_end].to_string();
let rest = s[email_end + 1..].trim();
let mut parts = rest.split_whitespace();
let ts: i64 = parts.next().and_then(|t| t.parse().ok()).ok_or_else(err)?;
let tz = parts.next().unwrap_or("+0000");
let sign = if tz.starts_with('-') { -1i32 } else { 1 };
let tz_digits = tz.trim_start_matches(['+', '-']);
let tz_offset_secs = if tz_digits.len() == 4 && tz_digits.is_ascii() {
let hh: i32 = tz_digits[..2].parse().unwrap_or(0);
let mm: i32 = tz_digits[2..].parse().unwrap_or(0);
sign * (hh * 3600 + mm * 60)
} else {
0
};
Ok(Signature {
name,
email,
timestamp: ts,
tz_offset_secs,
})
}
#[cfg(test)]
mod tests {
use super::parse_signature;
#[test]
fn tz_field_that_is_a_single_multibyte_char_does_not_panic() {
let sig = parse_signature("n <e> 0 \u{d168a}").expect("parses");
assert_eq!(sig.tz_offset_secs, 0, "malformed tz degrades to offset 0");
}
#[test]
fn ascii_tz_offset_still_parses() {
let sig = parse_signature("Ada <ada@x> 1136239445 +0530").expect("parses");
assert_eq!(sig.tz_offset_secs, 5 * 3600 + 30 * 60);
let neg = parse_signature("Ada <ada@x> 1136239445 -0800").expect("parses");
assert_eq!(neg.tz_offset_secs, -8 * 3600);
}
}