diff --git a/src/generator.rs b/src/generator.rs new file mode 100644 index 0000000..cc74056 --- /dev/null +++ b/src/generator.rs @@ -0,0 +1,166 @@ +use crate::analyzer::{AnalysisResult, CommitType}; +use crate::git::StagedChanges; + +pub fn generate_message(analysis: &AnalysisResult, staged: &StagedChanges) -> String { + let scope = analysis.scope.clone().unwrap_or_default(); + let description = &analysis.description; + + if !scope.is_empty() { + format!("{} ({}): {}", analysis.commit_type, scope, description) + } else { + format!("{}: {}", analysis.commit_type, description) + } +} + +pub fn generate_alternative_messages( + analysis: &AnalysisResult, + staged: &StagedChanges, +) -> Vec { + let mut messages = Vec::new(); + + let scope = analysis.scope.clone().unwrap_or_default(); + let base_description = &analysis.description; + + let verb_alternatives = vec!["update", "modify", "change", "revise"]; + + if !scope.is_empty() { + for verb in &verb_alternatives { + let desc = verb_alternatives_description(verb, &analysis.description, &staged.files); + messages.push(format!("{} ({}): {}", analysis.commit_type, scope, desc)); + } + } else { + for verb in &verb_alternatives { + let desc = verb_alternatives_description(verb, &analysis.description, &staged.files); + messages.push(format!("{}: {}", analysis.commit_type, desc)); + } + } + + messages +} + +fn verb_alternatives_description( + verb: &str, + base: &str, + files: &[crate::git::ChangedFile], +) -> String { + let file_count = files.len(); + + if file_count <= 3 && !files.is_empty() { + let file_names: Vec = files + .iter() + .map(|f| f.path.split('/').last().unwrap_or(&f.path).to_string()) + .collect(); + let file_list = file_names.join(", "); + format!("{} {}", verb, file_list) + } else { + format!("{} {} files", verb, file_count) + } +} + +pub fn format_message(message: &str) -> String { + if message.len() <= 72 { + return message.to_string(); + } + + let words: Vec<&str> = message.split_whitespace().collect(); + let mut lines = Vec::new(); + let mut current_line = String::new(); + + for word in words { + if current_line.is_empty() { + current_line = word.to_string(); + } else if current_line.len() + 1 + word.len() <= 72 { + current_line.push(' '); + current_line.push_str(word); + } else { + lines.push(current_line); + current_line = word.to_string(); + } + } + + if !current_line.is_empty() { + lines.push(current_line); + } + + lines.join("\n") +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::analyzer::{AnalysisResult, CommitType}; + use crate::git::{ChangedFile, FileStatus, StagedChanges}; + + #[test] + fn test_message_format_with_scope() { + let analysis = AnalysisResult { + commit_type: CommitType::Feat, + scope: Some(String::from("auth")), + confidence: 0.9, + description: String::from("add login functionality"), + reasons: vec![], + }; + let staged = StagedChanges { + files: vec![], + diff_text: String::new(), + }; + let message = generate_message(&analysis, &staged); + assert_eq!(message, "feat(auth): add login functionality"); + } + + #[test] + fn test_message_format_without_scope() { + let analysis = AnalysisResult { + commit_type: CommitType::Fix, + scope: None, + confidence: 0.9, + description: String::from("resolve null pointer exception"), + reasons: vec![], + }; + let staged = StagedChanges { + files: vec![], + diff_text: String::new(), + }; + let message = generate_message(&analysis, &staged); + assert_eq!(message, "fix: resolve null pointer exception"); + } + + #[test] + fn test_scope_extraction() { + let analysis = AnalysisResult { + commit_type: CommitType::Test, + scope: Some(String::from("api")), + confidence: 0.9, + description: String::from("add API endpoint tests"), + reasons: vec![], + }; + let staged = StagedChanges { + files: vec![ChangedFile { + path: String::from("api/users_test.rs"), + status: FileStatus::Added, + additions: 50, + deletions: 0, + is_new: true, + is_deleted: false, + is_renamed: false, + old_path: None, + }], + diff_text: String::new(), + }; + let message = generate_message(&analysis, &staged); + assert_eq!(message, "test(api): add API endpoint tests"); + } + + #[test] + fn test_format_message_short() { + let message = format_message("feat: add new feature"); + assert_eq!(message, "feat: add new feature"); + } + + #[test] + fn test_format_message_long() { + let long_desc = "this is a very long description that should be wrapped because it exceeds seventy two characters in length"; + let message = format_message(&format!("feat: {}", long_desc)); + assert!(message.contains('\n')); + } +}