use anyhow::{bail, Context, Result}; use clap::Parser; use colored::Colorize; use dialoguer::{Confirm, Input, Select}; use crate::config::{Language, manager::ConfigManager}; use crate::config::CommitFormat; use crate::generator::ContentGenerator; use crate::git::{find_repo, GitRepo}; use crate::git::commit::{CommitBuilder, create_date_commit_message}; use crate::i18n::Messages; use crate::utils::validators::get_commit_types; /// Generate and execute conventional commits #[derive(Parser)] pub struct CommitCommand { /// Commit type #[arg(long)] commit_type: Option, /// Commit scope #[arg(short, long)] scope: Option, /// Commit description/subject #[arg(short, long)] message: Option, /// Commit body #[arg(long)] body: Option, /// Mark as breaking change #[arg(short, long)] breaking: bool, /// Use date-based commit message #[arg(short, long)] date: bool, /// Manual input (skip AI generation) #[arg(long)] manual: bool, /// Sign the commit #[arg(short = 'S', long)] sign: bool, /// Amend previous commit #[arg(long)] amend: bool, /// Stage all changes before committing #[arg(short = 'a', long)] all: bool, /// Dry run (show message without committing) #[arg(long)] dry_run: bool, /// Use conventional commit format #[arg(long, group = "format")] conventional: bool, /// Use commitlint format #[arg(long, group = "format")] commitlint: bool, /// Don't verify commit message #[arg(long)] no_verify: bool, /// Skip interactive prompts #[arg(short = 'y', long)] yes: bool, /// Push after committing #[arg(long)] push: bool, /// Remote to push to #[arg(long, default_value = "origin")] remote: String, } impl CommitCommand { pub async fn execute(&self) -> Result<()> { // Find git repository let repo = find_repo(std::env::current_dir()?.as_path())?; // Load configuration let manager = ConfigManager::new()?; let config = manager.config(); let language = manager.get_language().unwrap_or(Language::English); let messages = Messages::new(language); // Check for changes let status = repo.status_summary()?; if status.clean && !self.amend { bail!("{}", messages.no_changes()); } // Determine commit format let format = if self.conventional { CommitFormat::Conventional } else if self.commitlint { CommitFormat::Commitlint } else { config.commit.format }; // Auto-add if no files are staged and there are unstaged/untracked changes if status.staged == 0 && (status.unstaged > 0 || status.untracked > 0) && !self.all { println!("{}", messages.auto_stage_changes().yellow()); repo.stage_all()?; println!("{}", messages.staged_all().green()); // Re-check status after staging to ensure changes are detected let new_status = repo.status_summary()?; if new_status.staged == 0 { bail!("Failed to stage changes. Please try running 'git add -A' manually."); } } // Stage all if requested if self.all { repo.stage_all()?; println!("{}", messages.staged_all().green()); } // Generate or build commit message let commit_message = if self.date { // Date-based commit self.create_date_commit() } else if self.manual || self.message.is_some() { // Manual commit self.create_manual_commit(format)? } else if config.commit.auto_generate { // AI-generated commit self.generate_commit(&repo, format, &messages).await? } else { // Interactive commit creation self.create_interactive_commit(format, &messages).await? }; // Validate message match format { CommitFormat::Conventional => { crate::utils::validators::validate_conventional_commit(&commit_message)?; } CommitFormat::Commitlint => { crate::utils::validators::validate_commitlint_commit(&commit_message)?; } } // Show commit preview if !self.yes { println!("\n{}", "─".repeat(60)); println!("{}", messages.commit_preview().bold()); println!("{}", "─".repeat(60)); println!("{}", commit_message); println!("{}", "─".repeat(60)); let confirm = Confirm::new() .with_prompt(messages.proceed_commit()) .default(true) .interact()?; if !confirm { println!("{}", messages.commit_cancelled().yellow()); return Ok(()); } } let result = if self.amend { if self.dry_run { println!("\n{} {}", messages.dry_run(), "- commit not amended.".yellow()); return Ok(()); } self.amend_commit(&repo, &commit_message)?; None } else { if self.dry_run { println!("\n{} {}", messages.dry_run(), "- commit not created.".yellow()); return Ok(()); } CommitBuilder::new() .message(&commit_message) .sign(self.sign) .execute(&repo)? }; if let Some(commit_oid) = result { println!("{} {}", messages.commit_created().green().bold(), commit_oid.to_string()[..8].to_string().cyan()); } else { println!("{} {}", messages.commit_amended().green().bold(), "successfully"); } // Push after commit if requested or ask user if self.push { println!("\n{}", messages.pushing_commit(&self.remote)); repo.push(&self.remote, "HEAD")?; println!("{}", messages.pushed_commit(&self.remote)); } else if !self.yes && !self.dry_run { let should_push = Confirm::new() .with_prompt(messages.push_after_commit()) .default(false) .interact()?; if should_push { println!("\n{}", messages.pushing_commit(&self.remote)); repo.push(&self.remote, "HEAD")?; println!("{}", messages.pushed_commit(&self.remote)); } } Ok(()) } fn create_date_commit(&self) -> String { let prefix = self.commit_type.as_deref(); create_date_commit_message(prefix) } fn create_manual_commit(&self, format: CommitFormat) -> Result { let description = self.message.clone() .ok_or_else(|| anyhow::anyhow!("Description required for manual commit. Use -m "))?; // Try to extract commit type from message if not provided let commit_type = if let Some(ref ct) = self.commit_type { ct.clone() } else { // Parse from conventional commit format: "type: description" description .split(':') .next() .unwrap_or("feat") .trim() .to_string() }; let builder = CommitBuilder::new() .commit_type(commit_type) .description(description) .scope_opt(self.scope.clone()) .body_opt(self.body.clone()) .breaking(self.breaking) .format(format); builder.build_message() } async fn generate_commit(&self, repo: &GitRepo, format: CommitFormat, messages: &Messages) -> Result { let manager = ConfigManager::new()?; let config = manager.config(); // Check if LLM is configured let generator = ContentGenerator::new(&config.llm).await .context("Failed to initialize LLM. Use --manual for manual commit.")?; println!("{}", messages.ai_analyzing()); let language_str = &config.language.output_language; let language = Language::from_str(language_str).unwrap_or(Language::English); let generated = if self.yes { // Non-interactive mode: generate directly generator.generate_commit_from_repo(repo, format, language).await? } else { // Interactive mode: allow user to review and regenerate generator.generate_commit_interactive(repo, format, language).await? }; Ok(generated.to_conventional()) } async fn create_interactive_commit(&self, format: CommitFormat, messages: &Messages) -> Result { let types = get_commit_types(format == CommitFormat::Commitlint); // Select type let type_idx = Select::new() .with_prompt(messages.select_commit_type()) .items(types) .interact()?; let commit_type = types[type_idx].to_string(); // Enter scope (optional) let scope: String = Input::new() .with_prompt(messages.scope_optional()) .allow_empty(true) .interact_text()?; let scope = if scope.is_empty() { None } else { Some(scope) }; // Enter description let description: String = Input::new() .with_prompt(messages.description()) .interact_text()?; // Breaking change let breaking = Confirm::new() .with_prompt(messages.breaking_change()) .default(false) .interact()?; // Add body let add_body = Confirm::new() .with_prompt(messages.add_body()) .default(false) .interact()?; let body = if add_body { let body_text = crate::utils::editor::edit_content(messages.enter_commit_body())?; if body_text.trim().is_empty() { None } else { Some(body_text) } } else { None }; // Build commit let builder = CommitBuilder::new() .commit_type(commit_type) .description(description) .scope_opt(scope) .body_opt(body) .breaking(breaking) .format(format); builder.build_message() } fn amend_commit(&self, repo: &GitRepo, message: &str) -> Result<()> { use std::process::Command; let mut args = vec!["commit", "--amend", "-m", message]; if self.no_verify { args.push("--no-verify"); } if self.sign { args.push("-S"); } let output = Command::new("git") .args(&args) .current_dir(repo.path()) .output()?; if !output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); let error_msg = if stderr.is_empty() { if stdout.is_empty() { "GPG signing failed. Please check:\n\ 1. GPG signing key is configured (git config --get user.signingkey)\n\ 2. GPG agent is running\n\ 3. You can sign commits manually (try: git commit --amend -S)".to_string() } else { stdout.to_string() } } else { stderr.to_string() }; bail!("Failed to amend commit: {}", error_msg); } Ok(()) } } // Helper trait for optional builder methods trait CommitBuilderExt { fn scope_opt(self, scope: Option) -> Self; fn body_opt(self, body: Option) -> Self; } impl CommitBuilderExt for CommitBuilder { fn scope_opt(self, scope: Option) -> Self { if let Some(s) = scope { self.scope(s) } else { self } } fn body_opt(self, body: Option) -> Self { if let Some(b) = body { self.body(b) } else { self } } }