1use crate::error::{GitError, Result};
2use crate::hash::GitHash;
3
4#[derive(Debug, Clone)]
5pub struct Signature {
6 pub name: String,
7 pub email: String,
8 pub timestamp: i64,
10 pub tz_offset_secs: i32,
12}
13
14#[derive(Debug, Clone)]
15pub struct CommitObject {
16 pub hash: GitHash,
17 pub tree: GitHash,
18 pub parents: Vec<GitHash>,
19 pub author: Signature,
20 pub committer: Signature,
21 pub message: String,
22 pub is_signed: bool,
26}
27
28impl CommitObject {
29 pub fn parse(hash: GitHash, data: &[u8]) -> Result<Self> {
31 let text = std::str::from_utf8(data)
32 .map_err(|e| GitError::InvalidObject(format!("commit not valid UTF-8: {e}")))?;
33
34 let mut tree = None;
35 let mut parents = Vec::new();
36 let mut author = None;
37 let mut committer = None;
38 let mut is_signed = false;
39 let mut message_start = text.len();
40
41 for (i, line) in text.lines().enumerate() {
42 if line.is_empty() {
43 let byte_pos = text
45 .char_indices()
46 .filter(|(_, c)| *c == '\n')
47 .nth(i)
48 .map_or(text.len(), |(pos, _)| pos + 1);
49 message_start = byte_pos;
50 break;
51 }
52 if let Some(rest) = line.strip_prefix("tree ") {
53 tree = Some(GitHash::from_hex(rest.trim())?);
54 } else if let Some(rest) = line.strip_prefix("parent ") {
55 parents.push(GitHash::from_hex(rest.trim())?);
56 } else if let Some(rest) = line.strip_prefix("author ") {
57 author = Some(parse_signature(rest)?);
58 } else if let Some(rest) = line.strip_prefix("committer ") {
59 committer = Some(parse_signature(rest)?);
60 } else if line.strip_prefix("gpgsig ").is_some() {
61 is_signed = true;
66 }
67 }
68
69 Ok(Self {
70 hash,
71 tree: tree.ok_or_else(|| GitError::InvalidObject("commit missing tree".into()))?,
72 parents,
73 author: author
74 .ok_or_else(|| GitError::InvalidObject("commit missing author".into()))?,
75 committer: committer
76 .ok_or_else(|| GitError::InvalidObject("commit missing committer".into()))?,
77 message: text[message_start..].to_string(),
78 is_signed,
79 })
80 }
81}
82
83fn parse_signature(s: &str) -> Result<Signature> {
84 let err = || GitError::InvalidObject(format!("invalid signature: {s:?}"));
86
87 let email_end = s.rfind('>').ok_or_else(err)?;
88 let email_start = s.rfind('<').ok_or_else(err)?;
89 if email_start >= email_end {
90 return Err(err());
91 }
92 let name = s[..email_start].trim().to_string();
93 let email = s[email_start + 1..email_end].to_string();
94
95 let rest = s[email_end + 1..].trim();
96 let mut parts = rest.split_whitespace();
97 let ts: i64 = parts.next().and_then(|t| t.parse().ok()).ok_or_else(err)?;
98 let tz = parts.next().unwrap_or("+0000");
99
100 let sign = if tz.starts_with('-') { -1i32 } else { 1 };
101 let tz_digits = tz.trim_start_matches(['+', '-']);
102 let tz_offset_secs = if tz_digits.len() == 4 {
103 let hh: i32 = tz_digits[..2].parse().unwrap_or(0);
104 let mm: i32 = tz_digits[2..].parse().unwrap_or(0);
105 sign * (hh * 3600 + mm * 60)
106 } else {
107 0
108 };
109
110 Ok(Signature {
111 name,
112 email,
113 timestamp: ts,
114 tz_offset_secs,
115 })
116}