feat(commands):为所有命令添加config_path参数支持,实现自定义配置文件路径

♻️ refactor(config):重构ConfigManager,添加with_path_fresh方法用于初始化新配置
🔧 fix(git):改进跨平台路径处理,增强git仓库检测的鲁棒性
 test(tests):添加全面的集成测试,覆盖所有命令和跨平台场景
This commit is contained in:
2026-02-14 14:28:11 +08:00
parent 3c925d8268
commit e822ba1f54
14 changed files with 1152 additions and 272 deletions

View File

@@ -2,6 +2,7 @@ use anyhow::{bail, Result};
use clap::{Parser, Subcommand};
use colored::Colorize;
use dialoguer::{Confirm, Input, Select};
use std::path::PathBuf;
use crate::config::{Language, manager::ConfigManager};
use crate::config::CommitFormat;
@@ -191,43 +192,60 @@ enum ConfigSubcommand {
/// Test LLM connection
TestLlm,
/// Show config file path
Path,
}
impl ConfigCommand {
pub async fn execute(&self) -> Result<()> {
pub async fn execute(&self, config_path: Option<PathBuf>) -> Result<()> {
match &self.command {
Some(ConfigSubcommand::Show) => self.show_config().await,
Some(ConfigSubcommand::List) => self.list_config().await,
Some(ConfigSubcommand::Edit) => self.edit_config().await,
Some(ConfigSubcommand::Set { key, value }) => self.set_value(key, value).await,
Some(ConfigSubcommand::Get { key }) => self.get_value(key).await,
Some(ConfigSubcommand::SetLlm { provider }) => self.set_llm(provider.as_deref()).await,
Some(ConfigSubcommand::SetOpenAiKey { key }) => self.set_openai_key(key).await,
Some(ConfigSubcommand::SetAnthropicKey { key }) => self.set_anthropic_key(key).await,
Some(ConfigSubcommand::SetKimiKey { key }) => self.set_kimi_key(key).await,
Some(ConfigSubcommand::SetDeepSeekKey { key }) => self.set_deepseek_key(key).await,
Some(ConfigSubcommand::SetOpenRouterKey { key }) => self.set_openrouter_key(key).await,
Some(ConfigSubcommand::SetOllama { url, model }) => self.set_ollama(url.as_deref(), model.as_deref()).await,
Some(ConfigSubcommand::SetKimi { base_url, model }) => self.set_kimi(base_url.as_deref(), model.as_deref()).await,
Some(ConfigSubcommand::SetDeepSeek { base_url, model }) => self.set_deepseek(base_url.as_deref(), model.as_deref()).await,
Some(ConfigSubcommand::SetOpenRouter { base_url, model }) => self.set_openrouter(base_url.as_deref(), model.as_deref()).await,
Some(ConfigSubcommand::SetCommitFormat { format }) => self.set_commit_format(format).await,
Some(ConfigSubcommand::SetVersionPrefix { prefix }) => self.set_version_prefix(prefix).await,
Some(ConfigSubcommand::SetChangelogPath { path }) => self.set_changelog_path(path).await,
Some(ConfigSubcommand::SetLanguage { language }) => self.set_language(language.as_deref()).await,
Some(ConfigSubcommand::SetKeepTypesEnglish { keep }) => self.set_keep_types_english(*keep).await,
Some(ConfigSubcommand::SetKeepChangelogTypesEnglish { keep }) => self.set_keep_changelog_types_english(*keep).await,
Some(ConfigSubcommand::Reset { force }) => self.reset(*force).await,
Some(ConfigSubcommand::Export { output }) => self.export_config(output.as_deref()).await,
Some(ConfigSubcommand::Import { file }) => self.import_config(file).await,
Some(ConfigSubcommand::ListModels) => self.list_models().await,
Some(ConfigSubcommand::TestLlm) => self.test_llm().await,
None => self.show_config().await,
Some(ConfigSubcommand::Show) => self.show_config(&config_path).await,
Some(ConfigSubcommand::List) => self.list_config(&config_path).await,
Some(ConfigSubcommand::Edit) => self.edit_config(&config_path).await,
Some(ConfigSubcommand::Set { key, value }) => self.set_value(key, value, &config_path).await,
Some(ConfigSubcommand::Get { key }) => self.get_value(key, &config_path).await,
Some(ConfigSubcommand::SetLlm { provider }) => self.set_llm(provider.as_deref(), &config_path).await,
Some(ConfigSubcommand::SetOpenAiKey { key }) => self.set_openai_key(key, &config_path).await,
Some(ConfigSubcommand::SetAnthropicKey { key }) => self.set_anthropic_key(key, &config_path).await,
Some(ConfigSubcommand::SetKimiKey { key }) => self.set_kimi_key(key, &config_path).await,
Some(ConfigSubcommand::SetDeepSeekKey { key }) => self.set_deepseek_key(key, &config_path).await,
Some(ConfigSubcommand::SetOpenRouterKey { key }) => self.set_openrouter_key(key, &config_path).await,
Some(ConfigSubcommand::SetOllama { url, model }) => self.set_ollama(url.as_deref(), model.as_deref(), &config_path).await,
Some(ConfigSubcommand::SetKimi { base_url, model }) => self.set_kimi(base_url.as_deref(), model.as_deref(), &config_path).await,
Some(ConfigSubcommand::SetDeepSeek { base_url, model }) => self.set_deepseek(base_url.as_deref(), model.as_deref(), &config_path).await,
Some(ConfigSubcommand::SetOpenRouter { base_url, model }) => self.set_openrouter(base_url.as_deref(), model.as_deref(), &config_path).await,
Some(ConfigSubcommand::SetCommitFormat { format }) => self.set_commit_format(format, &config_path).await,
Some(ConfigSubcommand::SetVersionPrefix { prefix }) => self.set_version_prefix(prefix, &config_path).await,
Some(ConfigSubcommand::SetChangelogPath { path }) => self.set_changelog_path(path, &config_path).await,
Some(ConfigSubcommand::SetLanguage { language }) => self.set_language(language.as_deref(), &config_path).await,
Some(ConfigSubcommand::SetKeepTypesEnglish { keep }) => self.set_keep_types_english(*keep, &config_path).await,
Some(ConfigSubcommand::SetKeepChangelogTypesEnglish { keep }) => self.set_keep_changelog_types_english(*keep, &config_path).await,
Some(ConfigSubcommand::Reset { force }) => self.reset(*force, &config_path).await,
Some(ConfigSubcommand::Export { output }) => self.export_config(output.as_deref(), &config_path).await,
Some(ConfigSubcommand::Import { file }) => self.import_config(file, &config_path).await,
Some(ConfigSubcommand::ListModels) => self.list_models(&config_path).await,
Some(ConfigSubcommand::TestLlm) => self.test_llm(&config_path).await,
Some(ConfigSubcommand::Path) => self.show_path(&config_path).await,
None => self.show_config(&config_path).await,
}
}
async fn show_config(&self) -> Result<()> {
let manager = ConfigManager::new()?;
fn get_manager(&self, config_path: &Option<PathBuf>) -> Result<ConfigManager> {
match config_path {
Some(path) => ConfigManager::with_path(path),
None => ConfigManager::new(),
}
}
async fn show_path(&self, config_path: &Option<PathBuf>) -> Result<()> {
let manager = self.get_manager(config_path)?;
println!("{}", manager.path().display());
Ok(())
}
async fn show_config(&self, config_path: &Option<PathBuf>) -> Result<()> {
let manager = self.get_manager(config_path)?;
let config = manager.config();
println!("{}", "\nQuiCommit Configuration".bold());
@@ -306,8 +324,8 @@ impl ConfigCommand {
}
/// List all configuration information with masked API keys
async fn list_config(&self) -> Result<()> {
let manager = ConfigManager::new()?;
async fn list_config(&self, config_path: &Option<PathBuf>) -> Result<()> {
let manager = self.get_manager(config_path)?;
let config = manager.config();
println!("{}", "\nQuiCommit Configuration".bold());
@@ -404,15 +422,15 @@ impl ConfigCommand {
Ok(())
}
async fn edit_config(&self) -> Result<()> {
let manager = ConfigManager::new()?;
async fn edit_config(&self, config_path: &Option<PathBuf>) -> Result<()> {
let manager = self.get_manager(config_path)?;
crate::utils::editor::edit_file(manager.path())?;
println!("{} Configuration updated", "".green());
Ok(())
}
async fn set_value(&self, key: &str, value: &str) -> Result<()> {
let mut manager = ConfigManager::new()?;
async fn set_value(&self, key: &str, value: &str, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
match key {
"llm.provider" => manager.set_llm_provider(value.to_string()),
@@ -450,8 +468,8 @@ impl ConfigCommand {
Ok(())
}
async fn get_value(&self, key: &str) -> Result<()> {
let manager = ConfigManager::new()?;
async fn get_value(&self, key: &str, config_path: &Option<PathBuf>) -> Result<()> {
let manager = self.get_manager(config_path)?;
let config = manager.config();
let value = match key {
@@ -469,8 +487,8 @@ impl ConfigCommand {
Ok(())
}
async fn set_llm(&self, provider: Option<&str>) -> Result<()> {
let mut manager = ConfigManager::new()?;
async fn set_llm(&self, provider: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
let provider = if let Some(p) = provider {
p.to_string()
@@ -602,48 +620,48 @@ impl ConfigCommand {
Ok(())
}
async fn set_openai_key(&self, key: &str) -> Result<()> {
let mut manager = ConfigManager::new()?;
async fn set_openai_key(&self, key: &str, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
manager.set_openai_api_key(key.to_string());
manager.save()?;
println!("{} OpenAI API key set", "".green());
Ok(())
}
async fn set_anthropic_key(&self, key: &str) -> Result<()> {
let mut manager = ConfigManager::new()?;
async fn set_anthropic_key(&self, key: &str, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
manager.set_anthropic_api_key(key.to_string());
manager.save()?;
println!("{} Anthropic API key set", "".green());
Ok(())
}
async fn set_kimi_key(&self, key: &str) -> Result<()> {
let mut manager = ConfigManager::new()?;
async fn set_kimi_key(&self, key: &str, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
manager.set_kimi_api_key(key.to_string());
manager.save()?;
println!("{} Kimi API key set", "".green());
Ok(())
}
async fn set_deepseek_key(&self, key: &str) -> Result<()> {
let mut manager = ConfigManager::new()?;
async fn set_deepseek_key(&self, key: &str, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
manager.set_deepseek_api_key(key.to_string());
manager.save()?;
println!("{} DeepSeek API key set", "".green());
Ok(())
}
async fn set_openrouter_key(&self, key: &str) -> Result<()> {
let mut manager = ConfigManager::new()?;
async fn set_openrouter_key(&self, key: &str, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
manager.set_openrouter_api_key(key.to_string());
manager.save()?;
println!("{} OpenRouter API key set", "".green());
Ok(())
}
async fn set_kimi(&self, base_url: Option<&str>, model: Option<&str>) -> Result<()> {
let mut manager = ConfigManager::new()?;
async fn set_kimi(&self, base_url: Option<&str>, model: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
if let Some(url) = base_url {
manager.set_kimi_base_url(url.to_string());
@@ -657,8 +675,8 @@ impl ConfigCommand {
Ok(())
}
async fn set_deepseek(&self, base_url: Option<&str>, model: Option<&str>) -> Result<()> {
let mut manager = ConfigManager::new()?;
async fn set_deepseek(&self, base_url: Option<&str>, model: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
if let Some(url) = base_url {
manager.set_deepseek_base_url(url.to_string());
@@ -672,8 +690,8 @@ impl ConfigCommand {
Ok(())
}
async fn set_openrouter(&self, base_url: Option<&str>, model: Option<&str>) -> Result<()> {
let mut manager = ConfigManager::new()?;
async fn set_openrouter(&self, base_url: Option<&str>, model: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
if let Some(url) = base_url {
manager.set_openrouter_base_url(url.to_string());
@@ -687,8 +705,8 @@ impl ConfigCommand {
Ok(())
}
async fn set_ollama(&self, url: Option<&str>, model: Option<&str>) -> Result<()> {
let mut manager = ConfigManager::new()?;
async fn set_ollama(&self, url: Option<&str>, model: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
if let Some(u) = url {
manager.config_mut().llm.ollama.url = u.to_string();
@@ -702,8 +720,8 @@ impl ConfigCommand {
Ok(())
}
async fn set_commit_format(&self, format: &str) -> Result<()> {
let mut manager = ConfigManager::new()?;
async fn set_commit_format(&self, format: &str, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
let format = match format {
"conventional" => CommitFormat::Conventional,
@@ -717,24 +735,24 @@ impl ConfigCommand {
Ok(())
}
async fn set_version_prefix(&self, prefix: &str) -> Result<()> {
let mut manager = ConfigManager::new()?;
async fn set_version_prefix(&self, prefix: &str, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
manager.set_version_prefix(prefix.to_string());
manager.save()?;
println!("{} Set version prefix to '{}'", "".green(), prefix);
Ok(())
}
async fn set_changelog_path(&self, path: &str) -> Result<()> {
let mut manager = ConfigManager::new()?;
async fn set_changelog_path(&self, path: &str, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
manager.set_changelog_path(path.to_string());
manager.save()?;
println!("{} Set changelog path to {}", "".green(), path);
Ok(())
}
async fn set_language(&self, language: Option<&str>) -> Result<()> {
let mut manager = ConfigManager::new()?;
async fn set_language(&self, language: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
let language_code = if let Some(lang) = language {
lang.to_string()
@@ -763,8 +781,8 @@ impl ConfigCommand {
Ok(())
}
async fn set_keep_types_english(&self, keep: bool) -> Result<()> {
let mut manager = ConfigManager::new()?;
async fn set_keep_types_english(&self, keep: bool, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
manager.set_keep_types_english(keep);
manager.save()?;
let status = if keep { "enabled" } else { "disabled" };
@@ -772,8 +790,8 @@ impl ConfigCommand {
Ok(())
}
async fn set_keep_changelog_types_english(&self, keep: bool) -> Result<()> {
let mut manager = ConfigManager::new()?;
async fn set_keep_changelog_types_english(&self, keep: bool, config_path: &Option<PathBuf>) -> Result<()> {
let mut manager = self.get_manager(config_path)?;
manager.set_keep_changelog_types_english(keep);
manager.save()?;
let status = if keep { "enabled" } else { "disabled" };
@@ -781,7 +799,7 @@ impl ConfigCommand {
Ok(())
}
async fn reset(&self, force: bool) -> Result<()> {
async fn reset(&self, force: bool, config_path: &Option<PathBuf>) -> Result<()> {
if !force {
let confirm = Confirm::new()
.with_prompt("Are you sure you want to reset all configuration?")
@@ -794,7 +812,7 @@ impl ConfigCommand {
}
}
let mut manager = ConfigManager::new()?;
let mut manager = self.get_manager(config_path)?;
manager.reset();
manager.save()?;
@@ -802,8 +820,8 @@ impl ConfigCommand {
Ok(())
}
async fn export_config(&self, output: Option<&str>) -> Result<()> {
let manager = ConfigManager::new()?;
async fn export_config(&self, output: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
let manager = self.get_manager(config_path)?;
let toml = manager.export()?;
if let Some(path) = output {
@@ -816,10 +834,10 @@ impl ConfigCommand {
Ok(())
}
async fn import_config(&self, file: &str) -> Result<()> {
async fn import_config(&self, file: &str, config_path: &Option<PathBuf>) -> Result<()> {
let toml = std::fs::read_to_string(file)?;
let mut manager = ConfigManager::new()?;
let mut manager = self.get_manager(config_path)?;
manager.import(&toml)?;
manager.save()?;
@@ -827,8 +845,8 @@ impl ConfigCommand {
Ok(())
}
async fn list_models(&self) -> Result<()> {
let manager = ConfigManager::new()?;
async fn list_models(&self, config_path: &Option<PathBuf>) -> Result<()> {
let manager = self.get_manager(config_path)?;
let config = manager.config();
match config.llm.provider.as_str() {
@@ -984,8 +1002,8 @@ impl ConfigCommand {
Ok(())
}
async fn test_llm(&self) -> Result<()> {
let manager = ConfigManager::new()?;
async fn test_llm(&self, config_path: &Option<PathBuf>) -> Result<()> {
let manager = self.get_manager(config_path)?;
let config = manager.config();
println!("Testing LLM connection ({})...", config.llm.provider.cyan());