use gix::bstr::ByteSlice; use crate::commit::Message; #[derive(Debug)] #[cfg_attr(test, derive(PartialEq, Eq))] pub enum Addition { /// The plain issue ID, like "123". IssueId(String), } mod additions { use std::{borrow::Cow, ops::Range}; use crate::commit::message::Addition; fn cut(mut s: String, Range { start, end }: Range) -> String { let part_to_left = &s[..start]; let new_start = part_to_left .rfind(|c: char| !c.is_whitespace()) .and_then(|p| { part_to_left .is_char_boundary(p + 1) .then_some(p + 1) .or_else(|| part_to_left[p..].chars().next().map(|c| p + c.len_utf8())) }) .unwrap_or(start); let new_end = s[end..] .find(|c: char| !c.is_whitespace()) .map(|p| p + end) .unwrap_or(end); s.replace_range( new_start..new_end, if new_end != end && new_start != start { " " } else { "" }, ); s } pub fn strip(mut title: Cow<'_, str>) -> (Cow<'_, str>, Vec) { let mut additions = Vec::new(); loop { let previous_len = title.len(); let issue_sep = "(#"; if let Some((pos, end_pos)) = title.find(issue_sep).and_then(|mut pos| { pos += issue_sep.len(); title[pos..].find(')').map(|ep| (pos, ep)) }) { additions.push(Addition::IssueId(title[pos..][..end_pos].to_owned())); title = cut(title.into_owned(), (pos - issue_sep.len())..(pos + end_pos + 1)).into(); }; if title.len() == previous_len { break; } } (title, additions) } #[cfg(test)] mod tests { use super::*; #[test] fn no_addition() { let (nt, a) = strip("hello there [abc] (abc)".into()); assert_eq!(nt, "hello there [abc] (abc)"); assert_eq!(a, vec![]); } #[test] fn strip_multiple_issue_numbers() { let (nt, a) = strip("(#other) foo (#123) hello (#42)".into()); assert_eq!(nt, "foo hello"); assert_eq!( a, vec![ Addition::IssueId("other".into()), Addition::IssueId("123".into()), Addition::IssueId("42".into()) ] ); } } } impl From<&'_ str> for Message { fn from(m: &str) -> Self { let (title, kind, body, breaking, breaking_description) = git_conventional::Commit::parse(m) .map(|c: git_conventional::Commit<'_>| { ( c.description().into(), Some(c.type_()), c.body().map(Into::into), c.breaking(), c.breaking_description() .and_then(|d| if d == c.description() { None } else { Some(d) }), ) }) .unwrap_or_else(|_| { let m = gix::objs::commit::MessageRef::from_bytes(m.as_bytes()); ( m.summary().as_ref().to_string().into(), None, m.body().map(|b| b.without_trailer().to_str_lossy()), false, None, ) }); let (title, additions) = additions::strip(title); Message { title: title.into_owned(), kind: as_static_str(kind), body: body.map(|b| b.into_owned()), breaking, breaking_description: breaking_description.map(ToOwned::to_owned), additions, } } } /// Note that this depends on `crate::changelog::section::segment::Conventional::as_headline_name()`, fn as_static_str(kind: Option>) -> Option<&'static str> { kind.map(|kind| match kind.as_str() { "feat" | "add" | "added" => "feat", "fix" => "fix", "revert" | "remove" => "revert", "docs" => "docs", "style" => "style", "refactor" => "refactor", "change" => "change", "perf" => "perf", "test" => "test", "chore" => "chore", _ => "other", }) } #[cfg(test)] mod tests { use super::*; #[test] fn no_conventional_no_additions_no_body() { assert_eq!( Message::from("hi"), Message { title: "hi".into(), body: None, kind: None, breaking: false, breaking_description: None, additions: vec![] } ) } #[test] fn no_conventional_uses_summary() { assert_eq!( Message::from("hi\nho\nfoo\n\nbody"), Message { title: "hi ho foo".into(), body: Some("body".into()), kind: None, breaking: false, breaking_description: None, additions: vec![] } ) } #[test] fn no_conventional_additions() { assert_eq!( Message::from("hi (#14123)\n\nbody\nother\n\nSigned: bar"), Message { title: "hi".into(), body: Some("body\nother".into()), kind: None, breaking: false, breaking_description: None, additions: vec![Addition::IssueId("14123".into())] } ) } #[test] fn conventional_with_additions() { assert_eq!( Message::from("feat!: hi (#123)\n\nthe body\n\nBREAKING-CHANGE: breaks\n\nSigned: foobar"), Message { title: "hi".into(), body: Some("the body".into()), kind: Some("feat"), breaking: true, breaking_description: Some("breaks".into()), additions: vec![Addition::IssueId("123".into())] } ) } }