use crate::prompt::create_commit_prompt;
use crate::providers::AIProvider;
use crate::types::{CommitorError, ConventionalCommit};
use anyhow::{Context, Result};
use colored::*;
use std::io::{self, Write};
use std::process::Command;
use std::time::Instant;
use tracing::{info, warn};
pub async fn generate_commit_messages(
diff: &str,
provider: &dyn AIProvider,
count: u8,
) -> Result<Vec<String>> {
info!(
"Generating commit messages using provider: {}",
provider.provider_name()
);
let start_time = Instant::now();
let prompt = create_commit_prompt(diff);
let mut messages = Vec::new();
let mut attempts = 0;
let max_attempts = count as usize * 2;
while messages.len() < count as usize && attempts < max_attempts {
attempts += 1;
match provider.generate_message(&prompt).await {
Ok(response) => {
let message = response.trim().to_string();
if !message.is_empty() && is_valid_commit_message(&message) {
if !messages.contains(&message) {
messages.push(message);
}
}
}
Err(e) => {
warn!(
"Failed to generate commit message (attempt {}): {}",
attempts, e
);
if attempts == 1 {
return Err(CommitorError::AIProviderError(e.to_string()).into());
}
}
}
}
let generation_time = start_time.elapsed();
info!(
"Generated {} messages in {:?}",
messages.len(),
generation_time
);
if messages.is_empty() {
return Err(CommitorError::AIProviderError(
"Failed to generate any valid commit messages".to_string(),
)
.into());
}
Ok(messages)
}
pub fn is_valid_commit_message(message: &str) -> bool {
let regex = regex::Regex::new(
r"^(feat|fix|docs|style|refactor|test|chore|perf|ci|build)(\(.+\))?: .+$",
)
.unwrap();
regex.is_match(message) && message.len() <= 72
}
pub fn parse_commit_message(message: &str) -> Result<ConventionalCommit> {
let regex = regex::Regex::new(
r"^(feat|fix|docs|style|refactor|test|chore|perf|ci|build)(\(([^)]+)\))?(!)?: (.+)$",
)
.unwrap();
if let Some(captures) = regex.captures(message) {
let commit_type = match captures.get(1).unwrap().as_str() {
"feat" => crate::types::CommitType::Feat,
"fix" => crate::types::CommitType::Fix,
"docs" => crate::types::CommitType::Docs,
"style" => crate::types::CommitType::Style,
"refactor" => crate::types::CommitType::Refactor,
"test" => crate::types::CommitType::Test,
"chore" => crate::types::CommitType::Chore,
"perf" => crate::types::CommitType::Perf,
"ci" => crate::types::CommitType::Ci,
"build" => crate::types::CommitType::Build,
_ => {
return Err(
CommitorError::InvalidCommitFormat("Unknown commit type".to_string()).into(),
)
}
};
let scope = captures.get(3).map(|m| m.as_str().to_string());
let breaking = captures.get(4).is_some();
let description = captures.get(5).unwrap().as_str().to_string();
let mut commit = ConventionalCommit::new(commit_type, description);
if let Some(scope) = scope {
commit = commit.with_scope(scope);
}
if breaking {
commit = commit.with_breaking();
}
Ok(commit)
} else {
Err(
CommitorError::InvalidCommitFormat("Invalid conventional commit format".to_string())
.into(),
)
}
}
pub fn display_commit_options(messages: &[String]) {
println!("{}", "Generated commit message options:".green().bold());
println!();
for (i, message) in messages.iter().enumerate() {
println!("{} {}", format!("{}.", i + 1).cyan().bold(), message);
}
println!();
}
pub fn prompt_user_choice(count: usize) -> Result<Option<usize>> {
print!(
"{}",
format!("Choose an option (1-{}, or 'q' to quit): ", count).yellow()
);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let input = input.trim();
if input.eq_ignore_ascii_case("q") || input.eq_ignore_ascii_case("quit") {
return Ok(None);
}
match input.parse::<usize>() {
Ok(n) if n >= 1 && n <= count => Ok(Some(n - 1)),
_ => {
println!("{}", "Invalid choice. Please try again.".red());
prompt_user_choice(count)
}
}
}
pub fn commit_with_message(message: &str) -> Result<()> {
println!(
"{}",
format!("Committing with message: {}", message).green()
);
let output = Command::new("git")
.args(["commit", "-m", message])
.output()
.context("Failed to execute git commit")?;
if output.status.success() {
println!("{}", "✓ Commit successful!".green().bold());
if let Ok(hash_output) = Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.output()
{
if hash_output.status.success() {
let hash = String::from_utf8_lossy(&hash_output.stdout)
.trim()
.to_string();
println!("{}", format!("Commit hash: {}", hash).cyan());
}
}
} else {
let error = String::from_utf8_lossy(&output.stderr);
return Err(CommitorError::GitError(error.to_string()).into());
}
Ok(())
}
pub fn validate_git_environment() -> Result<()> {
let git_version = Command::new("git")
.args(["--version"])
.output()
.context("Git is not installed or not available in PATH")?;
if !git_version.status.success() {
return Err(anyhow::anyhow!("Git is not working properly"));
}
let git_status = Command::new("git")
.args(["rev-parse", "--git-dir"])
.output()
.context("Not in a git repository")?;
if !git_status.status.success() {
return Err(CommitorError::GitRepoNotFound.into());
}
Ok(())
}
pub fn get_current_branch() -> Result<String> {
let output = Command::new("git")
.args(["branch", "--show-current"])
.output()
.context("Failed to get current branch")?;
if output.status.success() {
let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok(branch)
} else {
Ok("HEAD".to_string()) }
}
pub fn get_last_commit_message() -> Result<String> {
let output = Command::new("git")
.args(["log", "-1", "--pretty=format:%s"])
.output()
.context("Failed to get last commit message")?;
if output.status.success() {
let message = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok(message)
} else {
Err(anyhow::anyhow!("Failed to get last commit message"))
}
}
pub fn has_uncommitted_changes() -> Result<bool> {
let output = Command::new("git")
.args(["status", "--porcelain"])
.output()
.context("Failed to check git status")?;
if output.status.success() {
let status = String::from_utf8_lossy(&output.stdout);
Ok(!status.trim().is_empty())
} else {
Err(anyhow::anyhow!("Failed to check git status"))
}
}
pub fn enhance_commit_message(message: &str, branch: &str) -> String {
let mut enhanced = message.to_string();
if branch.starts_with("feature/") || branch.starts_with("feat/") {
if !enhanced.starts_with("feat") {
enhanced = format!("feat: {}", enhanced.trim_start_matches("feat: "));
}
} else if branch.starts_with("fix/") || branch.starts_with("bugfix/") {
if !enhanced.starts_with("fix") {
enhanced = format!("fix: {}", enhanced.trim_start_matches("fix: "));
}
}
enhanced
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_valid_commit_message() {
assert!(is_valid_commit_message("feat: add new feature"));
assert!(is_valid_commit_message("fix(auth): resolve login issue"));
assert!(is_valid_commit_message("docs: update README"));
assert!(is_valid_commit_message("style: format code"));
assert!(is_valid_commit_message(
"refactor(utils): simplify helper functions"
));
assert!(is_valid_commit_message("test: add unit tests"));
assert!(is_valid_commit_message("chore: update dependencies"));
assert!(is_valid_commit_message("perf: optimize database queries"));
assert!(is_valid_commit_message("ci: update GitHub Actions"));
assert!(is_valid_commit_message("build: configure webpack"));
assert!(!is_valid_commit_message("invalid message"));
assert!(!is_valid_commit_message("feat"));
assert!(!is_valid_commit_message("feat:"));
assert!(!is_valid_commit_message("feature: add something")); assert!(!is_valid_commit_message(&"feat: ".repeat(100))); }
#[test]
fn test_parse_commit_message() {
let commit = parse_commit_message("feat(auth): add JWT validation").unwrap();
assert_eq!(commit.commit_type, crate::types::CommitType::Feat);
assert_eq!(commit.scope, Some("auth".to_string()));
assert_eq!(commit.description, "add JWT validation");
assert!(!commit.breaking);
let commit = parse_commit_message("fix!: resolve critical bug").unwrap();
assert_eq!(commit.commit_type, crate::types::CommitType::Fix);
assert_eq!(commit.scope, None);
assert_eq!(commit.description, "resolve critical bug");
assert!(commit.breaking);
let commit = parse_commit_message("docs: update README").unwrap();
assert_eq!(commit.commit_type, crate::types::CommitType::Docs);
assert_eq!(commit.scope, None);
assert_eq!(commit.description, "update README");
assert!(!commit.breaking);
assert!(parse_commit_message("invalid message").is_err());
}
#[test]
fn test_enhance_commit_message() {
assert_eq!(
enhance_commit_message("add new feature", "feature/user-auth"),
"feat: add new feature"
);
assert_eq!(
enhance_commit_message("resolve login issue", "fix/auth-bug"),
"fix: resolve login issue"
);
assert_eq!(
enhance_commit_message("feat: add new feature", "main"),
"feat: add new feature"
);
}
}