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 | +} | |