This categorizes them semantically without using the overloaded names of owning and borrowing, which mean different things in Rust.
Showing the changes to git-object/src/immutable/commit.rs (1 of 34 files). Show all
Unified · Split
| @@ -0,0 +1,109 @@ | ||
| 1 | +use super::Error; | |
| 2 | +use crate::{ | |
| 3 | + commit, | |
| 4 | + immutable::{parse, parse::NL, Signature}, | |
| 5 | + BStr, ByteSlice, | |
| 6 | +}; | |
| 7 | +use nom::{ | |
| 8 | + branch::alt, | |
| 9 | + bytes::{complete::is_not, complete::tag}, | |
| 10 | + combinator::{all_consuming, opt}, | |
| 11 | + multi::many0, | |
| 12 | + IResult, | |
| 13 | +}; | |
| 14 | +use smallvec::SmallVec; | |
| 15 | +use std::borrow::Cow; | |
| 16 | + | |
| 17 | +/// A git commit parsed using [`from_bytes()`][Commit::from_bytes()]. | |
| 18 | +/// | |
| 19 | +/// A commit encapsulates information about a point in time at which the state of the repository is recorded, usually after a | |
| 20 | +/// change which is documented in the commit `message`. | |
| 21 | +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)] | |
| 22 | +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] | |
| 23 | +pub struct Commit<'a> { | |
| 24 | + /// HEX hash of tree object we point to. Usually 40 bytes long. | |
| 25 | + /// | |
| 26 | + /// Use [`tree()`][Commit::tree()] to obtain a decoded version of it. | |
| 27 | + #[cfg_attr(feature = "serde1", serde(borrow))] | |
| 28 | + pub tree: &'a BStr, | |
| 29 | + /// HEX hash of each parent commit. Empty for first commit in repository. | |
| 30 | + pub parents: SmallVec<[&'a BStr; 1]>, | |
| 31 | + /// Who wrote this commit. | |
| 32 | + pub author: Signature<'a>, | |
| 33 | + /// Who committed this commit. | |
| 34 | + /// | |
| 35 | + /// This may be different from the `author` in case the author couldn't write to the repository themselves and | |
| 36 | + /// is commonly encountered with contributed commits. | |
| 37 | + pub committer: Signature<'a>, | |
| 38 | + /// The name of the message encoding, otherwise [UTF-8 should be assumed](https://github.com/git/git/blob/e67fbf927dfdf13d0b21dc6ea15dc3c7ef448ea0/commit.c#L1493:L1493). | |
| 39 | + pub encoding: Option<&'a BStr>, | |
| 40 | + /// The commit message documenting the change. | |
| 41 | + pub message: &'a BStr, | |
| 42 | + /// Extra header fields, in order of them being encountered, made accessible with the iterator returned by [`extra_headers()`][Commit::extra_headers()]. | |
| 43 | + pub extra_headers: Vec<(&'a BStr, Cow<'a, BStr>)>, | |
| 44 | +} | |
| 45 | + | |
| 46 | +impl<'a> Commit<'a> { | |
| 47 | + /// Deserialize a commit from the given `data` bytes while avoiding most allocations. | |
| 48 | + pub fn from_bytes(data: &'a [u8]) -> Result<Commit<'a>, Error> { | |
| 49 | + parse(data).map(|(_, t)| t).map_err(Error::from) | |
| 50 | + } | |
| 51 | + /// Return the `tree` fields hash digest. | |
| 52 | + pub fn tree(&self) -> git_hash::ObjectId { | |
| 53 | + git_hash::ObjectId::from_hex(self.tree).expect("prior validation of tree hash during parsing") | |
| 54 | + } | |
| 55 | + | |
| 56 | + /// Returns an iterator of parent object ids | |
| 57 | + pub fn parents(&self) -> impl Iterator<Item = git_hash::ObjectId> + '_ { | |
| 58 | + self.parents | |
| 59 | + .iter() | |
| 60 | + .map(|hex_hash| git_hash::ObjectId::from_hex(hex_hash).expect("prior validation of hashes during parsing")) | |
| 61 | + } | |
| 62 | + | |
| 63 | + /// Returns a convenient iterator over all extra headers. | |
| 64 | + pub fn extra_headers(&self) -> commit::ExtraHeaders<impl Iterator<Item = (&BStr, &BStr)>> { | |
| 65 | + commit::ExtraHeaders::new(self.extra_headers.iter().map(|(k, v)| (*k, v.as_ref()))) | |
| 66 | + } | |
| 67 | +} | |
| 68 | + | |
| 69 | +fn parse_message(i: &[u8]) -> IResult<&[u8], &BStr, Error> { | |
| 70 | + if i.is_empty() { | |
| 71 | + // newline + [message] | |
| 72 | + return Err(nom::Err::Error(Error::NomDetail(i.into(), "commit message is missing"))); | |
| 73 | + } | |
| 74 | + let (i, _) = tag(NL)(i).map_err(Error::context("a newline separates headers from the message"))?; | |
| 75 | + debug_assert!(!i.is_empty()); | |
| 76 | + Ok((&[], &i.as_bstr())) | |
| 77 | +} | |
| 78 | + | |
| 79 | +fn parse(i: &[u8]) -> IResult<&[u8], Commit<'_>, Error> { | |
| 80 | + let (i, tree) = | |
| 81 | + parse::header_field(i, b"tree", parse::hex_sha1).map_err(Error::context("tree <40 lowercase hex char>"))?; | |
| 82 | + let (i, parents) = many0(|i| parse::header_field(i, b"parent", parse::hex_sha1))(i) | |
| 83 | + .map_err(Error::context("zero or more 'parent <40 lowercase hex char>'"))?; | |
| 84 | + let (i, author) = | |
| 85 | + parse::header_field(i, b"author", parse::signature).map_err(Error::context("author <signature>"))?; | |
| 86 | + let (i, committer) = | |
| 87 | + parse::header_field(i, b"committer", parse::signature).map_err(Error::context("committer <signature>"))?; | |
| 88 | + let (i, encoding) = | |
| 89 | + opt(|i| parse::header_field(i, b"encoding", is_not(NL)))(i).map_err(Error::context("encoding <encoding>"))?; | |
| 90 | + let (i, extra_headers) = many0(alt(( | |
| 91 | + |i| parse::any_header_field_multi_line(i).map(|(i, (k, o))| (i, (k.as_bstr(), Cow::Owned(o)))), | |
| 92 | + |i| parse::any_header_field(i, is_not(NL)).map(|(i, (k, o))| (i, (k.as_bstr(), Cow::Borrowed(o.as_bstr())))), | |
| 93 | + )))(i) | |
| 94 | + .map_err(Error::context("<field> <single-line|multi-line>"))?; | |
| 95 | + let (i, message) = all_consuming(parse_message)(i)?; | |
| 96 | + | |
| 97 | + Ok(( | |
| 98 | + i, | |
| 99 | + Commit { | |
| 100 | + tree, | |
| 101 | + parents: SmallVec::from(parents), | |
| 102 | + author, | |
| 103 | + committer, | |
| 104 | + encoding: encoding.map(ByteSlice::as_bstr), | |
| 105 | + message, | |
| 106 | + extra_headers, | |
| 107 | + }, | |
| 108 | + )) | |
| 109 | +} | |