History

[object #177] migrate immutable::commit into crate::commit

Sebastian Thiel committed 5 years ago

Commit 45d39341 parent 7f1955911 files · +75 −67Browse files

Showing the changes to git-object/src/commit (3 of 11 files). Show all

Unified · Split

3 files
  1. git-object/src/commit/decode.rs +74 −0
  2. git-object/src/commit/mod.rs +74 −0
  3. git-object/src/commit/ref_iter.rs +240 −0
git-object/src/commit/decode.rs+74−0
@@ -0,0 +1,74 @@
1+use std::borrow::Cow;
2+
3+use nom::{
4+ branch::alt,
5+ bytes::complete::{is_not, tag},
6+ combinator::{all_consuming, opt},
7+ error::{context, ContextError, ParseError},
8+ multi::many0,
9+ IResult,
10+};
11+use smallvec::SmallVec;
12+
13+use crate::{
14+ immutable::{parse, parse::NL},
15+ BStr, ByteSlice, CommitRef,
16+};
17+
18+pub fn message<'a, E: ParseError<&'a [u8]> + ContextError<&'a [u8]>>(i: &'a [u8]) -> IResult<&'a [u8], &'a BStr, E> {
19+ if i.is_empty() {
20+ // newline + [message]
21+ return Err(nom::Err::Error(E::add_context(
22+ i,
23+ "newline + <message>",
24+ E::from_error_kind(i, nom::error::ErrorKind::Eof),
25+ )));
26+ }
27+ let (i, _) = context("a newline separates headers from the message", tag(NL))(i)?;
28+ Ok((&[], i.as_bstr()))
29+}
30+
31+pub fn commit<'a, E: ParseError<&'a [u8]> + ContextError<&'a [u8]>>(
32+ i: &'a [u8],
33+) -> IResult<&'a [u8], CommitRef<'_>, E> {
34+ let (i, tree) = context("tree <40 lowercase hex char>", |i| {
35+ parse::header_field(i, b"tree", parse::hex_hash)
36+ })(i)?;
37+ let (i, parents) = context(
38+ "zero or more 'parent <40 lowercase hex char>'",
39+ many0(|i| parse::header_field(i, b"parent", parse::hex_hash)),
40+ )(i)?;
41+ let (i, author) = context("author <signature>", |i| {
42+ parse::header_field(i, b"author", parse::signature)
43+ })(i)?;
44+ let (i, committer) = context("committer <signature>", |i| {
45+ parse::header_field(i, b"committer", parse::signature)
46+ })(i)?;
47+ let (i, encoding) = context(
48+ "encoding <encoding>",
49+ opt(|i| parse::header_field(i, b"encoding", is_not(NL))),
50+ )(i)?;
51+ let (i, extra_headers) = context(
52+ "<field> <single-line|multi-line>",
53+ many0(alt((
54+ |i| parse::any_header_field_multi_line(i).map(|(i, (k, o))| (i, (k.as_bstr(), Cow::Owned(o)))),
55+ |i| {
56+ parse::any_header_field(i, is_not(NL)).map(|(i, (k, o))| (i, (k.as_bstr(), Cow::Borrowed(o.as_bstr()))))
57+ },
58+ ))),
59+ )(i)?;
60+ let (i, message) = all_consuming(message)(i)?;
61+
62+ Ok((
63+ i,
64+ CommitRef {
65+ tree,
66+ parents: SmallVec::from(parents),
67+ author,
68+ committer,
69+ encoding: encoding.map(ByteSlice::as_bstr),
70+ message,
71+ extra_headers,
72+ },
73+ ))
74+}
git-object/src/commit/mod.rs+74−0
@@ -0,0 +1,74 @@
1+use bstr::{BStr, ByteSlice};
2+
3+pub use ref_iter::RefIter;
4+
5+use crate::{immutable, TagRef};
6+use crate::{immutable::object, CommitRef};
7+
8+mod decode;
9+
10+///
11+pub mod ref_iter;
12+
13+impl<'a> CommitRef<'a> {
14+ /// Deserialize a commit from the given `data` bytes while avoiding most allocations.
15+ pub fn from_bytes(data: &'a [u8]) -> Result<CommitRef<'a>, object::decode::Error> {
16+ decode::commit(data)
17+ .map(|(_, t)| t)
18+ .map_err(object::decode::Error::from)
19+ }
20+ /// Return the `tree` fields hash digest.
21+ pub fn tree(&self) -> git_hash::ObjectId {
22+ git_hash::ObjectId::from_hex(self.tree).expect("prior validation of tree hash during parsing")
23+ }
24+
25+ /// Returns an iterator of parent object ids
26+ pub fn parents(&self) -> impl Iterator<Item = git_hash::ObjectId> + '_ {
27+ self.parents
28+ .iter()
29+ .map(|hex_hash| git_hash::ObjectId::from_hex(hex_hash).expect("prior validation of hashes during parsing"))
30+ }
31+
32+ /// Returns a convenient iterator over all extra headers.
33+ pub fn extra_headers(&self) -> crate::commit::ExtraHeaders<impl Iterator<Item = (&BStr, &BStr)>> {
34+ crate::commit::ExtraHeaders::new(self.extra_headers.iter().map(|(k, v)| (*k, v.as_ref())))
35+ }
36+}
37+
38+/// An iterator over extra headers in [owned][crate::Commit] and [borrowed][crate::CommitRef] commits.
39+pub struct ExtraHeaders<I> {
40+ inner: I,
41+}
42+
43+/// Instantiation and convenience.
44+impl<'a, I> ExtraHeaders<I>
45+where
46+ I: Iterator<Item = (&'a BStr, &'a BStr)>,
47+{
48+ /// Create a new instance from an iterator over tuples of (name, value) pairs.
49+ pub fn new(iter: I) -> Self {
50+ ExtraHeaders { inner: iter }
51+ }
52+ /// Find the _value_ of the _first_ header with the given `name`.
53+ pub fn find(mut self, name: &str) -> Option<&'a BStr> {
54+ self.inner
55+ .find_map(move |(k, v)| if k == name.as_bytes().as_bstr() { Some(v) } else { None })
56+ }
57+ /// Return an iterator over all _values_ of headers with the given `name`.
58+ pub fn find_all(self, name: &'a str) -> impl Iterator<Item = &'a BStr> {
59+ self.inner
60+ .filter_map(move |(k, v)| if k == name.as_bytes().as_bstr() { Some(v) } else { None })
61+ }
62+ /// Return an iterator over all git mergetags.
63+ ///
64+ /// A merge tag is a tag object embedded within the respective header field of a commit, making
65+ /// it a child object of sorts.
66+ pub fn mergetags(self) -> impl Iterator<Item = Result<TagRef<'a>, immutable::object::decode::Error>> {
67+ self.find_all("mergetag").map(|b| TagRef::from_bytes(b))
68+ }
69+
70+ /// Return the cryptographic signature provided by gpg/pgp verbatim.
71+ pub fn pgp_signature(self) -> Option<&'a BStr> {
72+ self.find("gpgsig")
73+ }
74+}
git-object/src/commit/ref_iter.rs+240−0
@@ -0,0 +1,240 @@
1+use std::borrow::Cow;
2+
3+use bstr::BStr;
4+use nom::{
5+ branch::alt,
6+ bytes::complete::is_not,
7+ combinator::{all_consuming, opt},
8+ error::context,
9+};
10+
11+use git_hash::{oid, ObjectId};
12+
13+use crate::commit::decode;
14+use crate::{
15+ bstr::ByteSlice,
16+ immutable::{object, parse, parse::NL},
17+};
18+
19+#[derive(Copy, Clone)]
20+enum SignatureKind {
21+ Author,
22+ Committer,
23+}
24+
25+enum State {
26+ Tree,
27+ Parents,
28+ Signature { of: SignatureKind },
29+ Encoding,
30+ ExtraHeaders,
31+ Message,
32+}
33+
34+impl Default for State {
35+ fn default() -> Self {
36+ State::Tree
37+ }
38+}
39+
40+/// Like [`CommitRef`][crate::CommitRef], but as `Iterator` to support (up to) entirely allocation free parsing.
41+/// It's particularly useful to traverse the commit graph without ever allocating arrays for parents.
42+pub struct CommitRefIter<'a> {
43+ data: &'a [u8],
44+ state: State,
45+}
46+
47+impl<'a> CommitRefIter<'a> {
48+ /// Create a commit iterator from data.
49+ pub fn from_bytes(data: &'a [u8]) -> CommitRefIter<'a> {
50+ CommitRefIter {
51+ data,
52+ state: State::default(),
53+ }
54+ }
55+
56+ /// Returns the object id of this commits tree if it is the first function called and if there is no error in decoding
57+ /// the data.
58+ ///
59+ /// Note that this method must only be called once or else will always return None while consuming a single token.
60+ /// Errors are coerced into options, hiding whether there was an error or not. The caller should assume an error if they
61+ /// call the method as intended. Such a squelched error cannot be recovered unless the objects data is retrieved and parsed again.
62+ /// `next()`.
63+ pub fn tree_id(&mut self) -> Option<ObjectId> {
64+ self.next().and_then(Result::ok).and_then(Token::into_id)
65+ }
66+
67+ /// Returns all signatures, first the author, then the committer, if there is no decoding error.
68+ ///
69+ /// Errors are coerced into options, hiding whether there was an error or not. The caller knows if there was an error or not
70+ /// if not exactly two signatures were iterable.
71+ /// Errors are not the common case - if an error needs to be detectable, use this instance as iterator.
72+ pub fn signatures(&'a mut self) -> impl Iterator<Item = git_actor::SignatureRef<'_>> + 'a {
73+ self.filter_map(Result::ok)
74+ .skip_while(|t| !matches!(t, Token::Author { .. } | Token::Committer { .. }))
75+ .filter_map(|t| match t {
76+ Token::Author { signature } | Token::Committer { signature } => Some(signature),
77+ _ => None,
78+ })
79+ }
80+}
81+
82+impl<'a> CommitRefIter<'a> {
83+ fn next_inner(i: &'a [u8], state: &mut State) -> Result<(&'a [u8], Token<'a>), object::decode::Error> {
84+ use State::*;
85+ Ok(match state {
86+ Tree => {
87+ let (i, tree) = context("tree <40 lowercase hex char>", |i| {
88+ parse::header_field(i, b"tree", parse::hex_hash)
89+ })(i)?;
90+ *state = State::Parents;
91+ (
92+ i,
93+ Token::Tree {
94+ id: ObjectId::from_hex(tree).expect("parsing validation"),
95+ },
96+ )
97+ }
98+ Parents => {
99+ let (i, parent) = context(
100+ "commit <40 lowercase hex char>",
101+ opt(|i| parse::header_field(i, b"parent", parse::hex_hash)),
102+ )(i)?;
103+ match parent {
104+ Some(parent) => (
105+ i,
106+ Token::Parent {
107+ id: ObjectId::from_hex(parent).expect("parsing validation"),
108+ },
109+ ),
110+ None => {
111+ *state = State::Signature {
112+ of: SignatureKind::Author,
113+ };
114+ return Self::next_inner(i, state);
115+ }
116+ }
117+ }
118+ Signature { ref mut of } => {
119+ let who = *of;
120+ let (field_name, err_msg) = match of {
121+ SignatureKind::Author => {
122+ *of = SignatureKind::Committer;
123+ (&b"author"[..], "author <signature>")
124+ }
125+ SignatureKind::Committer => {
126+ *state = State::Encoding;
127+ (&b"committer"[..], "committer <signature>")
128+ }
129+ };
130+ let (i, signature) = context(err_msg, |i| parse::header_field(i, field_name, parse::signature))(i)?;
131+ (
132+ i,
133+ match who {
134+ SignatureKind::Author => Token::Author { signature },
135+ SignatureKind::Committer => Token::Committer { signature },
136+ },
137+ )
138+ }
139+ Encoding => {
140+ let (i, encoding) = context(
141+ "encoding <encoding>",
142+ opt(|i| parse::header_field(i, b"encoding", is_not(NL))),
143+ )(i)?;
144+ *state = State::ExtraHeaders;
145+ match encoding {
146+ Some(encoding) => (i, Token::Encoding(encoding.as_bstr())),
147+ None => return Self::next_inner(i, state),
148+ }
149+ }
150+ ExtraHeaders => {
151+ let (i, extra_header) = context(
152+ "<field> <single-line|multi-line>",
153+ opt(alt((
154+ |i| parse::any_header_field_multi_line(i).map(|(i, (k, o))| (i, (k.as_bstr(), Cow::Owned(o)))),
155+ |i| {
156+ parse::any_header_field(i, is_not(NL))
157+ .map(|(i, (k, o))| (i, (k.as_bstr(), Cow::Borrowed(o.as_bstr()))))
158+ },
159+ ))),
160+ )(i)?;
161+ match extra_header {
162+ Some(extra_header) => (i, Token::ExtraHeader(extra_header)),
163+ None => {
164+ *state = State::Message;
165+ return Self::next_inner(i, state);
166+ }
167+ }
168+ }
169+ Message => {
170+ let (i, message) = all_consuming(decode::message)(i)?;
171+ debug_assert!(
172+ i.is_empty(),
173+ "we should have consumed all data - otherwise iter may go forever"
174+ );
175+ return Ok((i, Token::Message(message)));
176+ }
177+ })
178+ }
179+}
180+
181+impl<'a> Iterator for CommitRefIter<'a> {
182+ type Item = Result<Token<'a>, object::decode::Error>;
183+
184+ fn next(&mut self) -> Option<Self::Item> {
185+ if self.data.is_empty() {
186+ return None;
187+ }
188+ match Self::next_inner(self.data, &mut self.state) {
189+ Ok((data, token)) => {
190+ self.data = data;
191+ Some(Ok(token))
192+ }
193+ Err(err) => {
194+ self.data = &[];
195+ Some(Err(err))
196+ }
197+ }
198+ }
199+}
200+
201+/// A token returned by the [commit iterator][RefIter].
202+#[allow(missing_docs)]
203+#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
204+pub enum Token<'a> {
205+ Tree {
206+ id: ObjectId,
207+ },
208+ Parent {
209+ id: ObjectId,
210+ },
211+ /// A person who authored the content of the commit.
212+ Author {
213+ signature: git_actor::SignatureRef<'a>,
214+ },
215+ /// A person who committed the authors work to the repository.
216+ Committer {
217+ signature: git_actor::SignatureRef<'a>,
218+ },
219+ Encoding(&'a BStr),
220+ ExtraHeader((&'a BStr, Cow<'a, BStr>)),
221+ Message(&'a BStr),
222+}
223+
224+impl<'a> Token<'a> {
225+ /// Return the object id of this token if its a [tree][Token::Tree] or a [parent commit][Token::Parent].
226+ pub fn id(&self) -> Option<&oid> {
227+ match self {
228+ Token::Tree { id } | Token::Parent { id } => Some(id.as_ref()),
229+ _ => None,
230+ }
231+ }
232+
233+ /// Return the owned object id of this token if its a [tree][Token::Tree] or a [parent commit][Token::Parent].
234+ pub fn into_id(self) -> Option<ObjectId> {
235+ match self {
236+ Token::Tree { id } | Token::Parent { id } => Some(id),
237+ _ => None,
238+ }
239+ }
240+}