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

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
77
88 ///
99 pub 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+
1018 use crate::{
1119 bstr::{BStr, ByteSlice},
1220 commit::message::BodyRef,
1321 };
22+ use nom::error::ErrorKind;
1423
1524 /// An iterator over trailers as parsed from a commit message body.
1625 ///
1726 /// lines with parsing failures will be skipped
1827 pub struct Trailers<'a> {
19− cursor: &'a [u8],
28+ pub(crate) cursor: &'a [u8],
2029 }
2130
2231 /// A trailer as parsed from the commit message body.
2332 #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
2433 #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
25− pub struct Trailer<'a> {
34+ pub struct TrailerRef<'a> {
2635 /// The name of the trailer, like "Signed-off-by", up to the separator ": "
2736 #[cfg_attr(feature = "serde1", serde(borrow))]
2837 pub token: &'a BStr,
@@ -31,13 +40,74 @@ pub mod body { · 2 unmodified lines
3140 pub value: &'a BStr,
3241 }
3342
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+
3491 impl<'a> Iterator for Trailers<'a> {
35− type Item = Trailer<'a>;
92+ type Item = TrailerRef<'a>;
3693
3794 fn next(&mut self) -> Option<Self::Item> {
3895 if self.cursor.is_empty() {
3996 return None;
4097 }
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+ }
41111 None
42112 }
43113 }
@@ -75,7 +145,7 @@ mod decode { · 31 unmodified lines
75145
76146 use crate::bstr::{BStr, ByteSlice};
77147
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> {
79149 alt((tag(b"\r\n"), tag(b"\n")))(i)
80150 }
81151
@@ -149,10 +219,7 @@ impl<'a> MessageRef<'a> { · 67 unmodified lines
149219
150220 /// Further parse the body into into non-trailer and trailers, which can be iterated from the returned [`BodyRef`].
151221 pub fn body(&self) -> Option<BodyRef<'a>> {
152− self.body.map(|b| BodyRef {
153− body_without_trailer: b,
154− start_of_trailer: &[],
155− })
222+ self.body.map(|b| BodyRef::from_bytes(b))
156223 }
157224 }
158225
@@ -204,6 +271,15 @@ pub struct BodyRef<'a> { · 45 unmodified lines
204271 start_of_trailer: &'a [u8],
205272 }
206273
274+impl<'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+
207283 impl<'a> AsRef<BStr> for BodyRef<'a> {
208284 fn as_ref(&self) -> &BStr {
209285 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
134134 }
135135
136136 #[test]
137−#[ignore]
138−fn with_footer() -> crate::Result {
137+fn with_trailer() -> crate::Result {
139138 let kim = SignatureRef {
140139 name: "Kim Altintop".into(),
141140 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
132132 }
133133
134134 mod body {
135− use git_object::commit::{message::BodyRef, MessageRef};
135+ use git_object::commit::{
136+ message::{body::TrailerRef, BodyRef},
137+ MessageRef,
138+ };
136139
137140 fn body(input: &str) -> BodyRef<'_> {
138141 BodyRef::from_bytes(input.as_bytes())
@@ -175,6 +178,20 @@ mod body { · 36 unmodified lines
175178 assert_eq!(body(input).as_ref(), input);
176179 }
177180
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+
178195 #[test]
179196 fn no_trailer_after_a_paragraph_windows() {
180197 let input = "foo\nbar\n\nbar\r\n\r\nbaz";