Showing the changes to git-object/src/commit (4 of 4 files). Show all
Unified · Split
| @@ -1,309 +0,0 @@ | |||
| 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 | use nom::{ | ||
| 12 | bytes::complete::{tag, take_until1}, | ||
| 13 | combinator::all_consuming, | ||
| 14 | error::{ErrorKind, ParseError}, | ||
| 15 | sequence::terminated, | ||
| 16 | IResult, | ||
| 17 | }; | ||
| 18 | |||
| 19 | use crate::{ | ||
| 20 | bstr::{BStr, ByteSlice}, | ||
| 21 | commit::message::BodyRef, | ||
| 22 | }; | ||
| 23 | |||
| 24 | /// An iterator over trailers as parsed from a commit message body. | ||
| 25 | /// | ||
| 26 | /// lines with parsing failures will be skipped | ||
| 27 | pub struct Trailers<'a> { | ||
| 28 | pub(crate) cursor: &'a [u8], | ||
| 29 | } | ||
| 30 | |||
| 31 | /// A trailer as parsed from the commit message body. | ||
| 32 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] | ||
| 33 | #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] | ||
| 34 | pub struct TrailerRef<'a> { | ||
| 35 | /// The name of the trailer, like "Signed-off-by", up to the separator ": " | ||
| 36 | #[cfg_attr(feature = "serde1", serde(borrow))] | ||
| 37 | pub token: &'a BStr, | ||
| 38 | /// The value right after the separator ": ", with leading and trailing whitespace trimmed. | ||
| 39 | /// Note that multi-line values aren't currently supported. | ||
| 40 | pub value: &'a BStr, | ||
| 41 | } | ||
| 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() || value.trim_start().len() != value.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 extra_whitespace_before_token_or_value_is_error() { | ||
| 82 | assert!(parse_single_line_trailer::<()>(b"foo : bar").is_err()); | ||
| 83 | assert!(parse_single_line_trailer::<()>(b"foo: bar").is_err()) | ||
| 84 | } | ||
| 85 | |||
| 86 | #[test] | ||
| 87 | fn simple_newline_windows() { | ||
| 88 | assert_eq!(parse("foo: bar\r\n"), ("foo".into(), "bar".into())); | ||
| 89 | } | ||
| 90 | } | ||
| 91 | |||
| 92 | impl<'a> Iterator for Trailers<'a> { | ||
| 93 | type Item = TrailerRef<'a>; | ||
| 94 | |||
| 95 | fn next(&mut self) -> Option<Self::Item> { | ||
| 96 | if self.cursor.is_empty() { | ||
| 97 | return None; | ||
| 98 | } | ||
| 99 | for line in self.cursor.lines_with_terminator() { | ||
| 100 | self.cursor = &self.cursor[line.len()..]; | ||
| 101 | if let Some(trailer) = | ||
| 102 | all_consuming(parse_single_line_trailer::<()>)(line) | ||
| 103 | .ok() | ||
| 104 | .map(|(_, (token, value))| TrailerRef { | ||
| 105 | token: token.trim().as_bstr(), | ||
| 106 | value: value.trim().as_bstr(), | ||
| 107 | }) | ||
| 108 | { | ||
| 109 | return Some(trailer); | ||
| 110 | } | ||
| 111 | } | ||
| 112 | None | ||
| 113 | } | ||
| 114 | } | ||
| 115 | |||
| 116 | impl<'a> BodyRef<'a> { | ||
| 117 | /// Parse `body` bytes into the trailer and the actual body. | ||
| 118 | pub fn from_bytes(body: &'a [u8]) -> Self { | ||
| 119 | body.rfind(b"\n\n") | ||
| 120 | .map(|pos| (2, pos)) | ||
| 121 | .or_else(|| body.rfind(b"\r\n\r\n").map(|pos| (4, pos))) | ||
| 122 | .and_then(|(sep_len, pos)| { | ||
| 123 | let trailer = &body[pos + sep_len..]; | ||
| 124 | let body = &body[..pos]; | ||
| 125 | Trailers { cursor: trailer }.next().map(|_| BodyRef { | ||
| 126 | body_without_trailer: body.as_bstr(), | ||
| 127 | start_of_trailer: trailer, | ||
| 128 | }) | ||
| 129 | }) | ||
| 130 | .unwrap_or_else(|| BodyRef { | ||
| 131 | body_without_trailer: body.as_bstr(), | ||
| 132 | start_of_trailer: &[], | ||
| 133 | }) | ||
| 134 | } | ||
| 135 | } | ||
| 136 | } | ||
| 137 | mod decode { | ||
| 138 | use nom::{ | ||
| 139 | branch::alt, | ||
| 140 | bytes::complete::{tag, take_till1}, | ||
| 141 | combinator::all_consuming, | ||
| 142 | error::ParseError, | ||
| 143 | sequence::pair, | ||
| 144 | IResult, | ||
| 145 | }; | ||
| 146 | |||
| 147 | use crate::bstr::{BStr, ByteSlice}; | ||
| 148 | |||
| 149 | pub(crate) fn newline<'a, E: ParseError<&'a [u8]>>(i: &'a [u8]) -> IResult<&'a [u8], &'a [u8], E> { | ||
| 150 | alt((tag(b"\r\n"), tag(b"\n")))(i) | ||
| 151 | } | ||
| 152 | |||
| 153 | fn subject_and_body<'a, E: ParseError<&'a [u8]>>( | ||
| 154 | i: &'a [u8], | ||
| 155 | ) -> IResult<&'a [u8], (&'a BStr, Option<&'a BStr>), E> { | ||
| 156 | let mut c = i; | ||
| 157 | let mut consumed_bytes = 0; | ||
| 158 | while !c.is_empty() { | ||
| 159 | c = match take_till1::<_, _, E>(|c| c == b'\n' || c == b'\r')(c) { | ||
| 160 | Ok((i1, segment)) => { | ||
| 161 | consumed_bytes += segment.len(); | ||
| 162 | match pair::<_, _, _, E, _, _>(newline, newline)(i1) { | ||
| 163 | Ok((body, _)) => { | ||
| 164 | return Ok(( | ||
| 165 | &[], | ||
| 166 | ( | ||
| 167 | i[0usize..consumed_bytes].as_bstr(), | ||
| 168 | (!body.is_empty()).then(|| body.as_bstr()), | ||
| 169 | ), | ||
| 170 | )); | ||
| 171 | } | ||
| 172 | Err(_) => match i1.get(1..) { | ||
| 173 | Some(next) => { | ||
| 174 | consumed_bytes += 1; | ||
| 175 | next | ||
| 176 | } | ||
| 177 | None => break, | ||
| 178 | }, | ||
| 179 | } | ||
| 180 | } | ||
| 181 | Err(_) => match c.get(1..) { | ||
| 182 | Some(next) => { | ||
| 183 | consumed_bytes += 1; | ||
| 184 | next | ||
| 185 | } | ||
| 186 | None => break, | ||
| 187 | }, | ||
| 188 | }; | ||
| 189 | } | ||
| 190 | Ok((&[], (i.as_bstr(), None))) | ||
| 191 | } | ||
| 192 | |||
| 193 | /// Returns title and body, without separator | ||
| 194 | pub fn message(input: &[u8]) -> (&BStr, Option<&BStr>) { | ||
| 195 | all_consuming(subject_and_body::<()>)(input).expect("cannot fail").1 | ||
| 196 | } | ||
| 197 | } | ||
| 198 | |||
| 199 | impl<'a> CommitRef<'a> { | ||
| 200 | /// Return exactly the same message as [`MessageRef::summary()`]. | ||
| 201 | pub fn message_summary(&self) -> Cow<'a, BStr> { | ||
| 202 | summary(self.message) | ||
| 203 | } | ||
| 204 | |||
| 205 | /// Return an iterator over message trailers as obtained from the last paragraph of the commit message. | ||
| 206 | /// May be empty. | ||
| 207 | pub fn message_trailers(&self) -> body::Trailers<'a> { | ||
| 208 | BodyRef::from_bytes(&self.message).trailers() | ||
| 209 | } | ||
| 210 | } | ||
| 211 | |||
| 212 | impl<'a> MessageRef<'a> { | ||
| 213 | /// Parse the given `input` as message. | ||
| 214 | /// | ||
| 215 | /// Note that this cannot fail as everything will be interpreted as title if there is no body separator. | ||
| 216 | pub fn from_bytes(input: &'a [u8]) -> Self { | ||
| 217 | let (title, body) = decode::message(input); | ||
| 218 | MessageRef { title, body } | ||
| 219 | } | ||
| 220 | |||
| 221 | /// Produce a short commit summary for the message title. | ||
| 222 | /// | ||
| 223 | /// This means the following | ||
| 224 | /// | ||
| 225 | /// * Take the subject line which is delimited by two newlines (\n\n) | ||
| 226 | /// * transform intermediate consecutive whitespace including \r into one space | ||
| 227 | /// | ||
| 228 | /// The resulting summary will have folded whitespace before a newline into spaces and stopped that process | ||
| 229 | /// once two consecutive newlines are encountered. | ||
| 230 | pub fn summary(&self) -> Cow<'a, BStr> { | ||
| 231 | summary(self.title) | ||
| 232 | } | ||
| 233 | |||
| 234 | /// Further parse the body into into non-trailer and trailers, which can be iterated from the returned [`BodyRef`]. | ||
| 235 | pub fn body(&self) -> Option<BodyRef<'a>> { | ||
| 236 | self.body.map(|b| BodyRef::from_bytes(b)) | ||
| 237 | } | ||
| 238 | } | ||
| 239 | |||
| 240 | pub(crate) fn summary(message: &BStr) -> Cow<'_, BStr> { | ||
| 241 | let message = message.trim(); | ||
| 242 | match message.find_byte(b'\n') { | ||
| 243 | Some(mut pos) => { | ||
| 244 | let mut out = BString::default(); | ||
| 245 | let mut previous_pos = None; | ||
| 246 | loop { | ||
| 247 | if let Some(previous_pos) = previous_pos { | ||
| 248 | if previous_pos + 1 == pos { | ||
| 249 | let len_after_trim = out.trim_end().len(); | ||
| 250 | out.resize(len_after_trim, 0); | ||
| 251 | break out.into(); | ||
| 252 | } | ||
| 253 | } | ||
| 254 | let message_to_newline = &message[previous_pos.map(|p| p + 1).unwrap_or(0)..pos]; | ||
| 255 | |||
| 256 | if let Some(pos_before_whitespace) = message_to_newline.rfind_not_byteset(b"\t\n\x0C\r ") { | ||
| 257 | out.extend_from_slice(&message_to_newline[..pos_before_whitespace + 1]); | ||
| 258 | } | ||
| 259 | out.push_byte(b' '); | ||
| 260 | previous_pos = Some(pos); | ||
| 261 | match message.get(pos + 1..).and_then(|i| i.find_byte(b'\n')) { | ||
| 262 | Some(next_nl_pos) => pos += next_nl_pos + 1, | ||
| 263 | None => { | ||
| 264 | if let Some(slice) = message.get((pos + 1)..) { | ||
| 265 | out.extend_from_slice(slice); | ||
| 266 | } | ||
| 267 | break out.into(); | ||
| 268 | } | ||
| 269 | } | ||
| 270 | } | ||
| 271 | } | ||
| 272 | None => message.as_bstr().into(), | ||
| 273 | } | ||
| 274 | } | ||
| 275 | |||
| 276 | /// A reference to a message body, further parsed to only contain the non-trailer parts. | ||
| 277 | /// | ||
| 278 | /// See [git-interpret-trailers](https://git-scm.com/docs/git-interpret-trailers) for more information | ||
| 279 | /// on what constitutes trailers and not that this implementation is only good for typical sign-off footer or key-value parsing. | ||
| 280 | /// | ||
| 281 | /// Note that we only parse trailers from the bottom of the body. | ||
| 282 | #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] | ||
| 283 | pub struct BodyRef<'a> { | ||
| 284 | body_without_trailer: &'a BStr, | ||
| 285 | start_of_trailer: &'a [u8], | ||
| 286 | } | ||
| 287 | |||
| 288 | impl<'a> BodyRef<'a> { | ||
| 289 | /// Return an iterator over the trailers parsed from the last paragraph of the body. May be empty. | ||
| 290 | pub fn trailers(&self) -> body::Trailers<'a> { | ||
| 291 | body::Trailers { | ||
| 292 | cursor: self.start_of_trailer, | ||
| 293 | } | ||
| 294 | } | ||
| 295 | } | ||
| 296 | |||
| 297 | impl<'a> AsRef<BStr> for BodyRef<'a> { | ||
| 298 | fn as_ref(&self) -> &BStr { | ||
| 299 | self.body_without_trailer | ||
| 300 | } | ||
| 301 | } | ||
| 302 | |||
| 303 | impl<'a> Deref for BodyRef<'a> { | ||
| 304 | type Target = BStr; | ||
| 305 | |||
| 306 | fn deref(&self) -> &Self::Target { | ||
| 307 | self.body_without_trailer | ||
| 308 | } | ||
| 309 | } | ||
| @@ -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 | } | ||