♻️ refactor(config):重构ConfigManager,添加with_path_fresh方法用于初始化新配置 🔧 fix(git):改进跨平台路径处理,增强git仓库检测的鲁棒性 ✅ test(tests):添加全面的集成测试,覆盖所有命令和跨平台场景
333 lines
11 KiB
Rust
333 lines
11 KiB
Rust
use anyhow::Result;
|
|
use clap::Parser;
|
|
use colored::Colorize;
|
|
use dialoguer::{Confirm, Input, Select};
|
|
use std::path::PathBuf;
|
|
|
|
use crate::config::{GitProfile, Language};
|
|
use crate::config::manager::ConfigManager;
|
|
use crate::config::profile::{GpgConfig, SshConfig};
|
|
use crate::i18n::Messages;
|
|
use crate::utils::validators::validate_email;
|
|
|
|
/// Initialize quicommit configuration
|
|
#[derive(Parser)]
|
|
pub struct InitCommand {
|
|
/// Skip interactive setup
|
|
#[arg(short, long)]
|
|
yes: bool,
|
|
|
|
/// Reset existing configuration
|
|
#[arg(long)]
|
|
reset: bool,
|
|
}
|
|
|
|
impl InitCommand {
|
|
pub async fn execute(&self, config_path: Option<PathBuf>) -> Result<()> {
|
|
let messages = Messages::new(Language::English);
|
|
println!("{}", messages.initializing().bold().cyan());
|
|
|
|
let config_path = config_path.unwrap_or_else(|| {
|
|
crate::config::AppConfig::default_path().unwrap()
|
|
});
|
|
|
|
// Check if config already exists
|
|
if config_path.exists() && !self.reset {
|
|
if !self.yes {
|
|
let overwrite = Confirm::new()
|
|
.with_prompt("Configuration already exists. Overwrite?")
|
|
.default(false)
|
|
.interact()?;
|
|
|
|
if !overwrite {
|
|
println!("{}", "Initialization cancelled.".yellow());
|
|
return Ok(());
|
|
}
|
|
} else {
|
|
println!("{}", "Configuration already exists. Use --reset to overwrite.".yellow());
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
// Create parent directory if needed
|
|
if let Some(parent) = config_path.parent() {
|
|
std::fs::create_dir_all(parent)
|
|
.map_err(|e| anyhow::anyhow!("Failed to create config directory: {}", e))?;
|
|
}
|
|
|
|
// Create new config manager with fresh config
|
|
let mut manager = ConfigManager::with_path_fresh(&config_path)?;
|
|
|
|
if self.yes {
|
|
self.quick_setup(&mut manager).await?;
|
|
} else {
|
|
self.interactive_setup(&mut manager).await?;
|
|
}
|
|
|
|
manager.save()?;
|
|
|
|
// Get configured language for final messages
|
|
let language = manager.get_language().unwrap_or(Language::English);
|
|
let messages = Messages::new(language);
|
|
|
|
println!("{}", messages.init_success().bold().green());
|
|
println!("\n{}: {}", messages.config_file(), config_path.display());
|
|
println!("\n{}:", messages.next_steps());
|
|
println!(" 1. Create a profile: {}", "quicommit profile add".cyan());
|
|
println!(" 2. Configure LLM: {}", "quicommit config set-llm".cyan());
|
|
println!(" 3. Start committing: {}", "quicommit commit".cyan());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn quick_setup(&self, manager: &mut ConfigManager) -> Result<()> {
|
|
// Try to get git user info
|
|
let git_config = git2::Config::open_default()?;
|
|
|
|
let user_name = git_config.get_string("user.name").unwrap_or_else(|_| "User".to_string());
|
|
let user_email = git_config.get_string("user.email").unwrap_or_else(|_| "user@example.com".to_string());
|
|
|
|
let profile = GitProfile::new(
|
|
"default".to_string(),
|
|
user_name,
|
|
user_email,
|
|
);
|
|
|
|
manager.add_profile("default".to_string(), profile)?;
|
|
manager.set_default_profile(Some("default".to_string()))?;
|
|
|
|
// Set default LLM to Ollama
|
|
manager.set_llm_provider("ollama".to_string());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn interactive_setup(&self, manager: &mut ConfigManager) -> Result<()> {
|
|
let messages = Messages::new(Language::English);
|
|
println!("\n{}", messages.setup_profile().bold());
|
|
|
|
// Language selection
|
|
println!("\n{}", messages.select_output_language().bold());
|
|
let languages = vec![
|
|
Language::English,
|
|
Language::Chinese,
|
|
Language::Japanese,
|
|
Language::Korean,
|
|
Language::Spanish,
|
|
Language::French,
|
|
Language::German,
|
|
];
|
|
let language_names: Vec<String> = languages.iter().map(|l| l.display_name().to_string()).collect();
|
|
let language_idx = Select::new()
|
|
.items(&language_names)
|
|
.default(0)
|
|
.interact()?;
|
|
|
|
let selected_language = languages[language_idx];
|
|
manager.set_output_language(selected_language.to_code().to_string());
|
|
|
|
// Update messages to selected language
|
|
let messages = Messages::new(selected_language);
|
|
|
|
// Profile name
|
|
let profile_name: String = Input::new()
|
|
.with_prompt(messages.profile_name())
|
|
.default("personal".to_string())
|
|
.interact_text()?;
|
|
|
|
// User info
|
|
let git_config = git2::Config::open_default().ok();
|
|
|
|
let default_name = git_config.as_ref()
|
|
.and_then(|c| c.get_string("user.name").ok())
|
|
.unwrap_or_default();
|
|
|
|
let default_email = git_config.as_ref()
|
|
.and_then(|c| c.get_string("user.email").ok())
|
|
.unwrap_or_default();
|
|
|
|
let user_name: String = Input::new()
|
|
.with_prompt(messages.git_user_name())
|
|
.default(default_name)
|
|
.interact_text()?;
|
|
|
|
let user_email: String = Input::new()
|
|
.with_prompt(messages.git_user_email())
|
|
.default(default_email)
|
|
.validate_with(|input: &String| {
|
|
validate_email(input).map_err(|e| e.to_string())
|
|
})
|
|
.interact_text()?;
|
|
|
|
let description: String = Input::new()
|
|
.with_prompt(messages.profile_description())
|
|
.allow_empty(true)
|
|
.interact_text()?;
|
|
|
|
let is_work = Confirm::new()
|
|
.with_prompt(messages.is_work_profile())
|
|
.default(false)
|
|
.interact()?;
|
|
|
|
let organization = if is_work {
|
|
Some(Input::new()
|
|
.with_prompt(messages.organization_name())
|
|
.interact_text()?)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// SSH configuration
|
|
let setup_ssh = Confirm::new()
|
|
.with_prompt(messages.configure_ssh())
|
|
.default(false)
|
|
.interact()?;
|
|
|
|
let ssh_config = if setup_ssh {
|
|
Some(self.setup_ssh_interactive(&messages).await?)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// GPG configuration
|
|
let setup_gpg = Confirm::new()
|
|
.with_prompt(messages.configure_gpg())
|
|
.default(false)
|
|
.interact()?;
|
|
|
|
let gpg_config = if setup_gpg {
|
|
Some(self.setup_gpg_interactive(&messages).await?)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// Create profile
|
|
let mut profile = GitProfile::new(
|
|
profile_name.clone(),
|
|
user_name,
|
|
user_email,
|
|
);
|
|
|
|
if !description.is_empty() {
|
|
profile.description = Some(description);
|
|
}
|
|
|
|
profile.is_work = is_work;
|
|
profile.organization = organization;
|
|
profile.ssh = ssh_config;
|
|
profile.gpg = gpg_config;
|
|
|
|
manager.add_profile(profile_name.clone(), profile)?;
|
|
manager.set_default_profile(Some(profile_name))?;
|
|
|
|
// LLM provider selection
|
|
println!("\n{}", messages.select_llm_provider().bold());
|
|
let providers = vec![
|
|
"Ollama (local)",
|
|
"OpenAI",
|
|
"Anthropic Claude",
|
|
"Kimi (Moonshot AI)",
|
|
"DeepSeek",
|
|
"OpenRouter"
|
|
];
|
|
let provider_idx = Select::new()
|
|
.items(&providers)
|
|
.default(0)
|
|
.interact()?;
|
|
|
|
let provider = match provider_idx {
|
|
0 => "ollama",
|
|
1 => "openai",
|
|
2 => "anthropic",
|
|
3 => "kimi",
|
|
4 => "deepseek",
|
|
5 => "openrouter",
|
|
_ => "ollama",
|
|
};
|
|
|
|
manager.set_llm_provider(provider.to_string());
|
|
|
|
// Configure API key if needed
|
|
if provider == "openai" {
|
|
let api_key: String = Input::new()
|
|
.with_prompt(messages.openai_api_key())
|
|
.interact_text()?;
|
|
manager.set_openai_api_key(api_key);
|
|
} else if provider == "anthropic" {
|
|
let api_key: String = Input::new()
|
|
.with_prompt(messages.anthropic_api_key())
|
|
.interact_text()?;
|
|
manager.set_anthropic_api_key(api_key);
|
|
} else if provider == "kimi" {
|
|
let api_key: String = Input::new()
|
|
.with_prompt(messages.kimi_api_key())
|
|
.interact_text()?;
|
|
manager.set_kimi_api_key(api_key);
|
|
} else if provider == "deepseek" {
|
|
let api_key: String = Input::new()
|
|
.with_prompt(messages.deepseek_api_key())
|
|
.interact_text()?;
|
|
manager.set_deepseek_api_key(api_key);
|
|
} else if provider == "openrouter" {
|
|
let api_key: String = Input::new()
|
|
.with_prompt(messages.openrouter_api_key())
|
|
.interact_text()?;
|
|
manager.set_openrouter_api_key(api_key);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn setup_ssh_interactive(&self, messages: &Messages) -> Result<SshConfig> {
|
|
use std::path::PathBuf;
|
|
|
|
let ssh_dir = dirs::home_dir()
|
|
.map(|h| h.join(".ssh"))
|
|
.unwrap_or_else(|| PathBuf::from("~/.ssh"));
|
|
|
|
let key_path: String = Input::new()
|
|
.with_prompt(messages.ssh_private_key_path())
|
|
.default(ssh_dir.join("id_rsa").display().to_string())
|
|
.interact_text()?;
|
|
|
|
let has_passphrase = Confirm::new()
|
|
.with_prompt(messages.has_passphrase())
|
|
.default(false)
|
|
.interact()?;
|
|
|
|
let passphrase = if has_passphrase {
|
|
Some(crate::utils::password_input(messages.ssh_key_passphrase())?)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Ok(SshConfig {
|
|
private_key_path: Some(PathBuf::from(key_path)),
|
|
public_key_path: None,
|
|
passphrase,
|
|
agent_forwarding: false,
|
|
ssh_command: None,
|
|
known_hosts_file: None,
|
|
})
|
|
}
|
|
|
|
async fn setup_gpg_interactive(&self, messages: &Messages) -> Result<GpgConfig> {
|
|
let key_id: String = Input::new()
|
|
.with_prompt(messages.gpg_key_id())
|
|
.interact_text()?;
|
|
|
|
let use_agent = Confirm::new()
|
|
.with_prompt(messages.use_gpg_agent())
|
|
.default(true)
|
|
.interact()?;
|
|
|
|
Ok(GpgConfig {
|
|
key_id,
|
|
program: "gpg".to_string(),
|
|
home_dir: None,
|
|
passphrase: None,
|
|
use_agent,
|
|
})
|
|
}
|
|
}
|