Skip to main content

standard_commit/
format.rs

1use crate::parse::ConventionalCommit;
2
3/// Format a [`ConventionalCommit`] back into a well-formed conventional commit message string.
4pub fn format(commit: &ConventionalCommit) -> String {
5    let mut msg = String::new();
6
7    // Header: type[(scope)][!]: description
8    msg.push_str(&commit.r#type);
9    if let Some(scope) = &commit.scope {
10        msg.push('(');
11        msg.push_str(scope);
12        msg.push(')');
13    }
14    if commit.is_breaking {
15        msg.push('!');
16    }
17    msg.push_str(": ");
18    msg.push_str(&commit.description);
19
20    // Body
21    if let Some(body) = &commit.body {
22        msg.push_str("\n\n");
23        msg.push_str(body);
24    }
25
26    // Footers
27    if !commit.footers.is_empty() {
28        msg.push_str("\n\n");
29        for (i, footer) in commit.footers.iter().enumerate() {
30            if i > 0 {
31                msg.push('\n');
32            }
33            msg.push_str(&footer.token);
34            msg.push_str(": ");
35            msg.push_str(&footer.value);
36        }
37    }
38
39    msg
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45    use crate::parse::Footer;
46
47    #[test]
48    fn minimal() {
49        let commit = ConventionalCommit {
50            r#type: "feat".into(),
51            scope: None,
52            description: "add login".into(),
53            body: None,
54            footers: vec![],
55            is_breaking: false,
56        };
57        assert_eq!(format(&commit), "feat: add login");
58    }
59
60    #[test]
61    fn with_scope() {
62        let commit = ConventionalCommit {
63            r#type: "fix".into(),
64            scope: Some("auth".into()),
65            description: "handle tokens".into(),
66            body: None,
67            footers: vec![],
68            is_breaking: false,
69        };
70        assert_eq!(format(&commit), "fix(auth): handle tokens");
71    }
72
73    #[test]
74    fn breaking_with_bang() {
75        let commit = ConventionalCommit {
76            r#type: "feat".into(),
77            scope: None,
78            description: "remove legacy API".into(),
79            body: None,
80            footers: vec![],
81            is_breaking: true,
82        };
83        assert_eq!(format(&commit), "feat!: remove legacy API");
84    }
85
86    #[test]
87    fn with_body() {
88        let commit = ConventionalCommit {
89            r#type: "feat".into(),
90            scope: None,
91            description: "add PKCE".into(),
92            body: Some("Full PKCE flow.".into()),
93            footers: vec![],
94            is_breaking: false,
95        };
96        assert_eq!(format(&commit), "feat: add PKCE\n\nFull PKCE flow.");
97    }
98
99    #[test]
100    fn with_footers() {
101        let commit = ConventionalCommit {
102            r#type: "fix".into(),
103            scope: None,
104            description: "fix bug".into(),
105            body: None,
106            footers: vec![
107                Footer {
108                    token: "Refs".into(),
109                    value: "#42".into(),
110                },
111                Footer {
112                    token: "Reviewed-by".into(),
113                    value: "Alice".into(),
114                },
115            ],
116            is_breaking: false,
117        };
118        assert_eq!(
119            format(&commit),
120            "fix: fix bug\n\nRefs: #42\nReviewed-by: Alice"
121        );
122    }
123
124    #[test]
125    fn roundtrip() {
126        let msg = "feat(auth): add OAuth2 PKCE flow";
127        let commit = crate::parse::parse(msg).unwrap();
128        assert_eq!(format(&commit), msg);
129    }
130}