Skip to main content

grit_lib/objects/
commit_ref.rs

1//! Borrowed view of a commit object body ([`CommitRef`]) with on-demand decoding.
2//!
3//! [`CommitRef::parse`] is the single commit parser; [`super::parse_commit`] is
4//! `CommitRef::parse(data)?.to_owned()`.
5
6use std::borrow::Cow;
7
8use crate::commit_encoding;
9use crate::error::{Error, Result};
10use crate::ident::{
11    committer_timestamp_for_until_filter_bytes, committer_unix_seconds_for_ordering_bytes,
12};
13use smallvec::SmallVec;
14
15use super::{parse_oid_field, CommitData, HashAlgo, ObjectId};
16
17/// Parsed commit headers and message body borrowing from the object bytes.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct CommitRef<'a> {
20    headers_block: &'a [u8],
21    message: &'a [u8],
22    tree: ObjectId,
23    parents: SmallVec<[ObjectId; 2]>,
24    author_raw: Cow<'a, [u8]>,
25    committer_raw: Cow<'a, [u8]>,
26    encoding: Option<&'a [u8]>,
27}
28
29impl<'a> CommitRef<'a> {
30    /// Parse a commit object without allocating author, committer, or message strings.
31    ///
32    /// # Errors
33    ///
34    /// Returns [`Error::CorruptObject`] when required headers are missing or malformed.
35    pub fn parse(data: &'a [u8]) -> Result<Self> {
36        parse_one_pass(data)
37    }
38
39    /// Root tree object id.
40    #[must_use]
41    pub fn tree(&self) -> ObjectId {
42        self.tree
43    }
44
45    /// Parent commit ids in header order.
46    #[must_use]
47    pub fn parents(&self) -> &[ObjectId] {
48        &self.parents
49    }
50
51    /// Iterate parent commit ids.
52    pub fn parents_iter(&self) -> impl Iterator<Item = &ObjectId> + '_ {
53        self.parents.iter()
54    }
55
56    /// Raw `author` header payload (after `author `), with continuation lines merged.
57    #[must_use]
58    pub fn author_raw(&self) -> &[u8] {
59        &self.author_raw
60    }
61
62    /// Raw `committer` header payload (after `committer `), with continuation lines merged.
63    #[must_use]
64    pub fn committer_raw(&self) -> &[u8] {
65        &self.committer_raw
66    }
67
68    /// Optional `encoding` header value bytes when present and UTF-8.
69    #[must_use]
70    pub fn encoding_raw(&self) -> Option<&[u8]> {
71        self.encoding
72    }
73
74    /// Extra header bytes (`gpgsig`, `mergetag`, `change-id`, …) excluding `tree`/`parent`/`author`/`committer`/`encoding`.
75    #[must_use]
76    pub fn extra_headers(&self) -> Cow<'_, [u8]> {
77        collect_extra_header_bytes(self.headers_block)
78    }
79
80    /// Commit message body bytes (everything after the blank line).
81    #[must_use]
82    pub fn message_bytes(&self) -> &[u8] {
83        self.message
84    }
85
86    fn encoding_label(&self) -> Option<&str> {
87        self.encoding.and_then(|b| std::str::from_utf8(b).ok())
88    }
89
90    /// Decoded author identity line (uses [`Self::encoding_raw`] when set).
91    #[must_use]
92    pub fn author(&self) -> String {
93        commit_encoding::decode_bytes(self.encoding_label(), &self.author_raw)
94    }
95
96    /// Decoded committer identity line (uses [`Self::encoding_raw`] when set).
97    #[must_use]
98    pub fn committer(&self) -> String {
99        commit_encoding::decode_bytes(self.encoding_label(), &self.committer_raw)
100    }
101
102    /// Decoded commit message (uses [`Self::encoding_raw`] when set).
103    #[must_use]
104    pub fn message(&self) -> Cow<'_, str> {
105        decode_message(self.encoding.is_some(), self.encoding_label(), self.message)
106    }
107
108    /// Committer unix seconds for `--until` / `--since` filtering (unknown → `0`).
109    #[must_use]
110    pub fn committer_timestamp(&self) -> i64 {
111        committer_timestamp_for_until_filter_bytes(&self.committer_raw)
112    }
113
114    /// Committer unix seconds for rev-list date ordering (unknown → `0`).
115    #[must_use]
116    pub fn committer_unix_seconds_for_ordering(&self) -> i64 {
117        committer_unix_seconds_for_ordering_bytes(&self.committer_raw)
118    }
119
120    /// Author unix seconds for rev-list author-date ordering (unknown → `0`).
121    #[must_use]
122    pub fn author_unix_seconds_for_ordering(&self) -> i64 {
123        committer_unix_seconds_for_ordering_bytes(&self.author_raw)
124    }
125
126    /// Re-serialize using the original header block (byte-identical when the source object was valid).
127    #[must_use]
128    pub fn to_bytes(&self) -> Vec<u8> {
129        let mut out = Vec::with_capacity(self.headers_block.len() + 1 + self.message.len());
130        out.extend_from_slice(self.headers_block);
131        out.push(b'\n');
132        out.extend_from_slice(self.message);
133        out
134    }
135
136    /// Build an owning [`CommitData`] matching [`super::parse_commit`] output.
137    #[must_use]
138    pub fn to_owned(&self) -> CommitData {
139        let encoding = self.encoding_label().map(str::to_owned);
140        let message = self.message();
141        let body = self.message;
142        let has_non_utf8_encoding = encoding.as_deref().is_some_and(|label| {
143            !label.eq_ignore_ascii_case("utf-8") && !label.eq_ignore_ascii_case("utf8")
144        });
145        let raw_message = if body.is_empty() {
146            None
147        } else if has_non_utf8_encoding
148            || std::str::from_utf8(body).is_err()
149            || !body.ends_with(b"\n")
150        {
151            Some(body.to_vec())
152        } else {
153            None
154        };
155        CommitData {
156            tree: self.tree,
157            parents: self.parents.iter().copied().collect(),
158            author: self.author(),
159            committer: self.committer(),
160            author_raw: self.author_raw.to_vec(),
161            committer_raw: self.committer_raw.to_vec(),
162            encoding,
163            message: message.into_owned(),
164            raw_message,
165            extra_headers: self.extra_headers().into_owned(),
166        }
167    }
168}
169
170#[derive(Clone, Copy, PartialEq, Eq)]
171enum HeaderKind {
172    Tree,
173    Parent,
174    Author,
175    Committer,
176    Encoding,
177    Extra,
178    Malformed,
179}
180
181fn classify_header_line(line: &[u8]) -> HeaderKind {
182    if line.starts_with(b"tree ") {
183        HeaderKind::Tree
184    } else if line.starts_with(b"parent ") {
185        HeaderKind::Parent
186    } else if line.starts_with(b"author ") {
187        HeaderKind::Author
188    } else if line.starts_with(b"committer ") {
189        HeaderKind::Committer
190    } else if line.starts_with(b"encoding ") {
191        HeaderKind::Encoding
192    } else if line.is_empty() || line.first() == Some(&b' ') {
193        HeaderKind::Malformed
194    } else {
195        HeaderKind::Extra
196    }
197}
198
199fn header_value(line: &[u8], prefix_len: usize) -> &[u8] {
200    line.get(prefix_len..).unwrap_or_default()
201}
202
203fn parse_oid_fast(value: &[u8]) -> Result<ObjectId> {
204    if value.len() == HashAlgo::Sha1.hex_len() {
205        return ObjectId::from_hex_ascii(value);
206    }
207    parse_oid_field(value)
208}
209
210/// Header fields found by [`scan_commit`]; required ones may be absent in lenient mode.
211struct CommitScan<'a> {
212    headers_block: &'a [u8],
213    message: &'a [u8],
214    has_blank_line: bool,
215    tree: Option<ObjectId>,
216    parents: SmallVec<[ObjectId; 2]>,
217    author_raw: Option<Cow<'a, [u8]>>,
218    committer_raw: Option<Cow<'a, [u8]>>,
219    encoding: Option<&'a [u8]>,
220}
221
222/// One pass over the commit headers.
223///
224/// Strict mode rejects malformed header lines, unparsable ids, orphan continuations and a
225/// non-UTF-8 `encoding`. Lenient mode skips those lines instead, the way Git's display code
226/// reads whatever headers a commit does have.
227fn scan_commit(data: &[u8], lenient: bool) -> Result<CommitScan<'_>> {
228    #[derive(Clone, Copy)]
229    enum Continuation {
230        Author,
231        Committer,
232        MultilineExtra,
233        Ignore,
234    }
235
236    let mut pos = 0usize;
237    let mut tree = None;
238    let mut parents = SmallVec::<[ObjectId; 2]>::new();
239    let mut author_raw: Option<Cow<'_, [u8]>> = None;
240    let mut committer_raw: Option<Cow<'_, [u8]>> = None;
241    let mut encoding: Option<&[u8]> = None;
242    let mut cont = Continuation::Ignore;
243
244    let mut headers_end = 0usize;
245    let mut message: &[u8] = &[];
246    let mut has_blank_line = false;
247
248    while pos < data.len() {
249        let line_start = pos;
250        let mut line_end = pos;
251        while line_end < data.len() && data[line_end] != b'\n' {
252            line_end += 1;
253        }
254        let line = &data[line_start..line_end];
255        let after_nl = (line_end + 1).min(data.len());
256
257        if line.is_empty() {
258            headers_end = line_start;
259            message = data.get(after_nl..).unwrap_or_default();
260            has_blank_line = true;
261            break;
262        }
263
264        if line.first() == Some(&b' ') {
265            let rest = line.get(1..).unwrap_or_default();
266            let merged = match cont {
267                Continuation::Author => merge_identity_continuation(&mut author_raw, rest),
268                Continuation::Committer => merge_identity_continuation(&mut committer_raw, rest),
269                Continuation::MultilineExtra | Continuation::Ignore => Ok(()),
270            };
271            if !lenient {
272                merged?;
273            }
274            pos = after_nl;
275            continue;
276        }
277
278        match classify_header_line(line) {
279            HeaderKind::Tree => {
280                match parse_oid_fast(header_value(line, 5)) {
281                    Ok(oid) => tree = Some(oid),
282                    Err(err) if !lenient => return Err(err),
283                    Err(_) => {}
284                }
285                cont = Continuation::Ignore;
286            }
287            HeaderKind::Parent => {
288                match parse_oid_fast(header_value(line, 7)) {
289                    Ok(oid) => parents.push(oid),
290                    Err(err) if !lenient => return Err(err),
291                    Err(_) => {}
292                }
293                cont = Continuation::Ignore;
294            }
295            HeaderKind::Author => {
296                author_raw = Some(Cow::Borrowed(header_value(line, 7)));
297                cont = Continuation::Author;
298            }
299            HeaderKind::Committer => {
300                committer_raw = Some(Cow::Borrowed(header_value(line, 10)));
301                cont = Continuation::Committer;
302            }
303            HeaderKind::Encoding => {
304                let rest = header_value(line, 9);
305                if std::str::from_utf8(rest).is_ok() {
306                    encoding = Some(rest);
307                } else if !lenient {
308                    return Err(Error::CorruptObject("encoding is not UTF-8".to_owned()));
309                }
310                cont = Continuation::Ignore;
311            }
312            HeaderKind::Extra => {
313                cont = Continuation::MultilineExtra;
314            }
315            HeaderKind::Malformed => {
316                if !lenient {
317                    return Err(Error::CorruptObject(
318                        "malformed commit header line".to_owned(),
319                    ));
320                }
321                cont = Continuation::Ignore;
322            }
323        }
324        pos = after_nl;
325    }
326
327    if !has_blank_line {
328        headers_end = data.len();
329    }
330    Ok(CommitScan {
331        headers_block: &data[..headers_end],
332        message,
333        has_blank_line,
334        tree,
335        parents,
336        author_raw,
337        committer_raw,
338        encoding,
339    })
340}
341
342fn parse_one_pass<'a>(data: &'a [u8]) -> Result<CommitRef<'a>> {
343    let scan = scan_commit(data, false)?;
344    if !scan.has_blank_line {
345        return Err(Error::CorruptObject(
346            "commit missing blank line before message".to_owned(),
347        ));
348    }
349    let author_bytes = scan
350        .author_raw
351        .ok_or_else(|| Error::CorruptObject("commit missing author header".to_owned()))?;
352    let committer_bytes = scan
353        .committer_raw
354        .ok_or_else(|| Error::CorruptObject("commit missing committer header".to_owned()))?;
355
356    Ok(CommitRef {
357        headers_block: scan.headers_block,
358        message: scan.message,
359        tree: scan
360            .tree
361            .ok_or_else(|| Error::CorruptObject("commit missing tree header".to_owned()))?,
362        parents: scan.parents,
363        author_raw: author_bytes,
364        committer_raw: committer_bytes,
365        encoding: scan.encoding,
366    })
367}
368
369/// Author identity and message of a commit, read leniently.
370///
371/// Unlike [`CommitRef::parse`], a missing `author`/`committer` header, a malformed or
372/// unparsable header line, or a missing blank line does not fail: the author is `None` when
373/// absent and the message is empty without a blank line. Decoding (including `encoding`)
374/// matches [`CommitRef::author`] and [`CommitRef::message`], so well-formed commits yield the
375/// same strings as [`super::parse_commit`].
376pub(crate) fn commit_author_and_message_lenient(data: &[u8]) -> (Option<String>, String) {
377    let Ok(scan) = scan_commit(data, true) else {
378        return (None, String::new());
379    };
380    let label = scan.encoding.and_then(|b| std::str::from_utf8(b).ok());
381    let author = scan
382        .author_raw
383        .map(|raw| commit_encoding::decode_bytes(label, &raw));
384    let message = decode_message(scan.encoding.is_some(), label, scan.message).into_owned();
385    (author, message)
386}
387
388fn decode_message<'a>(has_encoding: bool, label: Option<&str>, body: &'a [u8]) -> Cow<'a, str> {
389    if has_encoding {
390        return Cow::Owned(commit_encoding::decode_bytes(label, body));
391    }
392    match std::str::from_utf8(body) {
393        Ok(s) => Cow::Borrowed(s),
394        Err(_) => Cow::Owned(commit_encoding::decode_bytes(None, body)),
395    }
396}
397
398fn collect_extra_header_bytes(headers_block: &[u8]) -> Cow<'_, [u8]> {
399    let mut extra = ExtraScan::default();
400    let mut pos = 0usize;
401    #[derive(Clone, Copy)]
402    enum Cont {
403        MultilineExtra,
404        Ignore,
405    }
406    let mut cont = Cont::Ignore;
407    while pos < headers_block.len() {
408        let line_start = pos;
409        let mut line_end = pos;
410        while line_end < headers_block.len() && headers_block[line_end] != b'\n' {
411            line_end += 1;
412        }
413        let line = &headers_block[line_start..line_end];
414        let after_nl = (line_end + 1).min(headers_block.len());
415        if line.first() == Some(&b' ') {
416            if matches!(cont, Cont::MultilineExtra) {
417                extra.push_line(line_start, after_nl);
418            }
419            pos = after_nl;
420            continue;
421        }
422        let kind = classify_header_line(line);
423        match kind {
424            HeaderKind::Tree | HeaderKind::Parent | HeaderKind::Author | HeaderKind::Committer => {
425                cont = Cont::Ignore;
426                extra.mark_structured_gap();
427            }
428            HeaderKind::Encoding => {
429                cont = Cont::Ignore;
430                extra.mark_structured_gap();
431            }
432            HeaderKind::Extra => {
433                cont = Cont::MultilineExtra;
434                extra.push_line(line_start, after_nl);
435            }
436            HeaderKind::Malformed => {
437                cont = Cont::Ignore;
438            }
439        }
440        pos = after_nl;
441    }
442    extra.finish(headers_block)
443}
444
445#[derive(Default)]
446struct ExtraScan {
447    start: Option<usize>,
448    end: usize,
449    structured_gap: bool,
450    piece_ranges: SmallVec<[(usize, usize); 4]>,
451}
452
453impl ExtraScan {
454    fn mark_structured_gap(&mut self) {
455        if self.start.is_some() {
456            self.structured_gap = true;
457        }
458    }
459
460    fn push_line(&mut self, line_start: usize, after_nl: usize) {
461        if self.start.is_none() {
462            self.start = Some(line_start);
463            self.end = after_nl;
464            return;
465        }
466        if !self.structured_gap && self.end == line_start {
467            self.end = after_nl;
468        } else {
469            self.structured_gap = true;
470            self.piece_ranges.push((line_start, after_nl));
471        }
472    }
473
474    fn finish<'a>(self, block: &'a [u8]) -> Cow<'a, [u8]> {
475        if self.start.is_none() && self.piece_ranges.is_empty() {
476            return Cow::Borrowed(&[]);
477        }
478        if self.structured_gap || !self.piece_ranges.is_empty() {
479            let mut out = Vec::new();
480            if let Some(start) = self.start {
481                out.extend_from_slice(&block[start..self.end]);
482            }
483            for &(from, to) in &self.piece_ranges {
484                out.extend_from_slice(&block[from..to]);
485            }
486            Cow::Owned(out)
487        } else if let Some(start) = self.start {
488            Cow::Borrowed(&block[start..self.end])
489        } else {
490            Cow::Borrowed(&[])
491        }
492    }
493}
494
495fn merge_identity_continuation(slot: &mut Option<Cow<'_, [u8]>>, rest: &[u8]) -> Result<()> {
496    let merged = slot
497        .as_mut()
498        .ok_or_else(|| Error::CorruptObject("orphan header continuation".to_owned()))?;
499    match merged {
500        Cow::Borrowed(first) => {
501            let mut buf = first.to_vec();
502            buf.extend_from_slice(rest);
503            *merged = Cow::Owned(buf);
504        }
505        Cow::Owned(buf) => buf.extend_from_slice(rest),
506    }
507    Ok(())
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513
514    #[test]
515    fn lenient_author_and_message_match_strict_parse_on_valid_commits() {
516        let latin1: &[u8] = b"tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n\
517            author J\xf6rg <j@example.com> 1 +0100\n\
518            committer C <c@example.com> 2 +0000\n\
519            encoding ISO-8859-1\n\
520            \n\
521            Gr\xfc\xdfe\n";
522        let continued: &[u8] = b"tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n\
523            author A\n Continued <a@example.com> 1 +0000\n\
524            committer C <c@example.com> 2 +0000\n\
525            gpgsig -----BEGIN-----\n sig\n -----END-----\n\
526            \n\
527            subject\n\nbody\n";
528        for data in [latin1, continued] {
529            let strict = CommitRef::parse(data).expect("valid commit");
530            let (author, message) = commit_author_and_message_lenient(data);
531            assert_eq!(author.as_deref(), Some(strict.author().as_str()));
532            assert_eq!(message, strict.message());
533        }
534    }
535
536    #[test]
537    fn lenient_author_and_message_tolerate_missing_and_malformed_headers() {
538        let tree = "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n";
539        let no_committer = format!("{tree}author A <a@x> 1700000000 +0000\n\nmessage\n");
540        assert!(CommitRef::parse(no_committer.as_bytes()).is_err());
541        assert_eq!(
542            commit_author_and_message_lenient(no_committer.as_bytes()),
543            (
544                Some("A <a@x> 1700000000 +0000".to_owned()),
545                "message\n".to_owned()
546            )
547        );
548
549        let no_author = format!("{tree}committer C <c@x> 2 +0000\n\n\nsubject\n");
550        assert!(CommitRef::parse(no_author.as_bytes()).is_err());
551        assert_eq!(
552            commit_author_and_message_lenient(no_author.as_bytes()),
553            (None, "\nsubject\n".to_owned())
554        );
555
556        let malformed =
557            format!("{tree}parent not-an-oid\n\n bad continuation\nauthor A <a@x> 1 +0000\n\nm\n");
558        assert_eq!(
559            commit_author_and_message_lenient(malformed.as_bytes()).1,
560            " bad continuation\nauthor A <a@x> 1 +0000\n\nm\n",
561            "the first blank line still ends the headers"
562        );
563
564        let mut odd_lines = tree.as_bytes().to_vec();
565        odd_lines.extend_from_slice(b"encoding \xff\n orphan\nauthor A <a@x> 1 +0000\n\nm\n");
566        assert!(CommitRef::parse(&odd_lines).is_err());
567        let (author, message) = commit_author_and_message_lenient(&odd_lines);
568        assert_eq!(author.as_deref(), Some("A <a@x> 1 +0000"));
569        assert_eq!(message, "m\n");
570
571        let no_blank = format!("{tree}author A <a@x> 1 +0000\ncommitter C <c@x> 2 +0000\n");
572        assert!(CommitRef::parse(no_blank.as_bytes()).is_err());
573        assert_eq!(
574            commit_author_and_message_lenient(no_blank.as_bytes()),
575            (Some("A <a@x> 1 +0000".to_owned()), String::new())
576        );
577        assert_eq!(
578            commit_author_and_message_lenient(b""),
579            (None, String::new())
580        );
581    }
582
583    #[test]
584    fn commit_ref_roundtrip_vectors() {
585        let cases: &[&[u8]] = &[
586            concat!(
587                "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
588                "parent 1111111111111111111111111111111111111111\n",
589                "author A <a@example.com> 1 +0000\n",
590                "committer C <c@example.com> 2 +0000\n",
591                "\n",
592                "ignored body\n",
593            )
594            .as_bytes(),
595            concat!(
596                "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
597                "author A U Thor <author@example.com> 1 +0000\n",
598                "committer C O Mitter <committer@example.com> 1 +0000\n",
599                "gpgsig -----BEGIN PGP SIGNATURE-----\n",
600                " abcdef\n",
601                " -----END PGP SIGNATURE-----\n",
602                "\n",
603                "msg\n",
604            )
605            .as_bytes(),
606            concat!(
607                "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
608                "author A <a@example.com> 1 +0000\n",
609                "committer C <c@example.com> 1 +0000\n",
610                "encoding ISO-8859-1\n",
611                "gpgsig -----BEGIN PGP SIGNATURE-----\n",
612                " line2\n",
613                " -----END PGP SIGNATURE-----\n",
614                "mergetag object 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
615                " type commit\n",
616                " tag merge-tag\n",
617                " tagger T <t@example.com> 1 +0000\n",
618                " -----BEGIN PGP SIGNATURE-----\n",
619                " inner\n",
620                " -----END PGP SIGNATURE-----\n",
621                "change-id test-change-id\n",
622                "\n",
623                "msg-only\n",
624            )
625            .as_bytes(),
626            concat!(
627                "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
628                "author A <a@example.com> 1 +0000\n",
629                "committer C <c@example.com> 1 +0000\n",
630                "change-id interleaved-change\n",
631                "encoding UTF-8\n",
632                "gpgsig -----BEGIN PGP SIGNATURE-----\n",
633                " sig\n",
634                " -----END PGP SIGNATURE-----\n",
635                "\n",
636                "body\n",
637            )
638            .as_bytes(),
639            b"tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n\
640author A <a@example.com> 1 +0000\n\
641committer C <c@example.com> 1 +0000\n\
642\n\
643no-final-newline",
644        ];
645        for raw in cases {
646            let parsed = super::super::parse_commit(raw).expect("parse_commit");
647            let cref = CommitRef::parse(raw).expect("CommitRef::parse");
648            assert_eq!(cref.to_owned(), parsed, "to_owned mismatch");
649            assert_eq!(cref.to_bytes(), *raw, "byte-identical serialize");
650        }
651    }
652
653    fn median(mut samples: Vec<u128>) -> u128 {
654        samples.sort_unstable();
655        samples[samples.len() / 2]
656    }
657
658    fn bench_commit_bodies(count: usize) -> Vec<Vec<u8>> {
659        let empty = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
660        let ident = "Bench <b@example.com> 1700000000 +0000";
661        let mut bodies = Vec::with_capacity(count);
662        let mut parent = empty.to_owned();
663        for i in 0..count {
664            let body = format!(
665                "tree {empty}\nparent {parent}\nauthor {ident}\ncommitter {ident}\nchange-id bench-{i:06}\n\nm{i}\n"
666            );
667            parent = format!("{:040x}", i + 1);
668            bodies.push(body.into_bytes());
669        }
670        bodies
671    }
672
673    #[test]
674    fn commit_ref_parse_at_least_2x_faster_than_parse_commit_100k() {
675        if cfg!(debug_assertions) {
676            return;
677        }
678        let bodies = bench_commit_bodies(100_000);
679        for body in &bodies[..128] {
680            let _ = CommitRef::parse(body).expect("parse");
681            let _ = super::super::parse_commit(body).expect("parse_commit");
682        }
683
684        let mut parse_commit_medians = Vec::with_capacity(9);
685        let mut commit_ref_medians = Vec::with_capacity(9);
686        for run in 0..9 {
687            let use_commit_ref = run % 2 == 0;
688            let start = std::time::Instant::now();
689            if use_commit_ref {
690                for body in &bodies {
691                    std::hint::black_box(CommitRef::parse(body).expect("parse"));
692                }
693            } else {
694                for body in &bodies {
695                    std::hint::black_box(super::super::parse_commit(body).expect("parse_commit"));
696                }
697            }
698            let elapsed = start.elapsed().as_nanos();
699            if use_commit_ref {
700                commit_ref_medians.push(elapsed);
701            } else {
702                parse_commit_medians.push(elapsed);
703            }
704        }
705        let parse_commit_median = median(parse_commit_medians);
706        let commit_ref_median = median(commit_ref_medians);
707        let ratio = parse_commit_median as f64 / commit_ref_median as f64;
708        assert!(
709            ratio >= 2.0,
710            "CommitRef::parse median {commit_ref_median}ns vs parse_commit {parse_commit_median}ns (ratio {ratio:.2}x, need >=2x)"
711        );
712    }
713}