Add source files: main.rs, lib.rs, cli.rs, error.rs
Some checks failed
CI / test (push) Has been cancelled
Some checks failed
CI / test (push) Has been cancelled
This commit is contained in:
237
src/cli.rs
Normal file
237
src/cli.rs
Normal file
@@ -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<FormatValue> 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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Args)]
|
||||||
|
pub struct ScopeArgs {
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub scope: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<FormatValue>,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub config: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[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<String>,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub scope: Option<String>,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub verbose: bool,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
pub quiet: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(args: &CliArgs) -> Result<i32> {
|
||||||
|
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<CommitType> {
|
||||||
|
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<String> {
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user