Showing the changes to git-object/src/commit (1 of 3 files). Show all
Unified · Split
| @@ -7,22 +7,31 @@ use crate::{ · 6 unmodified lines | ||
| 7 | 7 | |
| 8 | 8 | /// |
| 9 | 9 | 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 | + | |
| 10 | 18 | use crate::{ |
| 11 | 19 | bstr::{BStr, ByteSlice}, |
| 12 | 20 | commit::message::BodyRef, |
| 13 | 21 | }; |
| 22 | + use nom::error::ErrorKind; | |
| 14 | 23 | |
| 15 | 24 | /// An iterator over trailers as parsed from a commit message body. |
| 16 | 25 | /// |
| 17 | 26 | /// lines with parsing failures will be skipped |
| 18 | 27 | pub struct Trailers<'a> { |
| 19 | − cursor: &'a [u8], | |
| 28 | + pub(crate) cursor: &'a [u8], | |
| 20 | 29 | } |
| 21 | 30 | |
| 22 | 31 | /// A trailer as parsed from the commit message body. |
| 23 | 32 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] |
| 24 | 33 | #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] |
| 25 | − pub struct Trailer<'a> { | |
| 34 | + pub struct TrailerRef<'a> { | |
| 26 | 35 | /// The name of the trailer, like "Signed-off-by", up to the separator ": " |
| 27 | 36 | #[cfg_attr(feature = "serde1", serde(borrow))] |
| 28 | 37 | pub token: &'a BStr, |
| @@ -31,13 +40,74 @@ pub mod body { · 2 unmodified lines | ||
| 31 | 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 | 91 | impl<'a> Iterator for Trailers<'a> { |
| 35 | − type Item = Trailer<'a>; | |
| 92 | + type Item = TrailerRef<'a>; | |
| 36 | 93 | |
| 37 | 94 | fn next(&mut self) -> Option<Self::Item> { |
| 38 | 95 | if self.cursor.is_empty() { |
| 39 | 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 | 111 | None |
| 42 | 112 | } |
| 43 | 113 | } |
| @@ -75,7 +145,7 @@ mod decode { · 31 unmodified lines | ||
| 75 | 145 | |
| 76 | 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 | 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 | 220 | /// Further parse the body into into non-trailer and trailers, which can be iterated from the returned [`BodyRef`]. |
| 151 | 221 | 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)) | |
| 156 | 223 | } |
| 157 | 224 | } |
| 158 | 225 | |
| @@ -204,6 +271,15 @@ pub struct BodyRef<'a> { · 45 unmodified lines | ||
| 204 | 271 | start_of_trailer: &'a [u8], |
| 205 | 272 | } |
| 206 | 273 | |
| 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 | + | |
| 207 | 283 | impl<'a> AsRef<BStr> for BodyRef<'a> { |
| 208 | 284 | fn as_ref(&self) -> &BStr { |
| 209 | 285 | self.body_without_trailer |