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