Showing the changes to git-object/src/commit/message (3 of 4 files). Show all
Unified · Split
| @@ -0,0 +1,123 @@ | |||
| 1 | use nom::{ | ||
| 2 | bytes::complete::{tag, take_until1}, | ||
| 3 | combinator::all_consuming, | ||
| 4 | error::{ErrorKind, ParseError}, | ||
| 5 | sequence::terminated, | ||
| 6 | IResult, | ||
| 7 | }; | ||
| 8 | |||
| 9 | use crate::{ | ||
| 10 | bstr::{BStr, ByteSlice}, | ||
| 11 | commit::message::BodyRef, | ||
| 12 | }; | ||
| 13 | |||
| 14 | /// An iterator over trailers as parsed from a commit message body. | ||
| 15 | /// | ||
| 16 | /// lines with parsing failures will be skipped | ||
| 17 | pub struct Trailers<'a> { | ||
| 18 | pub(crate) cursor: &'a [u8], | ||
| 19 | } | ||
| 20 | |||
| 21 | /// A trailer as parsed from the commit message body. | ||
| 22 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] | ||
| 23 | #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] | ||
| 24 | pub struct TrailerRef<'a> { | ||
| 25 | /// The name of the trailer, like "Signed-off-by", up to the separator ": " | ||
| 26 | #[cfg_attr(feature = "serde1", serde(borrow))] | ||
| 27 | pub token: &'a BStr, | ||
| 28 | /// The value right after the separator ": ", with leading and trailing whitespace trimmed. | ||
| 29 | /// Note that multi-line values aren't currently supported. | ||
| 30 | pub value: &'a BStr, | ||
| 31 | } | ||
| 32 | |||
| 33 | fn parse_single_line_trailer<'a, E: ParseError<&'a [u8]>>(i: &'a [u8]) -> IResult<&'a [u8], (&'a BStr, &'a BStr), E> { | ||
| 34 | let (value, token) = terminated(take_until1(b":".as_ref()), tag(b": "))(i.trim_end())?; | ||
| 35 | if token.trim_end().len() != token.len() || value.trim_start().len() != value.len() { | ||
| 36 | Err(nom::Err::Failure(E::from_error_kind(i, ErrorKind::Fail))) | ||
| 37 | } else { | ||
| 38 | Ok((&[], (token.as_bstr(), value.as_bstr()))) | ||
| 39 | } | ||
| 40 | } | ||
| 41 | |||
| 42 | #[cfg(test)] | ||
| 43 | mod test_parse_trailer { | ||
| 44 | use super::*; | ||
| 45 | |||
| 46 | fn parse(input: &str) -> (&BStr, &BStr) { | ||
| 47 | parse_single_line_trailer::<()>(input.as_bytes()).unwrap().1 | ||
| 48 | } | ||
| 49 | |||
| 50 | #[test] | ||
| 51 | fn simple_newline() { | ||
| 52 | assert_eq!(parse("foo: bar\n"), ("foo".into(), "bar".into())); | ||
| 53 | } | ||
| 54 | |||
| 55 | #[test] | ||
| 56 | fn simple_non_ascii_no_newline() { | ||
| 57 | assert_eq!(parse("🤗: 🎉"), ("🤗".into(), "🎉".into())); | ||
| 58 | } | ||
| 59 | |||
| 60 | #[test] | ||
| 61 | fn with_lots_of_whitespace_newline() { | ||
| 62 | assert_eq!( | ||
| 63 | parse("hello foo: bar there \n"), | ||
| 64 | ("hello foo".into(), "bar there".into()) | ||
| 65 | ); | ||
| 66 | } | ||
| 67 | |||
| 68 | #[test] | ||
| 69 | fn extra_whitespace_before_token_or_value_is_error() { | ||
| 70 | assert!(parse_single_line_trailer::<()>(b"foo : bar").is_err()); | ||
| 71 | assert!(parse_single_line_trailer::<()>(b"foo: bar").is_err()) | ||
| 72 | } | ||
| 73 | |||
| 74 | #[test] | ||
| 75 | fn simple_newline_windows() { | ||
| 76 | assert_eq!(parse("foo: bar\r\n"), ("foo".into(), "bar".into())); | ||
| 77 | } | ||
| 78 | } | ||
| 79 | |||
| 80 | impl<'a> Iterator for Trailers<'a> { | ||
| 81 | type Item = TrailerRef<'a>; | ||
| 82 | |||
| 83 | fn next(&mut self) -> Option<Self::Item> { | ||
| 84 | if self.cursor.is_empty() { | ||
| 85 | return None; | ||
| 86 | } | ||
| 87 | for line in self.cursor.lines_with_terminator() { | ||
| 88 | self.cursor = &self.cursor[line.len()..]; | ||
| 89 | if let Some(trailer) = | ||
| 90 | all_consuming(parse_single_line_trailer::<()>)(line) | ||
| 91 | .ok() | ||
| 92 | .map(|(_, (token, value))| TrailerRef { | ||
| 93 | token: token.trim().as_bstr(), | ||
| 94 | value: value.trim().as_bstr(), | ||
| 95 | }) | ||
| 96 | { | ||
| 97 | return Some(trailer); | ||
| 98 | } | ||
| 99 | } | ||
| 100 | None | ||
| 101 | } | ||
| 102 | } | ||
| 103 | |||
| 104 | impl<'a> BodyRef<'a> { | ||
| 105 | /// Parse `body` bytes into the trailer and the actual body. | ||
| 106 | pub fn from_bytes(body: &'a [u8]) -> Self { | ||
| 107 | body.rfind(b"\n\n") | ||
| 108 | .map(|pos| (2, pos)) | ||
| 109 | .or_else(|| body.rfind(b"\r\n\r\n").map(|pos| (4, pos))) | ||
| 110 | .and_then(|(sep_len, pos)| { | ||
| 111 | let trailer = &body[pos + sep_len..]; | ||
| 112 | let body = &body[..pos]; | ||
| 113 | Trailers { cursor: trailer }.next().map(|_| BodyRef { | ||
| 114 | body_without_trailer: body.as_bstr(), | ||
| 115 | start_of_trailer: trailer, | ||
| 116 | }) | ||
| 117 | }) | ||
| 118 | .unwrap_or_else(|| BodyRef { | ||
| 119 | body_without_trailer: body.as_bstr(), | ||
| 120 | start_of_trailer: &[], | ||
| 121 | }) | ||
| 122 | } | ||
| 123 | } | ||
| @@ -0,0 +1,57 @@ | |||
| 1 | use nom::{ | ||
| 2 | branch::alt, | ||
| 3 | bytes::complete::{tag, take_till1}, | ||
| 4 | combinator::all_consuming, | ||
| 5 | error::ParseError, | ||
| 6 | sequence::pair, | ||
| 7 | IResult, | ||
| 8 | }; | ||
| 9 | |||
| 10 | use crate::bstr::{BStr, ByteSlice}; | ||
| 11 | |||
| 12 | pub(crate) fn newline<'a, E: ParseError<&'a [u8]>>(i: &'a [u8]) -> IResult<&'a [u8], &'a [u8], E> { | ||
| 13 | alt((tag(b"\r\n"), tag(b"\n")))(i) | ||
| 14 | } | ||
| 15 | |||
| 16 | fn subject_and_body<'a, E: ParseError<&'a [u8]>>(i: &'a [u8]) -> IResult<&'a [u8], (&'a BStr, Option<&'a BStr>), E> { | ||
| 17 | let mut c = i; | ||
| 18 | let mut consumed_bytes = 0; | ||
| 19 | while !c.is_empty() { | ||
| 20 | c = match take_till1::<_, _, E>(|c| c == b'\n' || c == b'\r')(c) { | ||
| 21 | Ok((i1, segment)) => { | ||
| 22 | consumed_bytes += segment.len(); | ||
| 23 | match pair::<_, _, _, E, _, _>(newline, newline)(i1) { | ||
| 24 | Ok((body, _)) => { | ||
| 25 | return Ok(( | ||
| 26 | &[], | ||
| 27 | ( | ||
| 28 | i[0usize..consumed_bytes].as_bstr(), | ||
| 29 | (!body.is_empty()).then(|| body.as_bstr()), | ||
| 30 | ), | ||
| 31 | )); | ||
| 32 | } | ||
| 33 | Err(_) => match i1.get(1..) { | ||
| 34 | Some(next) => { | ||
| 35 | consumed_bytes += 1; | ||
| 36 | next | ||
| 37 | } | ||
| 38 | None => break, | ||
| 39 | }, | ||
| 40 | } | ||
| 41 | } | ||
| 42 | Err(_) => match c.get(1..) { | ||
| 43 | Some(next) => { | ||
| 44 | consumed_bytes += 1; | ||
| 45 | next | ||
| 46 | } | ||
| 47 | None => break, | ||
| 48 | }, | ||
| 49 | }; | ||
| 50 | } | ||
| 51 | Ok((&[], (i.as_bstr(), None))) | ||
| 52 | } | ||
| 53 | |||
| 54 | /// Returns title and body, without separator | ||
| 55 | pub fn message(input: &[u8]) -> (&BStr, Option<&BStr>) { | ||
| 56 | all_consuming(subject_and_body::<()>)(input).expect("cannot fail").1 | ||
| 57 | } | ||
| @@ -0,0 +1,123 @@ | |||
| 1 | use std::{borrow::Cow, ops::Deref}; | ||
| 2 | |||
| 3 | use crate::{ | ||
| 4 | bstr::{BStr, BString, ByteSlice, ByteVec}, | ||
| 5 | commit::MessageRef, | ||
| 6 | CommitRef, | ||
| 7 | }; | ||
| 8 | |||
| 9 | /// | ||
| 10 | pub mod body; | ||
| 11 | mod decode; | ||
| 12 | |||
| 13 | impl<'a> CommitRef<'a> { | ||
| 14 | /// Return exactly the same message as [`MessageRef::summary()`]. | ||
| 15 | pub fn message_summary(&self) -> Cow<'a, BStr> { | ||
| 16 | summary(self.message) | ||
| 17 | } | ||
| 18 | |||
| 19 | /// Return an iterator over message trailers as obtained from the last paragraph of the commit message. | ||
| 20 | /// May be empty. | ||
| 21 | pub fn message_trailers(&self) -> body::Trailers<'a> { | ||
| 22 | BodyRef::from_bytes(&self.message).trailers() | ||
| 23 | } | ||
| 24 | } | ||
| 25 | |||
| 26 | impl<'a> MessageRef<'a> { | ||
| 27 | /// Parse the given `input` as message. | ||
| 28 | /// | ||
| 29 | /// Note that this cannot fail as everything will be interpreted as title if there is no body separator. | ||
| 30 | pub fn from_bytes(input: &'a [u8]) -> Self { | ||
| 31 | let (title, body) = decode::message(input); | ||
| 32 | MessageRef { title, body } | ||
| 33 | } | ||
| 34 | |||
| 35 | /// Produce a short commit summary for the message title. | ||
| 36 | /// | ||
| 37 | /// This means the following | ||
| 38 | /// | ||
| 39 | /// * Take the subject line which is delimited by two newlines (\n\n) | ||
| 40 | /// * transform intermediate consecutive whitespace including \r into one space | ||
| 41 | /// | ||
| 42 | /// The resulting summary will have folded whitespace before a newline into spaces and stopped that process | ||
| 43 | /// once two consecutive newlines are encountered. | ||
| 44 | pub fn summary(&self) -> Cow<'a, BStr> { | ||
| 45 | summary(self.title) | ||
| 46 | } | ||
| 47 | |||
| 48 | /// Further parse the body into into non-trailer and trailers, which can be iterated from the returned [`BodyRef`]. | ||
| 49 | pub fn body(&self) -> Option<BodyRef<'a>> { | ||
| 50 | self.body.map(|b| BodyRef::from_bytes(b)) | ||
| 51 | } | ||
| 52 | } | ||
| 53 | |||
| 54 | pub(crate) fn summary(message: &BStr) -> Cow<'_, BStr> { | ||
| 55 | let message = message.trim(); | ||
| 56 | match message.find_byte(b'\n') { | ||
| 57 | Some(mut pos) => { | ||
| 58 | let mut out = BString::default(); | ||
| 59 | let mut previous_pos = None; | ||
| 60 | loop { | ||
| 61 | if let Some(previous_pos) = previous_pos { | ||
| 62 | if previous_pos + 1 == pos { | ||
| 63 | let len_after_trim = out.trim_end().len(); | ||
| 64 | out.resize(len_after_trim, 0); | ||
| 65 | break out.into(); | ||
| 66 | } | ||
| 67 | } | ||
| 68 | let message_to_newline = &message[previous_pos.map(|p| p + 1).unwrap_or(0)..pos]; | ||
| 69 | |||
| 70 | if let Some(pos_before_whitespace) = message_to_newline.rfind_not_byteset(b"\t\n\x0C\r ") { | ||
| 71 | out.extend_from_slice(&message_to_newline[..pos_before_whitespace + 1]); | ||
| 72 | } | ||
| 73 | out.push_byte(b' '); | ||
| 74 | previous_pos = Some(pos); | ||
| 75 | match message.get(pos + 1..).and_then(|i| i.find_byte(b'\n')) { | ||
| 76 | Some(next_nl_pos) => pos += next_nl_pos + 1, | ||
| 77 | None => { | ||
| 78 | if let Some(slice) = message.get((pos + 1)..) { | ||
| 79 | out.extend_from_slice(slice); | ||
| 80 | } | ||
| 81 | break out.into(); | ||
| 82 | } | ||
| 83 | } | ||
| 84 | } | ||
| 85 | } | ||
| 86 | None => message.as_bstr().into(), | ||
| 87 | } | ||
| 88 | } | ||
| 89 | |||
| 90 | /// A reference to a message body, further parsed to only contain the non-trailer parts. | ||
| 91 | /// | ||
| 92 | /// See [git-interpret-trailers](https://git-scm.com/docs/git-interpret-trailers) for more information | ||
| 93 | /// on what constitutes trailers and not that this implementation is only good for typical sign-off footer or key-value parsing. | ||
| 94 | /// | ||
| 95 | /// Note that we only parse trailers from the bottom of the body. | ||
| 96 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] | ||
| 97 | pub struct BodyRef<'a> { | ||
| 98 | body_without_trailer: &'a BStr, | ||
| 99 | start_of_trailer: &'a [u8], | ||
| 100 | } | ||
| 101 | |||
| 102 | impl<'a> BodyRef<'a> { | ||
| 103 | /// Return an iterator over the trailers parsed from the last paragraph of the body. May be empty. | ||
| 104 | pub fn trailers(&self) -> body::Trailers<'a> { | ||
| 105 | body::Trailers { | ||
| 106 | cursor: self.start_of_trailer, | ||
| 107 | } | ||
| 108 | } | ||
| 109 | } | ||
| 110 | |||
| 111 | impl<'a> AsRef<BStr> for BodyRef<'a> { | ||
| 112 | fn as_ref(&self) -> &BStr { | ||
| 113 | self.body_without_trailer | ||
| 114 | } | ||
| 115 | } | ||
| 116 | |||
| 117 | impl<'a> Deref for BodyRef<'a> { | ||
| 118 | type Target = BStr; | ||
| 119 | |||
| 120 | fn deref(&self) -> &Self::Target { | ||
| 121 | self.body_without_trailer | ||
| 122 | } | ||
| 123 | } | ||