auto_commit/formatter/
mod.rs1use anyhow::Result;
2
3pub struct CommitFormatter {
4 format: Option<String>,
5}
6
7impl CommitFormatter {
8 pub fn new(format: Option<String>) -> Self {
9 Self { format }
10 }
11
12 pub fn format_message(&self, data: CommitData) -> Result<String> {
13 let template = self.format.as_deref().unwrap_or(Self::get_default_format());
14
15 let mut result = template.to_string();
16
17 result = result.replace("{title}", &data.title);
19 result = result.replace("{description}", &data.description);
20 result = result.replace("{prefix}", data.prefix.as_deref().unwrap_or(""));
21 result = result.replace("{emoji}", data.emoji.as_deref().unwrap_or(""));
22 result = result.replace("{scope}", data.scope.as_deref().unwrap_or(""));
23
24 Ok(result)
25 }
26
27 pub fn get_default_format() -> &'static str {
28 "{title}\n\n{description}"
29 }
30}
31
32#[derive(Debug, Clone)]
33pub struct CommitData {
34 pub title: String,
35 pub description: String,
36 pub prefix: Option<String>,
37 pub emoji: Option<String>,
38 pub scope: Option<String>,
39}
40
41impl CommitData {
42 pub fn from_message(message: &str) -> Self {
43 let parts: Vec<&str> = message.splitn(2, "\n\n").collect();
44 let title = parts[0].to_string();
45 let description = parts.get(1).unwrap_or(&"").to_string();
46
47 let (prefix, emoji) = Self::parse_title(&title);
49
50 Self {
51 title,
52 description,
53 prefix,
54 emoji,
55 scope: None,
56 }
57 }
58
59 fn parse_title(title: &str) -> (Option<String>, Option<String>) {
60 if let Some(colon_pos) = title.find(':') {
64 let prefix = title[..colon_pos].trim().to_string();
65 let rest = title[colon_pos + 1..].trim();
66
67 let mut emoji = None;
69 let mut chars = rest.chars();
70
71 if let Some(first_char) = chars.next() {
72 if is_emoji(first_char) {
73 emoji = Some(first_char.to_string());
74 }
75 }
76
77 (Some(prefix), emoji)
78 } else {
79 (None, None)
80 }
81 }
82}
83
84fn is_emoji(c: char) -> bool {
85 matches!(c, '✨' | '🐛' | '📝' | '💄' | '♻' | '🚀' | '💚' | '🍱' | '👍' | '🔥' | '🎨' | '⚙' | '🚧' | '💢' | '🆙' | '👮' | '👕' | '⏪' | '📛' | '🚨' | '💡')
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn test_default_format() {
94 let formatter = CommitFormatter::new(None);
95 let data = CommitData {
96 title: "feat: Add user authentication".to_string(),
97 description: "Implemented JWT-based authentication".to_string(),
98 prefix: Some("feat".to_string()),
99 emoji: None,
100 scope: None,
101 };
102
103 let result = formatter.format_message(data).unwrap();
104 assert_eq!(result, "feat: Add user authentication\n\nImplemented JWT-based authentication");
105 }
106
107 #[test]
108 fn test_custom_format_with_prefix() {
109 let formatter = CommitFormatter::new(Some("{prefix}: {title}".to_string()));
110 let data = CommitData {
111 title: "Add user authentication".to_string(),
112 description: "Implemented JWT-based authentication".to_string(),
113 prefix: Some("feat".to_string()),
114 emoji: None,
115 scope: None,
116 };
117
118 let result = formatter.format_message(data).unwrap();
119 assert_eq!(result, "feat: Add user authentication");
120 }
121
122 #[test]
123 fn test_custom_format_with_emoji() {
124 let formatter = CommitFormatter::new(Some("{prefix}: {emoji} {title}".to_string()));
125 let data = CommitData {
126 title: "Add user authentication".to_string(),
127 description: "Implemented JWT-based authentication".to_string(),
128 prefix: Some("feat".to_string()),
129 emoji: Some("✨".to_string()),
130 scope: None,
131 };
132
133 let result = formatter.format_message(data).unwrap();
134 assert_eq!(result, "feat: ✨ Add user authentication");
135 }
136
137 #[test]
138 fn test_custom_format_with_scope() {
139 let formatter = CommitFormatter::new(Some("{prefix}({scope}): {title}".to_string()));
140 let data = CommitData {
141 title: "Add login endpoint".to_string(),
142 description: "Added POST /api/login endpoint".to_string(),
143 prefix: Some("feat".to_string()),
144 emoji: None,
145 scope: Some("api".to_string()),
146 };
147
148 let result = formatter.format_message(data).unwrap();
149 assert_eq!(result, "feat(api): Add login endpoint");
150 }
151
152 #[test]
153 fn test_format_with_missing_placeholders() {
154 let formatter = CommitFormatter::new(Some("{prefix}: {emoji} {title}".to_string()));
155 let data = CommitData {
156 title: "Add user authentication".to_string(),
157 description: "Implemented JWT-based authentication".to_string(),
158 prefix: Some("feat".to_string()),
159 emoji: None, scope: None,
161 };
162
163 let result = formatter.format_message(data).unwrap();
164 assert_eq!(result, "feat: Add user authentication"); }
166
167 #[test]
168 fn test_parse_title_with_prefix() {
169 let data = CommitData::from_message("fix: Resolve memory leak\n\nFixed memory issue");
170 assert_eq!(data.title, "fix: Resolve memory leak");
171 assert_eq!(data.description, "Fixed memory issue");
172 assert_eq!(data.prefix, Some("fix".to_string()));
173 }
174
175 #[test]
176 fn test_parse_title_with_prefix_and_emoji() {
177 let data = CommitData::from_message("feat: ✨ Add dark mode\n\nAdded theme switching");
178 assert_eq!(data.title, "feat: ✨ Add dark mode");
179 assert_eq!(data.prefix, Some("feat".to_string()));
180 assert_eq!(data.emoji, Some("✨".to_string()));
181 }
182
183 #[test]
184 fn test_parse_title_without_prefix() {
185 let data = CommitData::from_message("Update README.md\n\nAdded installation instructions");
186 assert_eq!(data.title, "Update README.md");
187 assert_eq!(data.prefix, None);
188 assert_eq!(data.emoji, None);
189 }
190}