History

feat: `BodyRef::trailers()` allows iterating trailer tokens and values (#198)

Sebastian Thiel committed 5 years ago

Commit 175e1cb1 parent 3953c243 files · +103 −11Browse files

Showing the changes to git-object (3 of 3 files). Show all

Unified · Split

3 files
  1. git-object/src/commit/message.rs +84 −8
  2. git-object/tests/immutable/commit/from_bytes.rs +1 −2
  3. git-object/tests/immutable/commit/message.rs +18 −1
git-object/src/commit/message.rs+84−8
@@ -7,22 +7,31 @@ use crate::{ · 6 unmodified lines
7 7
8///8///
9pub mod body {9pub mod body {
10 use nom::{
11 bytes::complete::{tag, take_until1},
12 combinator::all_consuming,
13 error::ParseError,
14 sequence::terminated,
15 IResult,
16 };
17
10 use crate::{18 use crate::{
11 bstr::{BStr, ByteSlice},19 bstr::{BStr, ByteSlice},
12 commit::message::BodyRef,20 commit::message::BodyRef,
13 };21 };
22 use nom::error::ErrorKind;
14 23
15 /// An iterator over trailers as parsed from a commit message body.24 /// An iterator over trailers as parsed from a commit message body.
16 ///25 ///
17 /// lines with parsing failures will be skipped26 /// lines with parsing failures will be skipped
18 pub struct Trailers<'a> {27 pub struct Trailers<'a> {
19 cursor: &'a [u8],28 pub(crate) cursor: &'a [u8],
20 }29 }
21 30
22 /// A trailer as parsed from the commit message body.31 /// A trailer as parsed from the commit message body.
23 #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]32 #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
24 #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]33 #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
25 pub struct Trailer<'a> {34 pub struct TrailerRef<'a> {
26 /// The name of the trailer, like "Signed-off-by", up to the separator ": "35 /// The name of the trailer, like "Signed-off-by", up to the separator ": "
27 #[cfg_attr(feature = "serde1", serde(borrow))]36 #[cfg_attr(feature = "serde1", serde(borrow))]
28 pub token: &'a BStr,37 pub token: &'a BStr,
@@ -31,13 +40,74 @@ pub mod body { · 2 unmodified lines
31 pub value: &'a BStr,40 pub value: &'a BStr,
32 }41 }
33 42
43 fn parse_single_line_trailer<'a, E: ParseError<&'a [u8]>>(
44 i: &'a [u8],
45 ) -> IResult<&'a [u8], (&'a BStr, &'a BStr), E> {
46 let (value, token) = terminated(take_until1(b":".as_ref()), tag(b": "))(i.trim_end())?;
47 if token.trim_end().len() != token.len() {
48 Err(nom::Err::Failure(E::from_error_kind(i, ErrorKind::Fail)))
49 } else {
50 Ok((&[], (token.as_bstr(), value.as_bstr())))
51 }
52 }
53
54 #[cfg(test)]
55 mod test_parse_trailer {
56 use super::*;
57
58 fn parse(input: &str) -> (&BStr, &BStr) {
59 parse_single_line_trailer::<()>(input.as_bytes()).unwrap().1
60 }
61
62 #[test]
63 fn simple_newline() {
64 assert_eq!(parse("foo: bar\n"), ("foo".into(), "bar".into()));
65 }
66
67 #[test]
68 fn simple_non_ascii_no_newline() {
69 assert_eq!(parse("🤗: 🎉"), ("🤗".into(), "🎉".into()));
70 }
71
72 #[test]
73 fn with_lots_of_whitespace_newline() {
74 assert_eq!(
75 parse("hello foo: bar there \n"),
76 ("hello foo".into(), "bar there".into())
77 );
78 }
79
80 #[test]
81 fn whitespace_before_token_is_error() {
82 assert!(parse_single_line_trailer::<()>(b"foo : bar").is_err())
83 }
84
85 #[test]
86 fn simple_newline_windows() {
87 assert_eq!(parse("foo: bar\r\n"), ("foo".into(), "bar".into()));
88 }
89 }
90
34 impl<'a> Iterator for Trailers<'a> {91 impl<'a> Iterator for Trailers<'a> {
35 type Item = Trailer<'a>;92 type Item = TrailerRef<'a>;
36 93
37 fn next(&mut self) -> Option<Self::Item> {94 fn next(&mut self) -> Option<Self::Item> {
38 if self.cursor.is_empty() {95 if self.cursor.is_empty() {
39 return None;96 return None;
40 }97 }
98 for line in self.cursor.lines_with_terminator() {
99 self.cursor = &self.cursor[line.len()..];
100 if let Some(trailer) =
101 all_consuming(parse_single_line_trailer::<()>)(line)
102 .ok()
103 .map(|(_, (token, value))| TrailerRef {
104 token: token.trim().as_bstr(),
105 value: value.trim().as_bstr(),
106 })
107 {
108 return Some(trailer);
109 }
110 }
41 None111 None
42 }112 }
43 }113 }
@@ -75,7 +145,7 @@ mod decode { · 31 unmodified lines
75 145
76 use crate::bstr::{BStr, ByteSlice};146 use crate::bstr::{BStr, ByteSlice};
77 147
78 fn newline<'a, E: ParseError<&'a [u8]>>(i: &'a [u8]) -> IResult<&'a [u8], &'a [u8], E> {148 pub(crate) fn newline<'a, E: ParseError<&'a [u8]>>(i: &'a [u8]) -> IResult<&'a [u8], &'a [u8], E> {
79 alt((tag(b"\r\n"), tag(b"\n")))(i)149 alt((tag(b"\r\n"), tag(b"\n")))(i)
80 }150 }
81 151
@@ -149,10 +219,7 @@ impl<'a> MessageRef<'a> { · 67 unmodified lines
149 219
150 /// Further parse the body into into non-trailer and trailers, which can be iterated from the returned [`BodyRef`].220 /// Further parse the body into into non-trailer and trailers, which can be iterated from the returned [`BodyRef`].
151 pub fn body(&self) -> Option<BodyRef<'a>> {221 pub fn body(&self) -> Option<BodyRef<'a>> {
152 self.body.map(|b| BodyRef {222 self.body.map(|b| BodyRef::from_bytes(b))
153 body_without_trailer: b,
154 start_of_trailer: &[],
155 })
156 }223 }
157}224}
158 225
@@ -204,6 +271,15 @@ pub struct BodyRef<'a> { · 45 unmodified lines
204 start_of_trailer: &'a [u8],271 start_of_trailer: &'a [u8],
205}272}
206 273
274impl<'a> BodyRef<'a> {
275 /// Return an iterator over the trailers parsed from the last paragraph of the body. May be empty.
276 pub fn trailers(&self) -> body::Trailers<'a> {
277 body::Trailers {
278 cursor: self.start_of_trailer,
279 }
280 }
281}
282
207impl<'a> AsRef<BStr> for BodyRef<'a> {283impl<'a> AsRef<BStr> for BodyRef<'a> {
208 fn as_ref(&self) -> &BStr {284 fn as_ref(&self) -> &BStr {
209 self.body_without_trailer285 self.body_without_trailer
git-object/tests/immutable/commit/from_bytes.rs+1−2
@@ -134,8 +134,7 @@ fn with_encoding() -> crate::Result { · 133 unmodified lines
134}134}
135 135
136#[test]136#[test]
137#[ignore]137fn with_trailer() -> crate::Result {
138fn with_footer() -> crate::Result {
139 let kim = SignatureRef {138 let kim = SignatureRef {
140 name: "Kim Altintop".into(),139 name: "Kim Altintop".into(),
141 email: "kim@eagain.st".into(),140 email: "kim@eagain.st".into(),
git-object/tests/immutable/commit/message.rs+18−1
@@ -132,7 +132,10 @@ fn title_with_windows_separator_and_empty_body() { · 131 unmodified lines
132}132}
133 133
134mod body {134mod body {
135 use git_object::commit::{message::BodyRef, MessageRef};135 use git_object::commit::{
136 message::{body::TrailerRef, BodyRef},
137 MessageRef,
138 };
136 139
137 fn body(input: &str) -> BodyRef<'_> {140 fn body(input: &str) -> BodyRef<'_> {
138 BodyRef::from_bytes(input.as_bytes())141 BodyRef::from_bytes(input.as_bytes())
@@ -175,6 +178,20 @@ mod body { · 36 unmodified lines
175 assert_eq!(body(input).as_ref(), input);178 assert_eq!(body(input).as_ref(), input);
176 }179 }
177 180
181 #[test]
182 fn single_trailer_after_a_few_paragraphs() {
183 let input = "foo\nbar\n\nbar\n\nbaz\n\ntoken: value";
184 let body = body(input);
185 assert_eq!(body.as_ref(), "foo\nbar\n\nbar\n\nbaz");
186 assert_eq!(
187 body.trailers().collect::<Vec<_>>(),
188 vec![TrailerRef {
189 token: "token".into(),
190 value: "value".into()
191 }]
192 )
193 }
194
178 #[test]195 #[test]
179 fn no_trailer_after_a_paragraph_windows() {196 fn no_trailer_after_a_paragraph_windows() {
180 let input = "foo\nbar\n\nbar\r\n\r\nbaz";197 let input = "foo\nbar\n\nbar\r\n\r\nbaz";