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