diff options
Diffstat (limited to 'vendor/gix-object/src/commit')
-rw-r--r-- | vendor/gix-object/src/commit/decode.rs | 116 | ||||
-rw-r--r-- | vendor/gix-object/src/commit/message/body.rs | 43 | ||||
-rw-r--r-- | vendor/gix-object/src/commit/message/decode.rs | 70 | ||||
-rw-r--r-- | vendor/gix-object/src/commit/mod.rs | 45 | ||||
-rw-r--r-- | vendor/gix-object/src/commit/ref_iter.rs | 155 | ||||
-rw-r--r-- | vendor/gix-object/src/commit/write.rs | 4 |
6 files changed, 274 insertions, 159 deletions
diff --git a/vendor/gix-object/src/commit/decode.rs b/vendor/gix-object/src/commit/decode.rs index 821feaabb..0b8243ef3 100644 --- a/vendor/gix-object/src/commit/decode.rs +++ b/vendor/gix-object/src/commit/decode.rs @@ -1,71 +1,71 @@ use std::borrow::Cow; -use nom::{ - branch::alt, - bytes::complete::{is_not, tag}, - combinator::{all_consuming, opt}, - error::{context, ContextError, ParseError}, - multi::many0, - IResult, Parser, -}; use smallvec::SmallVec; +use winnow::{ + combinator::{alt, eof, opt, preceded, repeat, rest, terminated}, + error::{AddContext, ParserError, StrContext}, + prelude::*, + token::take_till1, +}; use crate::{parse, parse::NL, BStr, ByteSlice, CommitRef}; -pub fn message<'a, E: ParseError<&'a [u8]> + ContextError<&'a [u8]>>(i: &'a [u8]) -> IResult<&'a [u8], &'a BStr, E> { +pub fn message<'a, E: ParserError<&'a [u8]> + AddContext<&'a [u8], StrContext>>( + i: &mut &'a [u8], +) -> PResult<&'a BStr, E> { if i.is_empty() { // newline + [message] - return Err(nom::Err::Error(E::add_context( - i, - "newline + <message>", - E::from_error_kind(i, nom::error::ErrorKind::Eof), - ))); + return Err( + winnow::error::ErrMode::from_error_kind(i, winnow::error::ErrorKind::Eof) + .add_context(i, StrContext::Expected("newline + <message>".into())), + ); } - let (i, _) = context("a newline separates headers from the message", tag(NL))(i)?; - Ok((&[], i.as_bstr())) + preceded(NL, rest.map(ByteSlice::as_bstr)) + .context(StrContext::Expected( + "a newline separates headers from the message".into(), + )) + .parse_next(i) } -pub fn commit<'a, E: ParseError<&'a [u8]> + ContextError<&'a [u8]>>( - i: &'a [u8], -) -> IResult<&'a [u8], CommitRef<'_>, E> { - let (i, tree) = context("tree <40 lowercase hex char>", |i| { - parse::header_field(i, b"tree", parse::hex_hash) - })(i)?; - let (i, parents) = context( - "zero or more 'parent <40 lowercase hex char>'", - many0(|i| parse::header_field(i, b"parent", parse::hex_hash)), - )(i)?; - let (i, author) = context("author <signature>", |i| { - parse::header_field(i, b"author", parse::signature) - })(i)?; - let (i, committer) = context("committer <signature>", |i| { - parse::header_field(i, b"committer", parse::signature) - })(i)?; - let (i, encoding) = context( - "encoding <encoding>", - opt(|i| parse::header_field(i, b"encoding", is_not(NL))), - )(i)?; - let (i, extra_headers) = context( - "<field> <single-line|multi-line>", - many0(alt(( - parse::any_header_field_multi_line.map(|(k, o)| (k.as_bstr(), Cow::Owned(o))), - |i| { - parse::any_header_field(i, is_not(NL)).map(|(i, (k, o))| (i, (k.as_bstr(), Cow::Borrowed(o.as_bstr())))) +pub fn commit<'a, E: ParserError<&'a [u8]> + AddContext<&'a [u8], StrContext>>( + i: &mut &'a [u8], +) -> PResult<CommitRef<'a>, E> { + ( + (|i: &mut _| parse::header_field(i, b"tree", parse::hex_hash)) + .context(StrContext::Expected("tree <40 lowercase hex char>".into())), + repeat(0.., |i: &mut _| parse::header_field(i, b"parent", parse::hex_hash)) + .map(|p: Vec<_>| p) + .context(StrContext::Expected( + "zero or more 'parent <40 lowercase hex char>'".into(), + )), + (|i: &mut _| parse::header_field(i, b"author", parse::signature)) + .context(StrContext::Expected("author <signature>".into())), + (|i: &mut _| parse::header_field(i, b"committer", parse::signature)) + .context(StrContext::Expected("committer <signature>".into())), + opt(|i: &mut _| parse::header_field(i, b"encoding", take_till1(NL))) + .context(StrContext::Expected("encoding <encoding>".into())), + repeat( + 0.., + alt(( + parse::any_header_field_multi_line.map(|(k, o)| (k.as_bstr(), Cow::Owned(o))), + |i: &mut _| { + parse::any_header_field(i, take_till1(NL)).map(|(k, o)| (k.as_bstr(), Cow::Borrowed(o.as_bstr()))) + }, + )), + ) + .context(StrContext::Expected("<field> <single-line|multi-line>".into())), + terminated(message, eof), + ) + .map( + |(tree, parents, author, committer, encoding, extra_headers, message)| CommitRef { + tree, + parents: SmallVec::from(parents), + author, + committer, + encoding: encoding.map(ByteSlice::as_bstr), + message, + extra_headers, }, - ))), - )(i)?; - let (i, message) = all_consuming(message)(i)?; - - Ok(( - i, - CommitRef { - tree, - parents: SmallVec::from(parents), - author, - committer, - encoding: encoding.map(ByteSlice::as_bstr), - message, - extra_headers, - }, - )) + ) + .parse_next(i) } diff --git a/vendor/gix-object/src/commit/message/body.rs b/vendor/gix-object/src/commit/message/body.rs index 855f031be..6301bf30e 100644 --- a/vendor/gix-object/src/commit/message/body.rs +++ b/vendor/gix-object/src/commit/message/body.rs @@ -1,11 +1,10 @@ use std::ops::Deref; -use nom::{ - bytes::complete::{tag, take_until1}, - combinator::all_consuming, - error::{ErrorKind, ParseError}, - sequence::terminated, - IResult, +use winnow::{ + combinator::{eof, rest, separated_pair, terminated}, + error::{ErrorKind, ParserError}, + prelude::*, + token::take_until1, }; use crate::{ @@ -32,12 +31,14 @@ pub struct TrailerRef<'a> { pub value: &'a BStr, } -fn parse_single_line_trailer<'a, E: ParseError<&'a [u8]>>(i: &'a [u8]) -> IResult<&'a [u8], (&'a BStr, &'a BStr), E> { - let (value, token) = terminated(take_until1(b":".as_ref()), tag(b": "))(i.trim_end())?; +fn parse_single_line_trailer<'a, E: ParserError<&'a [u8]>>(i: &mut &'a [u8]) -> PResult<(&'a BStr, &'a BStr), E> { + *i = i.trim_end(); + let (token, value) = separated_pair(take_until1(b":".as_ref()), b": ", rest).parse_next(i)?; + if token.trim_end().len() != token.len() || value.trim_start().len() != value.len() { - Err(nom::Err::Failure(E::from_error_kind(i, ErrorKind::Fail))) + Err(winnow::error::ErrMode::from_error_kind(i, ErrorKind::Fail).cut()) } else { - Ok((&[], (token.as_bstr(), value.as_bstr()))) + Ok((token.as_bstr(), value.as_bstr())) } } @@ -48,15 +49,15 @@ impl<'a> Iterator for Trailers<'a> { if self.cursor.is_empty() { return None; } - for line in self.cursor.lines_with_terminator() { + for mut line in self.cursor.lines_with_terminator() { self.cursor = &self.cursor[line.len()..]; - if let Some(trailer) = - all_consuming(parse_single_line_trailer::<()>)(line) - .ok() - .map(|(_, (token, value))| TrailerRef { - token: token.trim().as_bstr(), - value: value.trim().as_bstr(), - }) + if let Some(trailer) = terminated(parse_single_line_trailer::<()>, eof) + .parse_next(&mut line) + .ok() + .map(|(token, value)| TrailerRef { + token: token.trim().as_bstr(), + value: value.trim().as_bstr(), + }) { return Some(trailer); } @@ -118,7 +119,7 @@ mod test_parse_trailer { use super::*; fn parse(input: &str) -> (&BStr, &BStr) { - parse_single_line_trailer::<()>(input.as_bytes()).unwrap().1 + parse_single_line_trailer::<()>.parse_peek(input.as_bytes()).unwrap().1 } #[test] @@ -141,8 +142,8 @@ mod test_parse_trailer { #[test] fn extra_whitespace_before_token_or_value_is_error() { - assert!(parse_single_line_trailer::<()>(b"foo : bar").is_err()); - assert!(parse_single_line_trailer::<()>(b"foo: bar").is_err()) + assert!(parse_single_line_trailer::<()>.parse_peek(b"foo : bar").is_err()); + assert!(parse_single_line_trailer::<()>.parse_peek(b"foo: bar").is_err()) } #[test] diff --git a/vendor/gix-object/src/commit/message/decode.rs b/vendor/gix-object/src/commit/message/decode.rs index 6224909bd..8038009b4 100644 --- a/vendor/gix-object/src/commit/message/decode.rs +++ b/vendor/gix-object/src/commit/message/decode.rs @@ -1,57 +1,49 @@ -use nom::{ - branch::alt, - bytes::complete::{tag, take_till1}, - combinator::all_consuming, - error::ParseError, - sequence::pair, - IResult, +use winnow::{ + combinator::{alt, eof, preceded, rest, terminated}, + error::ParserError, + prelude::*, + stream::{Offset, Stream}, + token::take_till1, }; use crate::bstr::{BStr, ByteSlice}; -pub(crate) fn newline<'a, E: ParseError<&'a [u8]>>(i: &'a [u8]) -> IResult<&'a [u8], &'a [u8], E> { - alt((tag(b"\r\n"), tag(b"\n")))(i) +pub(crate) fn newline<'a, E: ParserError<&'a [u8]>>(i: &mut &'a [u8]) -> PResult<&'a [u8], E> { + alt((b"\n", b"\r\n")).parse_next(i) } -fn subject_and_body<'a, E: ParseError<&'a [u8]>>(i: &'a [u8]) -> IResult<&'a [u8], (&'a BStr, Option<&'a BStr>), E> { - let mut c = i; - let mut consumed_bytes = 0; - while !c.is_empty() { - c = match take_till1::<_, _, E>(|c| c == b'\n' || c == b'\r')(c) { - Ok((i1, segment)) => { - consumed_bytes += segment.len(); - match pair::<_, _, _, E, _, _>(newline, newline)(i1) { - Ok((body, _)) => { - return Ok(( - &[], - ( - i[0usize..consumed_bytes].as_bstr(), - (!body.is_empty()).then(|| body.as_bstr()), - ), - )); +fn subject_and_body<'a, E: ParserError<&'a [u8]>>(i: &mut &'a [u8]) -> PResult<(&'a BStr, Option<&'a BStr>), E> { + let start_i = *i; + let start = i.checkpoint(); + while !i.is_empty() { + match take_till1::<_, _, E>(|c| c == b'\n' || c == b'\r').parse_next(i) { + Ok(_) => { + let consumed_bytes = i.offset_from(&start); + match preceded((newline::<E>, newline::<E>), rest).parse_next(i) { + Ok(body) => { + let body = (!body.is_empty()).then(|| body.as_bstr()); + return Ok((start_i[0usize..consumed_bytes].as_bstr(), body)); } - Err(_) => match i1.get(1..) { - Some(next) => { - consumed_bytes += 1; - next - } + Err(_) => match i.next_token() { + Some(_) => {} None => break, }, } } - Err(_) => match c.get(1..) { - Some(next) => { - consumed_bytes += 1; - next - } + Err(_) => match i.next_token() { + Some(_) => {} None => break, }, - }; + } } - Ok((&[], (i.as_bstr(), None))) + + i.reset(start); + rest.map(|r: &[u8]| (r.as_bstr(), None)).parse_next(i) } /// Returns title and body, without separator -pub fn message(input: &[u8]) -> (&BStr, Option<&BStr>) { - all_consuming(subject_and_body::<()>)(input).expect("cannot fail").1 +pub fn message(mut input: &[u8]) -> (&BStr, Option<&BStr>) { + terminated(subject_and_body::<()>, eof) + .parse_next(&mut input) + .expect("cannot fail") } diff --git a/vendor/gix-object/src/commit/mod.rs b/vendor/gix-object/src/commit/mod.rs index 9e135df28..b3de9b0a4 100644 --- a/vendor/gix-object/src/commit/mod.rs +++ b/vendor/gix-object/src/commit/mod.rs @@ -1,4 +1,6 @@ -use bstr::{BStr, ByteSlice}; +use std::ops::Range; + +use bstr::{BStr, BString, ByteSlice}; use crate::{Commit, CommitRef, TagRef}; @@ -21,16 +23,49 @@ pub struct MessageRef<'a> { pub body: Option<&'a BStr>, } +/// The raw commit data, parseable by [`CommitRef`] or [`Commit`], which was fed into a program to produce a signature. +/// +/// See [`extract_signature()`](crate::CommitRefIter::signature()) for how to obtain it. +// TODO: implement `std::io::Read` to avoid allocations +#[derive(PartialEq, Eq, Debug, Hash, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct SignedData<'a> { + /// The raw commit data that includes the signature. + data: &'a [u8], + /// The byte range at which we find the signature. All but the signature is the data that was signed. + signature_range: Range<usize>, +} + +impl SignedData<'_> { + /// Convenience method to obtain a copy of the signed data. + pub fn to_bstring(&self) -> BString { + let mut buf = BString::from(&self.data[..self.signature_range.start]); + buf.extend_from_slice(&self.data[self.signature_range.end..]); + buf + } +} + +impl From<SignedData<'_>> for BString { + fn from(value: SignedData<'_>) -> Self { + value.to_bstring() + } +} + /// pub mod ref_iter; mod write; +/// Lifecycle impl<'a> CommitRef<'a> { /// Deserialize a commit from the given `data` bytes while avoiding most allocations. - pub fn from_bytes(data: &'a [u8]) -> Result<CommitRef<'a>, crate::decode::Error> { - decode::commit(data).map(|(_, t)| t).map_err(crate::decode::Error::from) + pub fn from_bytes(mut data: &'a [u8]) -> Result<CommitRef<'a>, crate::decode::Error> { + decode::commit(&mut data).map_err(crate::decode::Error::with_err) } +} + +/// Access +impl<'a> CommitRef<'a> { /// Return the `tree` fields hash digest. pub fn tree(&self) -> gix_hash::ObjectId { gix_hash::ObjectId::from_hex(self.tree).expect("prior validation of tree hash during parsing") @@ -45,7 +80,7 @@ impl<'a> CommitRef<'a> { /// Returns a convenient iterator over all extra headers. pub fn extra_headers(&self) -> crate::commit::ExtraHeaders<impl Iterator<Item = (&BStr, &BStr)>> { - crate::commit::ExtraHeaders::new(self.extra_headers.iter().map(|(k, v)| (*k, v.as_ref()))) + ExtraHeaders::new(self.extra_headers.iter().map(|(k, v)| (*k, v.as_ref()))) } /// Return the author, with whitespace trimmed. @@ -68,7 +103,7 @@ impl<'a> CommitRef<'a> { } /// Returns the time at which this commit was created. - pub fn time(&self) -> gix_actor::Time { + pub fn time(&self) -> gix_date::Time { self.committer.time } } diff --git a/vendor/gix-object/src/commit/ref_iter.rs b/vendor/gix-object/src/commit/ref_iter.rs index 454f69ac0..4401384ca 100644 --- a/vendor/gix-object/src/commit/ref_iter.rs +++ b/vendor/gix-object/src/commit/ref_iter.rs @@ -1,15 +1,21 @@ -use std::borrow::Cow; +use std::{borrow::Cow, ops::Range}; use bstr::BStr; use gix_hash::{oid, ObjectId}; -use nom::{ - branch::alt, - bytes::complete::is_not, - combinator::{all_consuming, opt}, - error::context, +use winnow::{ + combinator::{alt, eof, opt, terminated}, + error::StrContext, + prelude::*, + token::take_till1, }; -use crate::{bstr::ByteSlice, commit::decode, parse, parse::NL, CommitRefIter}; +use crate::{ + bstr::ByteSlice, + commit::{decode, SignedData}, + parse, + parse::NL, + CommitRefIter, +}; #[derive(Copy, Clone)] pub(crate) enum SignatureKind { @@ -30,6 +36,7 @@ pub(crate) enum State { Message, } +/// Lifecycle impl<'a> CommitRefIter<'a> { /// Create a commit iterator from data. pub fn from_bytes(data: &'a [u8]) -> CommitRefIter<'a> { @@ -38,6 +45,37 @@ impl<'a> CommitRefIter<'a> { state: State::default(), } } +} + +/// Access +impl<'a> CommitRefIter<'a> { + /// Parse `data` as commit and return its PGP signature, along with *all non-signature* data as [`SignedData`], or `None` + /// if the commit isn't signed. + /// + /// This allows the caller to validate the signature by passing the signed data along with the signature back to the program + /// that created it. + pub fn signature(data: &'a [u8]) -> Result<Option<(Cow<'a, BStr>, SignedData<'a>)>, crate::decode::Error> { + let mut signature_and_range = None; + + let raw_tokens = CommitRefIterRaw { + data, + state: State::default(), + offset: 0, + }; + for token in raw_tokens { + let token = token?; + if let Token::ExtraHeader((name, value)) = &token.token { + if *name == "gpgsig" { + // keep track of the signature range alongside the signature data, + // because all but the signature is the signed data. + signature_and_range = Some((value.clone(), token.token_range)); + break; + } + } + } + + Ok(signature_and_range.map(|(sig, signature_range)| (sig, SignedData { data, signature_range }))) + } /// Returns the object id of this commits tree if it is the first function called and if there is no error in decoding /// the data. @@ -106,7 +144,7 @@ impl<'a> CommitRefIter<'a> { _ => None, }) .transpose() - .map(|msg| msg.unwrap_or_default()) + .map(Option::unwrap_or_default) } } @@ -115,13 +153,21 @@ fn missing_field() -> crate::decode::Error { } impl<'a> CommitRefIter<'a> { + #[inline] fn next_inner(i: &'a [u8], state: &mut State) -> Result<(&'a [u8], Token<'a>), crate::decode::Error> { + Self::next_inner_(i, state).map_err(crate::decode::Error::with_err) + } + + fn next_inner_( + mut i: &'a [u8], + state: &mut State, + ) -> Result<(&'a [u8], Token<'a>), winnow::error::ErrMode<crate::decode::ParseError>> { use State::*; Ok(match state { Tree => { - let (i, tree) = context("tree <40 lowercase hex char>", |i| { - parse::header_field(i, b"tree", parse::hex_hash) - })(i)?; + let tree = (|i: &mut _| parse::header_field(i, b"tree", parse::hex_hash)) + .context(StrContext::Expected("tree <40 lowercase hex char>".into())) + .parse_next(&mut i)?; *state = State::Parents; ( i, @@ -131,10 +177,9 @@ impl<'a> CommitRefIter<'a> { ) } Parents => { - let (i, parent) = context( - "commit <40 lowercase hex char>", - opt(|i| parse::header_field(i, b"parent", parse::hex_hash)), - )(i)?; + let parent = opt(|i: &mut _| parse::header_field(i, b"parent", parse::hex_hash)) + .context(StrContext::Expected("commit <40 lowercase hex char>".into())) + .parse_next(&mut i)?; match parent { Some(parent) => ( i, @@ -146,7 +191,7 @@ impl<'a> CommitRefIter<'a> { *state = State::Signature { of: SignatureKind::Author, }; - return Self::next_inner(i, state); + return Self::next_inner_(i, state); } } } @@ -162,7 +207,9 @@ impl<'a> CommitRefIter<'a> { (&b"committer"[..], "committer <signature>") } }; - let (i, signature) = context(err_msg, |i| parse::header_field(i, field_name, parse::signature))(i)?; + let signature = (|i: &mut _| parse::header_field(i, field_name, parse::signature)) + .context(StrContext::Expected(err_msg.into())) + .parse_next(&mut i)?; ( i, match who { @@ -172,37 +219,35 @@ impl<'a> CommitRefIter<'a> { ) } Encoding => { - let (i, encoding) = context( - "encoding <encoding>", - opt(|i| parse::header_field(i, b"encoding", is_not(NL))), - )(i)?; + let encoding = opt(|i: &mut _| parse::header_field(i, b"encoding", take_till1(NL))) + .context(StrContext::Expected("encoding <encoding>".into())) + .parse_next(&mut i)?; *state = State::ExtraHeaders; match encoding { Some(encoding) => (i, Token::Encoding(encoding.as_bstr())), - None => return Self::next_inner(i, state), + None => return Self::next_inner_(i, state), } } ExtraHeaders => { - let (i, extra_header) = context( - "<field> <single-line|multi-line>", - opt(alt(( - |i| parse::any_header_field_multi_line(i).map(|(i, (k, o))| (i, (k.as_bstr(), Cow::Owned(o)))), - |i| { - parse::any_header_field(i, is_not(NL)) - .map(|(i, (k, o))| (i, (k.as_bstr(), Cow::Borrowed(o.as_bstr())))) - }, - ))), - )(i)?; + let extra_header = opt(alt(( + |i: &mut _| parse::any_header_field_multi_line(i).map(|(k, o)| (k.as_bstr(), Cow::Owned(o))), + |i: &mut _| { + parse::any_header_field(i, take_till1(NL)) + .map(|(k, o)| (k.as_bstr(), Cow::Borrowed(o.as_bstr()))) + }, + ))) + .context(StrContext::Expected("<field> <single-line|multi-line>".into())) + .parse_next(&mut i)?; match extra_header { Some(extra_header) => (i, Token::ExtraHeader(extra_header)), None => { *state = State::Message; - return Self::next_inner(i, state); + return Self::next_inner_(i, state); } } } Message => { - let (i, message) = all_consuming(decode::message)(i)?; + let message = terminated(decode::message, eof).parse_next(&mut i)?; debug_assert!( i.is_empty(), "we should have consumed all data - otherwise iter may go forever" @@ -233,6 +278,48 @@ impl<'a> Iterator for CommitRefIter<'a> { } } +/// A variation of [`CommitRefIter`] that return's [`RawToken`]s instead. +struct CommitRefIterRaw<'a> { + data: &'a [u8], + state: State, + offset: usize, +} + +impl<'a> Iterator for CommitRefIterRaw<'a> { + type Item = Result<RawToken<'a>, crate::decode::Error>; + + fn next(&mut self) -> Option<Self::Item> { + if self.data.is_empty() { + return None; + } + match CommitRefIter::next_inner(self.data, &mut self.state) { + Ok((remaining, token)) => { + let consumed = self.data.len() - remaining.len(); + let start = self.offset; + let end = start + consumed; + self.offset = end; + + self.data = remaining; + Some(Ok(RawToken { + token, + token_range: start..end, + })) + } + Err(err) => { + self.data = &[]; + Some(Err(err)) + } + } + } +} + +/// A combination of a parsed [`Token`] as well as the range of bytes that were consumed to parse it. +struct RawToken<'a> { + /// The parsed token. + token: Token<'a>, + token_range: Range<usize>, +} + /// A token returned by the [commit iterator][CommitRefIter]. #[allow(missing_docs)] #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] diff --git a/vendor/gix-object/src/commit/write.rs b/vendor/gix-object/src/commit/write.rs index e498789a2..667d25763 100644 --- a/vendor/gix-object/src/commit/write.rs +++ b/vendor/gix-object/src/commit/write.rs @@ -6,7 +6,7 @@ use crate::{encode, encode::NL, Commit, CommitRef, Kind}; impl crate::WriteTo for Commit { /// Serializes this instance to `out` in the git serialization format. - fn write_to(&self, mut out: impl io::Write) -> io::Result<()> { + fn write_to(&self, mut out: &mut dyn io::Write) -> io::Result<()> { encode::trusted_header_id(b"tree", &self.tree, &mut out)?; for parent in &self.parents { encode::trusted_header_id(b"parent", parent, &mut out)?; @@ -52,7 +52,7 @@ impl crate::WriteTo for Commit { impl<'a> crate::WriteTo for CommitRef<'a> { /// Serializes this instance to `out` in the git serialization format. - fn write_to(&self, mut out: impl io::Write) -> io::Result<()> { + fn write_to(&self, mut out: &mut dyn io::Write) -> io::Result<()> { encode::trusted_header_id(b"tree", &self.tree(), &mut out)?; for parent in self.parents() { encode::trusted_header_id(b"parent", &parent, &mut out)?; |