auto_commit/formatter/
mod.rs

1use 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        // Replace placeholders
18        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        // Parse prefix and emoji from title
48        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        // Simple parser for conventional commit format
61        // Examples: "feat: ✨ Add feature", "fix: Bug fix", "Update README"
62        
63        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            // Check if the rest starts with an emoji
68            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, // Missing emoji
160            scope: None,
161        };
162
163        let result = formatter.format_message(data).unwrap();
164        assert_eq!(result, "feat:  Add user authentication"); // Empty emoji
165    }
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}