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 @@
1use std::borrow::Cow;
2
3use 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};
11use smallvec::SmallVec;
12
13use crate::{
14 immutable::{parse, parse::NL},
15 BStr, ByteSlice, CommitRef,
16};
17
18pub 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
31pub 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 @@
1use bstr::{BStr, ByteSlice};
2
3pub use ref_iter::RefIter;
4
5use crate::{immutable, TagRef};
6use crate::{immutable::object, CommitRef};
7
8mod decode;
9
10///
11pub mod ref_iter;
12
13impl<'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.
39pub struct ExtraHeaders<I> {
40 inner: I,
41}
42
43/// Instantiation and convenience.
44impl<'a, I> ExtraHeaders<I>
45where
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 @@
1use std::borrow::Cow;
2
3use bstr::BStr;
4use nom::{
5 branch::alt,
6 bytes::complete::is_not,
7 combinator::{all_consuming, opt},
8 error::context,
9};
10
11use git_hash::{oid, ObjectId};
12
13use crate::commit::decode;
14use crate::{
15 bstr::ByteSlice,
16 immutable::{object, parse, parse::NL},
17};
18
19#[derive(Copy, Clone)]
20enum SignatureKind {
21 Author,
22 Committer,
23}
24
25enum State {
26 Tree,
27 Parents,
28 Signature { of: SignatureKind },
29 Encoding,
30 ExtraHeaders,
31 Message,
32}
33
34impl 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.
42pub struct CommitRefIter<'a> {
43 data: &'a [u8],
44 state: State,
45}
46
47impl<'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
82impl<'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
181impl<'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)]
204pub 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
224impl<'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}