git_async/object/
commit.rs1use 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#[derive(Accessors, Clone)]
21pub struct Commit {
22 #[access(get(cp))]
24 id: ObjectId,
25
26 #[access(get(ty(&[u8])))]
28 body: Vec<u8>,
29
30 #[access(get(cp))]
32 tree: ObjectId,
33
34 #[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 #[access(get(cp))]
46 author_date: DateTime<FixedOffset>,
47
48 #[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 pub fn author_name(&self) -> &[u8] {
76 &self.body[self.author_name.clone()]
77 }
78
79 pub fn author_email(&self) -> &[u8] {
81 &self.body[self.author_email.clone()]
82 }
83
84 pub fn committer_name(&self) -> &[u8] {
86 &self.body[self.committer_name.clone()]
87 }
88
89 pub fn committer_email(&self) -> &[u8] {
91 &self.body[self.committer_email.clone()]
92 }
93
94 pub fn message(&self) -> &[u8] {
96 &self.body[self.message.clone()]
97 }
98
99 pub fn additional_headers(&self) -> ObjectHeaderIter<'_> {
103 ObjectHeaderIter::new(&self.body, &self.additional_headers)
104 }
105
106 pub fn as_object(self) -> Object {
108 Object::Commit(self)
109 }
110
111 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 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}