Skip to main content

git_async/object/
commit.rs

1use crate::{
2    error::GResult,
3    file_system::FileSystem,
4    object::{
5        Object, ObjectId, Tree,
6        header::{ObjectHeaderIter, RangeObjectHeader},
7        parse_author_committer_tagger,
8    },
9    parsing::ParseError,
10    repo::Repo,
11    subslice_range::SubsliceRange,
12};
13use accessory::Accessors;
14use alloc::vec::Vec;
15use chrono::{DateTime, FixedOffset};
16use core::ops::Range;
17use nom::{Parser, combinator::all_consuming};
18
19/// A commit object
20#[derive(Accessors, Clone)]
21pub struct Commit {
22    /// The [`ObjectId`] of the commit
23    #[access(get(cp))]
24    id: ObjectId,
25
26    /// The raw data in the object
27    #[access(get(ty(&[u8])))]
28    body: Vec<u8>,
29
30    /// The [`ObjectId`] of the tree that the commit points to
31    #[access(get(cp))]
32    tree: ObjectId,
33
34    /// The [`ObjectId`]s of all of the parents of the commit
35    #[access(get(ty(&[ObjectId])))]
36    parents: Vec<ObjectId>,
37
38    author_name: Range<usize>,
39    author_email: Range<usize>,
40    committer_name: Range<usize>,
41    committer_email: Range<usize>,
42    message: Range<usize>,
43
44    /// The author date of the commit
45    #[access(get(cp))]
46    author_date: DateTime<FixedOffset>,
47
48    /// The commit date of the commit
49    #[expect(clippy::struct_field_names)]
50    #[access(get(cp))]
51    commit_date: DateTime<FixedOffset>,
52
53    additional_headers: Vec<RangeObjectHeader>,
54}
55
56impl PartialEq for Commit {
57    fn eq(&self, other: &Self) -> bool {
58        self.id == other.id
59    }
60}
61impl Eq for Commit {}
62impl PartialOrd for Commit {
63    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
64        Some(self.cmp(other))
65    }
66}
67impl Ord for Commit {
68    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
69        self.id.cmp(&other.id)
70    }
71}
72
73impl Commit {
74    /// The name of the commit author
75    pub fn author_name(&self) -> &[u8] {
76        &self.body[self.author_name.clone()]
77    }
78
79    /// The email address of the commit author
80    pub fn author_email(&self) -> &[u8] {
81        &self.body[self.author_email.clone()]
82    }
83
84    /// The name of the committer
85    pub fn committer_name(&self) -> &[u8] {
86        &self.body[self.committer_name.clone()]
87    }
88
89    /// The email address of the committer
90    pub fn committer_email(&self) -> &[u8] {
91        &self.body[self.committer_email.clone()]
92    }
93
94    /// The commit message
95    pub fn message(&self) -> &[u8] {
96        &self.body[self.message.clone()]
97    }
98
99    /// Get an iterator over any additional headers in the commit.
100    ///
101    /// Additional headers are those not parsed by `git-async`, e.g. `mergetag`.
102    pub fn additional_headers(&self) -> ObjectHeaderIter<'_> {
103        ObjectHeaderIter::new(&self.body, &self.additional_headers)
104    }
105
106    /// Wrap the [`Commit`] as a generic [`Object`].
107    pub fn as_object(self) -> Object {
108        Object::Commit(self)
109    }
110
111    /// Look up the tree that the commit points to, using the provided [`Repo`].
112    pub async fn lookup_tree<F: FileSystem>(&self, repo: &Repo<F>) -> GResult<Tree> {
113        Ok(repo.lookup_object(self.tree).await?.tree()?)
114    }
115
116    /// Look up all the parents of the commit, using the provided [`Repo`].
117    pub async fn lookup_parents<F: FileSystem>(&self, repo: &Repo<F>) -> GResult<Vec<Commit>> {
118        let mut out = Vec::with_capacity(self.parents.len());
119        for parent in &self.parents {
120            out.push(repo.lookup_object(*parent).await?.commit()?);
121        }
122        Ok(out)
123    }
124
125    pub(crate) fn parse(id: ObjectId, body: Vec<u8>) -> Result<Self, ParseError> {
126        fn f<T>(val: Option<T>) -> Result<T, ParseError> {
127            val.ok_or(ParseError::MissingFields)
128        }
129        let (message, headers) = RangeObjectHeader::parser(&body)?;
130        let mut tree: Option<ObjectId> = None;
131        let mut parents: Vec<ObjectId> = Vec::new();
132        let mut author_name: Option<&[u8]> = None;
133        let mut author_email: Option<&[u8]> = None;
134        let mut author_date: Option<DateTime<FixedOffset>> = None;
135        let mut committer_name: Option<&[u8]> = None;
136        let mut committer_email: Option<&[u8]> = None;
137        let mut commit_date: Option<DateTime<FixedOffset>> = None;
138        let mut additional_headers: Vec<RangeObjectHeader> = Vec::new();
139        for (range_header, header) in headers
140            .iter()
141            .zip(ObjectHeaderIter::new(&body, headers.as_slice()))
142        {
143            match header.name() {
144                b"tree" => {
145                    let (_, object_id) = all_consuming(ObjectId::parse).parse(header.value())?;
146                    tree = Some(object_id);
147                }
148                b"parent" => {
149                    let (_, object_id) = all_consuming(ObjectId::parse).parse(header.value())?;
150                    parents.push(object_id);
151                }
152                b"author" => {
153                    let (_, (name, email, date)) =
154                        all_consuming(parse_author_committer_tagger).parse(header.value())?;
155                    author_name = Some(name);
156                    author_email = Some(email);
157                    author_date = Some(date);
158                }
159                b"committer" => {
160                    let (_, (name, email, date)) =
161                        all_consuming(parse_author_committer_tagger).parse(header.value())?;
162                    committer_name = Some(name);
163                    committer_email = Some(email);
164                    commit_date = Some(date);
165                }
166                _ => {
167                    additional_headers.push(range_header.clone());
168                }
169            }
170        }
171        Ok(Self {
172            id,
173            message: body.subslice_range_stable(message).unwrap(),
174            tree: f(tree)?,
175            parents,
176            author_name: body.subslice_range_stable(f(author_name)?).unwrap(),
177            author_email: body.subslice_range_stable(f(author_email)?).unwrap(),
178            author_date: f(author_date)?,
179            committer_name: body.subslice_range_stable(f(committer_name)?).unwrap(),
180            committer_email: body.subslice_range_stable(f(committer_email)?).unwrap(),
181            commit_date: f(commit_date)?,
182            additional_headers,
183            body,
184        })
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use hex_literal::hex;
192
193    const ZERO_OID: ObjectId = ObjectId::from_bytes([0; 20]);
194
195    #[test]
196    fn parse_root_commit() {
197        let data = b"tree 3a4df67dd7fd7cb3ca82d9896dbdd28053d39bdb
198author a-user <an-email-address> 1774735018 +0530
199committer another-user <another-email-address> 1774735019 -0800
200
201a commit
202";
203        let commit = Commit::parse(ZERO_OID, data.to_vec()).unwrap();
204        assert!(commit.parents.is_empty());
205        assert_eq!(
206            commit.tree,
207            ObjectId::from_bytes(hex!("3a4df67dd7fd7cb3ca82d9896dbdd28053d39bdb"),)
208        );
209        assert_eq!(str::from_utf8(commit.author_name()).unwrap(), "a-user");
210        assert_eq!(
211            str::from_utf8(commit.author_email()).unwrap(),
212            "an-email-address"
213        );
214        assert_eq!(
215            commit.author_date,
216            DateTime::parse_from_rfc3339("2026-03-29T03:26:58+05:30").unwrap()
217        );
218        assert_eq!(
219            str::from_utf8(commit.committer_name()).unwrap(),
220            "another-user"
221        );
222        assert_eq!(
223            str::from_utf8(commit.committer_email()).unwrap(),
224            "another-email-address"
225        );
226        assert_eq!(
227            commit.commit_date,
228            DateTime::parse_from_rfc3339("2026-03-28T13:56:59-08:00").unwrap()
229        );
230        assert_eq!(str::from_utf8(commit.message()).unwrap(), "a commit\n");
231    }
232
233    #[test]
234    fn parse_normal_commit() {
235        let data = b"tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
236parent 16dafd3d0ba5af72f035d641c076a4150eda548d
237author a-user <an-email-address> 1774739676 +0000
238committer a-user <an-email-address> 1774739676 +0000
239
240another commit
241";
242        let commit = Commit::parse(ZERO_OID, data.to_vec()).unwrap();
243        assert_eq!(
244            &commit.parents,
245            &[ObjectId::from_bytes(hex!(
246                "16dafd3d0ba5af72f035d641c076a4150eda548d"
247            ),)]
248        );
249    }
250
251    #[test]
252    fn parse_merge_commit() {
253        let data = b"tree bfb6d701e108f3be27395bd60c3417b47ffbe7d9
254parent f625376d12f2edc71cff70bb42d387ddf2408460
255parent 6904799d30a34bfcf6ca6a3526fc8b771ed6705c
256author a-user <an-email-address> 1774740069 +0000
257committer a-user <an-email-address> 1774740069 +0000
258
259Merge branch 'branch'
260";
261        let commit = Commit::parse(ZERO_OID, data.to_vec()).unwrap();
262        assert_eq!(commit.parents.len(), 2);
263    }
264
265    #[test]
266    fn parse_commit_additional_headers() {
267        let data = b"tree bfb6d701e108f3be27395bd60c3417b47ffbe7d9
268parent f625376d12f2edc71cff70bb42d387ddf2408460
269author a-user <an-email-address> 1774740069 +0000
270committer a-user <an-email-address> 1774740069 +0000
271some-header a value
272some-other-header a long line-wrapped
273  value
274
275the commit message
276";
277        let commit = Commit::parse(ZERO_OID, data.to_vec()).unwrap();
278        let expected = [
279            (b"some-header".as_slice(), b"a value".as_slice()),
280            (
281                b"some-other-header".as_slice(),
282                b"a long line-wrapped\n  value".as_slice(),
283            ),
284        ];
285        let iter = commit.additional_headers();
286        assert_eq!(iter.len(), 2);
287        for (received, (expected_name, expected_value)) in iter.zip(expected.into_iter()) {
288            assert_eq!(received.name(), expected_name);
289            assert_eq!(received.value(), expected_value);
290        }
291    }
292}