From 04c702989b30a7bf530eeb7f0e68bef9ceaf9128 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Sat, 31 Jan 2026 11:38:15 +0000 Subject: [PATCH] Add source files: main.rs, lib.rs, cli.rs, error.rs --- src/cli.rs | 237 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 src/cli.rs diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..42355da --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,237 @@ +use clap::{Parser, Args, ValueEnum}; +use std::path::PathBuf; +use crate::analyzer::Analyzer; +use crate::config::load_config; +use crate::convention::CommitType; +use crate::error::{Error, Result}; +use crate::interactive::InteractivePrompter; +use crate::output::{get_formatter, OutputFormat}; + +#[derive(Debug, Clone, ValueEnum)] +pub enum FormatValue { + short, + verbose, + compact, + json, +} + +impl From for OutputFormat { + fn from(val: FormatValue) -> Self { + match val { + FormatValue::short => OutputFormat::Short, + FormatValue::verbose => OutputFormat::Verbose, + FormatValue::compact => OutputFormat::Compact, + FormatValue::json => OutputFormat::Json, + } + } +} + +#[derive(Debug, Clone, Args)] +pub struct TypeArgs { + #[arg(short, long)] + pub type_flag: Option, +} + +#[derive(Debug, Clone, Args)] +pub struct ScopeArgs { + #[arg(short, long)] + pub scope: Option, +} + +#[derive(Parser, Debug)] +#[command(name = "git-commit-prefix-gen")] +#[command(author, version, about, long_about = None)] +pub struct CliArgs { + #[arg(short, long, value_enum)] + pub format: Option, + + #[arg(short, long)] + pub config: Option, + + #[arg(short, long)] + pub staged: bool, + + #[arg(short, long)] + pub interactive: bool, + + #[arg(short, long)] + pub dry_run: bool, + + #[arg(short, long)] + pub type_flag: Option, + + #[arg(short, long)] + pub scope: Option, + + #[arg(short, long)] + pub verbose: bool, + + #[arg(short, long)] + pub quiet: bool, +} + +pub fn run(args: &CliArgs) -> Result { + if !args.quiet { + if let Ok(repo) = git2::Repository::discover(".") { + if let Ok(head) = repo.head() { + if let Some(branch) = head.shorthand() { + eprintln!("On branch: {}", branch); + } + } + } + } + + let (convention, config_path) = load_config(args.config.as_deref())?; + if let Some(path) = config_path { + if !args.quiet { + eprintln!("Using config: {}", path.display()); + } + } + + let analyzer = Analyzer::with_convention(convention); + let repo_path = std::env::current_dir().map_err(Error::from)?; + + let changes = if args.staged { + analyzer.analyze_staged(&repo_path)? + } else { + analyzer.analyze(&repo_path)? + }; + + if changes.is_empty() { + if !args.quiet { + eprintln!("No changes to commit."); + } + return Ok(1); + } + + let mut suggestions = analyzer.generate_suggestions(&changes); + let suggestion = suggestions.remove(0); + + let commit_type = if let Some(ref type_flag) = args.type_flag { + type_flag.parse().unwrap_or(suggestion.commit_type.clone()) + } else { + suggestion.commit_type.clone() + }; + + let scope = if let Some(ref scope_arg) = args.scope { + Some(crate::convention::Scope::custom(scope_arg.clone())) + } else { + suggestion.scope.clone() + }; + + let final_suggestion = crate::convention::CommitSuggestion { + commit_type, + scope, + description: suggestion.description, + confidence: suggestion.confidence, + file_count: suggestion.file_count, + }; + + let format = args.format.clone().map(|f| f.into()).unwrap_or(OutputFormat::Short); + let formatter = get_formatter(&format); + let output = formatter.format(&final_suggestion, &changes); + + if args.interactive { + let prompter = InteractivePrompter::new(); + let commit_type = prompter.select_commit_type(&final_suggestion.commit_type, &get_commit_types())?; + + let scope = if let Some(ref s) = final_suggestion.scope { + prompter.confirm_scope(&s.name, &get_common_scopes())? + } else { + None + }; + + let description = prompter.confirm_description(&final_suggestion.description)?; + + let interactive_suggestion = crate::convention::CommitSuggestion { + commit_type, + scope, + description, + confidence: final_suggestion.confidence, + file_count: final_suggestion.file_count, + }; + + let interactive_output = formatter.format(&interactive_suggestion, &changes); + + if prompter.confirm_commit(&interactive_output)? { + println!("{}", interactive_output); + } + } else { + println!("{}", output); + } + + if args.dry_run { + if !args.quiet { + eprintln!("[dry-run] No changes made."); + } + } + + Ok(0) +} + +fn get_commit_types() -> Vec { + vec![ + CommitType::Feat, + CommitType::Fix, + CommitType::Docs, + CommitType::Style, + CommitType::Refactor, + CommitType::Test, + CommitType::Chore, + CommitType::Build, + CommitType::Ci, + CommitType::Perf, + ] +} + +fn get_common_scopes() -> Vec { + vec![ + "auth".to_string(), + "api".to_string(), + "ui".to_string(), + "db".to_string(), + "config".to_string(), + "utils".to_string(), + "core".to_string(), + "web".to_string(), + "mobile".to_string(), + ] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cli_args_parse() { + let args = CliArgs::parse_from(&["git-commit-prefix-gen", "--format", "short"]); + assert_eq!(args.format, Some(FormatValue::short)); + } + + #[test] + fn test_cli_args_staged() { + let args = CliArgs::parse_from(&["git-commit-prefix-gen", "--staged", "--type", "feat"]); + assert!(args.staged); + assert_eq!(args.type_flag, Some("feat".to_string())); + } + + #[test] + fn test_cli_args_all_options() { + let args = CliArgs::parse_from(&[ + "git-commit-prefix-gen", + "--format", "json", + "--config", "custom.json", + "--staged", + "--interactive", + "--dry-run", + "--type", "fix", + "--scope", "auth", + ]); + assert_eq!(args.format, Some(FormatValue::json)); + assert!(args.staged); + assert!(args.interactive); + assert!(args.dry_run); + assert_eq!(args.type_flag, Some("fix".to_string())); + assert_eq!(args.scope, Some("auth".to_string())); + } +}