1#[derive(Clone, PartialEq, Eq, Hash)]
2#[non_exhaustive]
3pub struct Commit<'c> {
4 pub raw_subject: &'c str,
5 pub body: Option<&'c str>,
6}
7
8impl<'c> Commit<'c> {
9 pub fn parse(commit: &'c str) -> Result<Self, anyhow::Error> {
10 let (raw_subject, body) = split_parts(commit);
11 let c = Commit { raw_subject, body };
12
13 Ok(c)
14 }
15}
16
17impl crate::style::Style for Commit<'_> {
18 fn subject(&self) -> &str {
19 self.raw_subject
20 }
21
22 fn body(&self) -> Option<&str> {
23 self.body
24 }
25
26 fn type_(&self) -> Option<unicase::UniCase<&str>> {
27 None
28 }
29
30 fn scope(&self) -> Option<unicase::UniCase<&str>> {
31 None
32 }
33}
34
35static SECTION_RE: std::sync::LazyLock<regex::Regex> =
36 std::sync::LazyLock::new(|| regex::Regex::new("\r?\n").unwrap());
37
38fn split_parts(commit: &str) -> (&str, Option<&str>) {
39 let mut sections = SECTION_RE.splitn(commit, 2);
40 let raw_subject = sections.next().expect("Regex should always match");
41 let body = sections.next().map(|s| s.trim()).unwrap_or("");
42 if body.is_empty() {
43 (raw_subject, None)
44 } else {
45 (raw_subject, Some(body))
46 }
47}
48
49#[cfg(test)]
50mod test_split_parts {
51 use super::*;
52
53 #[test]
54 fn subject() {
55 let actual = split_parts("feat(parser): Parse bad greetings");
56 let expected = ("feat(parser): Parse bad greetings", None);
57 assert_eq!(actual, expected);
58 }
59
60 #[test]
61 fn body() {
62 let actual = split_parts(
63 r#"feat(parser): Parse bad greetings
64
65Hello
66World
67
68Foo
69Bar"#,
70 );
71 let expected = (
72 "feat(parser): Parse bad greetings",
73 Some("Hello\nWorld\n\nFoo\nBar"),
74 );
75 assert_eq!(actual, expected);
76 }
77}