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