♻️ refactor(config):重构ConfigManager,添加with_path_fresh方法用于初始化新配置 🔧 fix(git):改进跨平台路径处理,增强git仓库检测的鲁棒性 ✅ test(tests):添加全面的集成测试,覆盖所有命令和跨平台场景
803 lines
26 KiB
Rust
803 lines
26 KiB
Rust
use anyhow::{bail, Result};
|
||
use clap::{Parser, Subcommand};
|
||
use colored::Colorize;
|
||
use dialoguer::{Confirm, Input, Select};
|
||
use std::path::PathBuf;
|
||
|
||
use crate::config::manager::ConfigManager;
|
||
use crate::config::{GitProfile, TokenConfig, TokenType};
|
||
use crate::config::profile::{GpgConfig, SshConfig};
|
||
use crate::git::find_repo;
|
||
use crate::utils::validators::validate_profile_name;
|
||
|
||
/// Manage Git profiles
|
||
#[derive(Parser)]
|
||
pub struct ProfileCommand {
|
||
#[command(subcommand)]
|
||
command: Option<ProfileSubcommand>,
|
||
}
|
||
|
||
#[derive(Subcommand)]
|
||
enum ProfileSubcommand {
|
||
/// Add a new profile
|
||
Add,
|
||
|
||
/// Remove a profile
|
||
Remove {
|
||
/// Profile name
|
||
name: String,
|
||
},
|
||
|
||
/// List all profiles
|
||
List,
|
||
|
||
/// Show profile details
|
||
Show {
|
||
/// Profile name
|
||
name: Option<String>,
|
||
},
|
||
|
||
/// Edit a profile
|
||
Edit {
|
||
/// Profile name
|
||
name: String,
|
||
},
|
||
|
||
/// Set default profile
|
||
SetDefault {
|
||
/// Profile name
|
||
name: String,
|
||
},
|
||
|
||
/// Set profile for current repository
|
||
SetRepo {
|
||
/// Profile name
|
||
name: String,
|
||
},
|
||
|
||
/// Apply profile to current repository
|
||
Apply {
|
||
/// Profile name (uses default if not specified)
|
||
name: Option<String>,
|
||
|
||
/// Apply globally instead of to current repo
|
||
#[arg(short, long)]
|
||
global: bool,
|
||
},
|
||
|
||
/// Switch between profiles interactively
|
||
Switch,
|
||
|
||
/// Copy/duplicate a profile
|
||
Copy {
|
||
/// Source profile name
|
||
from: String,
|
||
|
||
/// New profile name
|
||
to: String,
|
||
},
|
||
|
||
/// Manage tokens for a profile
|
||
Token {
|
||
#[command(subcommand)]
|
||
token_command: TokenSubcommand,
|
||
},
|
||
|
||
/// Check profile configuration against git
|
||
Check {
|
||
/// Profile name
|
||
name: Option<String>,
|
||
},
|
||
|
||
/// Show usage statistics
|
||
Stats {
|
||
/// Profile name
|
||
name: Option<String>,
|
||
},
|
||
}
|
||
|
||
#[derive(Subcommand)]
|
||
enum TokenSubcommand {
|
||
/// Add a token to a profile
|
||
Add {
|
||
/// Profile name
|
||
profile: String,
|
||
|
||
/// Service name (e.g., github, gitlab)
|
||
service: String,
|
||
},
|
||
|
||
/// Remove a token from a profile
|
||
Remove {
|
||
/// Profile name
|
||
profile: String,
|
||
|
||
/// Service name
|
||
service: String,
|
||
},
|
||
|
||
/// List tokens in a profile
|
||
List {
|
||
/// Profile name
|
||
profile: String,
|
||
},
|
||
}
|
||
|
||
impl ProfileCommand {
|
||
pub async fn execute(&self, config_path: Option<PathBuf>) -> Result<()> {
|
||
match &self.command {
|
||
Some(ProfileSubcommand::Add) => self.add_profile(&config_path).await,
|
||
Some(ProfileSubcommand::Remove { name }) => self.remove_profile(name, &config_path).await,
|
||
Some(ProfileSubcommand::List) => self.list_profiles(&config_path).await,
|
||
Some(ProfileSubcommand::Show { name }) => self.show_profile(name.as_deref(), &config_path).await,
|
||
Some(ProfileSubcommand::Edit { name }) => self.edit_profile(name, &config_path).await,
|
||
Some(ProfileSubcommand::SetDefault { name }) => self.set_default(name, &config_path).await,
|
||
Some(ProfileSubcommand::SetRepo { name }) => self.set_repo(name, &config_path).await,
|
||
Some(ProfileSubcommand::Apply { name, global }) => self.apply_profile(name.as_deref(), *global, &config_path).await,
|
||
Some(ProfileSubcommand::Switch) => self.switch_profile(&config_path).await,
|
||
Some(ProfileSubcommand::Copy { from, to }) => self.copy_profile(from, to, &config_path).await,
|
||
Some(ProfileSubcommand::Token { token_command }) => self.handle_token_command(token_command, &config_path).await,
|
||
Some(ProfileSubcommand::Check { name }) => self.check_profile(name.as_deref(), &config_path).await,
|
||
Some(ProfileSubcommand::Stats { name }) => self.show_stats(name.as_deref(), &config_path).await,
|
||
None => self.list_profiles(&config_path).await,
|
||
}
|
||
}
|
||
|
||
fn get_manager(&self, config_path: &Option<PathBuf>) -> Result<ConfigManager> {
|
||
match config_path {
|
||
Some(path) => ConfigManager::with_path(path),
|
||
None => ConfigManager::new(),
|
||
}
|
||
}
|
||
|
||
async fn add_profile(&self, config_path: &Option<PathBuf>) -> Result<()> {
|
||
let mut manager = self.get_manager(config_path)?;
|
||
|
||
println!("{}", "\nAdd new profile".bold());
|
||
println!("{}", "─".repeat(40));
|
||
|
||
let name: String = Input::new()
|
||
.with_prompt("Profile name")
|
||
.validate_with(|input: &String| {
|
||
validate_profile_name(input).map_err(|e| e.to_string())
|
||
})
|
||
.interact_text()?;
|
||
|
||
if manager.has_profile(&name) {
|
||
bail!("Profile '{}' already exists", name);
|
||
}
|
||
|
||
let user_name: String = Input::new()
|
||
.with_prompt("Git user name")
|
||
.interact_text()?;
|
||
|
||
let user_email: String = Input::new()
|
||
.with_prompt("Git user email")
|
||
.validate_with(|input: &String| {
|
||
crate::utils::validators::validate_email(input).map_err(|e| e.to_string())
|
||
})
|
||
.interact_text()?;
|
||
|
||
let description: String = Input::new()
|
||
.with_prompt("Description (optional)")
|
||
.allow_empty(true)
|
||
.interact_text()?;
|
||
|
||
let is_work = Confirm::new()
|
||
.with_prompt("Is this a work profile?")
|
||
.default(false)
|
||
.interact()?;
|
||
|
||
let organization = if is_work {
|
||
Some(Input::new()
|
||
.with_prompt("Organization")
|
||
.interact_text()?)
|
||
} else {
|
||
None
|
||
};
|
||
|
||
let mut profile = GitProfile::new(name.clone(), user_name, user_email);
|
||
|
||
if !description.is_empty() {
|
||
profile.description = Some(description);
|
||
}
|
||
profile.is_work = is_work;
|
||
profile.organization = organization;
|
||
|
||
let setup_ssh = Confirm::new()
|
||
.with_prompt("Configure SSH key?")
|
||
.default(false)
|
||
.interact()?;
|
||
|
||
if setup_ssh {
|
||
profile.ssh = Some(self.setup_ssh_interactive().await?);
|
||
}
|
||
|
||
let setup_gpg = Confirm::new()
|
||
.with_prompt("Configure GPG signing?")
|
||
.default(false)
|
||
.interact()?;
|
||
|
||
if setup_gpg {
|
||
profile.gpg = Some(self.setup_gpg_interactive().await?);
|
||
}
|
||
|
||
let setup_token = Confirm::new()
|
||
.with_prompt("Add a Personal Access Token?")
|
||
.default(false)
|
||
.interact()?;
|
||
|
||
if setup_token {
|
||
self.setup_token_interactive(&mut profile).await?;
|
||
}
|
||
|
||
manager.add_profile(name.clone(), profile)?;
|
||
manager.save()?;
|
||
|
||
println!("{} Profile '{}' added successfully", "✓".green(), name.cyan());
|
||
|
||
if manager.default_profile().is_none() {
|
||
let set_default = Confirm::new()
|
||
.with_prompt("Set as default profile?")
|
||
.default(true)
|
||
.interact()?;
|
||
|
||
if set_default {
|
||
manager.set_default_profile(Some(name.clone()))?;
|
||
manager.save()?;
|
||
println!("{} Set '{}' as default profile", "✓".green(), name.cyan());
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
async fn remove_profile(&self, name: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||
let mut manager = self.get_manager(config_path)?;
|
||
|
||
if !manager.has_profile(name) {
|
||
bail!("Profile '{}' not found", name);
|
||
}
|
||
|
||
let confirm = Confirm::new()
|
||
.with_prompt(&format!("Are you sure you want to remove profile '{}'?", name))
|
||
.default(false)
|
||
.interact()?;
|
||
|
||
if !confirm {
|
||
println!("{}", "Cancelled.".yellow());
|
||
return Ok(());
|
||
}
|
||
|
||
manager.remove_profile(name)?;
|
||
manager.save()?;
|
||
|
||
println!("{} Profile '{}' removed", "✓".green(), name);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
async fn list_profiles(&self, config_path: &Option<PathBuf>) -> Result<()> {
|
||
let manager = self.get_manager(config_path)?;
|
||
|
||
let profiles = manager.list_profiles();
|
||
|
||
if profiles.is_empty() {
|
||
println!("{}", "No profiles configured.".yellow());
|
||
println!("Run {} to create one.", "quicommit profile add".cyan());
|
||
return Ok(());
|
||
}
|
||
|
||
let default = manager.default_profile_name();
|
||
|
||
println!("{}", "\nConfigured profiles:".bold());
|
||
println!("{}", "─".repeat(60));
|
||
|
||
for name in profiles {
|
||
let profile = manager.get_profile(name).unwrap();
|
||
let is_default = default.map(|d| d == name).unwrap_or(false);
|
||
|
||
let marker = if is_default { "●".green() } else { "○".dimmed() };
|
||
let work_marker = if profile.is_work { " [work]".yellow() } else { "".normal() };
|
||
|
||
println!("{} {}{}", marker, name.cyan().bold(), work_marker);
|
||
println!(" {} <{}>", profile.user_name, profile.user_email);
|
||
|
||
if let Some(ref desc) = profile.description {
|
||
println!(" {}", desc.dimmed());
|
||
}
|
||
|
||
if profile.has_ssh() {
|
||
println!(" {} SSH configured", "🔑".to_string().dimmed());
|
||
}
|
||
if profile.has_gpg() {
|
||
println!(" {} GPG configured", "🔒".to_string().dimmed());
|
||
}
|
||
if profile.has_tokens() {
|
||
println!(" {} {} token(s)", "🔐".to_string().dimmed(), profile.tokens.len());
|
||
}
|
||
|
||
if let Some(ref usage) = profile.usage.last_used {
|
||
println!(" {} Last used: {}", "📊".to_string().dimmed(), usage.dimmed());
|
||
}
|
||
|
||
println!();
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
async fn show_profile(&self, name: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
|
||
let manager = self.get_manager(config_path)?;
|
||
|
||
let profile = if let Some(n) = name {
|
||
manager.get_profile(n)
|
||
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", n))?
|
||
} else {
|
||
manager.default_profile()
|
||
.ok_or_else(|| anyhow::anyhow!("No default profile set"))?
|
||
};
|
||
|
||
println!("{}", format!("\nProfile: {}", profile.name).bold());
|
||
println!("{}", "─".repeat(40));
|
||
println!("User name: {}", profile.user_name);
|
||
println!("User email: {}", profile.user_email);
|
||
|
||
if let Some(ref desc) = profile.description {
|
||
println!("Description: {}", desc);
|
||
}
|
||
|
||
println!("Work profile: {}", if profile.is_work { "yes".yellow() } else { "no".normal() });
|
||
|
||
if let Some(ref org) = profile.organization {
|
||
println!("Organization: {}", org);
|
||
}
|
||
|
||
if let Some(ref ssh) = profile.ssh {
|
||
println!("\n{}", "SSH Configuration:".bold());
|
||
if let Some(ref path) = ssh.private_key_path {
|
||
println!(" Private key: {:?}", path);
|
||
}
|
||
}
|
||
|
||
if let Some(ref gpg) = profile.gpg {
|
||
println!("\n{}", "GPG Configuration:".bold());
|
||
println!(" Key ID: {}", gpg.key_id);
|
||
println!(" Program: {}", gpg.program);
|
||
println!(" Use agent: {}", if gpg.use_agent { "yes" } else { "no" });
|
||
}
|
||
|
||
if !profile.tokens.is_empty() {
|
||
println!("\n{}", "Tokens:".bold());
|
||
for (service, token) in &profile.tokens {
|
||
println!(" {} ({})", service.cyan(), token.token_type);
|
||
if let Some(ref desc) = token.description {
|
||
println!(" {}", desc);
|
||
}
|
||
}
|
||
}
|
||
|
||
if profile.usage.total_uses > 0 {
|
||
println!("\n{}", "Usage Statistics:".bold());
|
||
println!(" Total uses: {}", profile.usage.total_uses);
|
||
if let Some(ref last_used) = profile.usage.last_used {
|
||
println!(" Last used: {}", last_used);
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
async fn edit_profile(&self, name: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||
let mut manager = self.get_manager(config_path)?;
|
||
|
||
let profile = manager.get_profile(name)
|
||
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", name))?
|
||
.clone();
|
||
|
||
println!("{}", format!("\nEditing profile: {}", name).bold());
|
||
println!("{}", "─".repeat(40));
|
||
|
||
let user_name: String = Input::new()
|
||
.with_prompt("Git user name")
|
||
.default(profile.user_name.clone())
|
||
.interact_text()?;
|
||
|
||
let user_email: String = Input::new()
|
||
.with_prompt("Git user email")
|
||
.default(profile.user_email.clone())
|
||
.validate_with(|input: &String| {
|
||
crate::utils::validators::validate_email(input).map_err(|e| e.to_string())
|
||
})
|
||
.interact_text()?;
|
||
|
||
let mut new_profile = GitProfile::new(name.to_string(), user_name, user_email);
|
||
new_profile.description = profile.description;
|
||
new_profile.is_work = profile.is_work;
|
||
new_profile.organization = profile.organization;
|
||
new_profile.ssh = profile.ssh;
|
||
new_profile.gpg = profile.gpg;
|
||
new_profile.tokens = profile.tokens;
|
||
new_profile.usage = profile.usage;
|
||
|
||
manager.update_profile(name, new_profile)?;
|
||
manager.save()?;
|
||
|
||
println!("{} Profile '{}' updated", "✓".green(), name);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
async fn set_default(&self, name: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||
let mut manager = self.get_manager(config_path)?;
|
||
|
||
manager.set_default_profile(Some(name.to_string()))?;
|
||
manager.save()?;
|
||
|
||
println!("{} Set '{}' as default profile", "✓".green(), name.cyan());
|
||
|
||
Ok(())
|
||
}
|
||
|
||
async fn set_repo(&self, name: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||
let mut manager = self.get_manager(config_path)?;
|
||
let repo = find_repo(std::env::current_dir()?.as_path())?;
|
||
|
||
let repo_path = repo.path().to_string_lossy().to_string();
|
||
|
||
manager.set_repo_profile(repo_path.clone(), name.to_string())?;
|
||
|
||
// Get the profile and apply it to the repository
|
||
let profile = manager.get_profile(name)
|
||
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", name))?;
|
||
|
||
profile.apply_to_repo(repo.inner())?;
|
||
manager.record_profile_usage(name, Some(repo_path))?;
|
||
manager.save()?;
|
||
|
||
println!("{} Set '{}' for current repository", "✓".green(), name.cyan());
|
||
println!("{} Applied profile '{}' to current repository", "✓".green(), name.cyan());
|
||
|
||
Ok(())
|
||
}
|
||
|
||
async fn apply_profile(&self, name: Option<&str>, global: bool, config_path: &Option<PathBuf>) -> Result<()> {
|
||
let mut manager = self.get_manager(config_path)?;
|
||
|
||
let profile_name = if let Some(n) = name {
|
||
n.to_string()
|
||
} else {
|
||
manager.default_profile_name()
|
||
.ok_or_else(|| anyhow::anyhow!("No default profile set"))?
|
||
.clone()
|
||
};
|
||
|
||
let profile = manager.get_profile(&profile_name)
|
||
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", profile_name))?
|
||
.clone();
|
||
|
||
let repo_path = if global {
|
||
None
|
||
} else {
|
||
let repo = find_repo(std::env::current_dir()?.as_path())?;
|
||
Some(repo.path().to_string_lossy().to_string())
|
||
};
|
||
|
||
if global {
|
||
profile.apply_global()?;
|
||
println!("{} Applied profile '{}' globally", "✓".green(), profile.name.cyan());
|
||
} else {
|
||
let repo = find_repo(std::env::current_dir()?.as_path())?;
|
||
profile.apply_to_repo(repo.inner())?;
|
||
println!("{} Applied profile '{}' to current repository", "✓".green(), profile.name.cyan());
|
||
}
|
||
|
||
manager.record_profile_usage(&profile_name, repo_path)?;
|
||
manager.save()?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
async fn switch_profile(&self, config_path: &Option<PathBuf>) -> Result<()> {
|
||
let mut manager = self.get_manager(config_path)?;
|
||
|
||
let profiles: Vec<String> = manager.list_profiles()
|
||
.into_iter()
|
||
.map(|s| s.to_string())
|
||
.collect();
|
||
|
||
if profiles.is_empty() {
|
||
bail!("No profiles configured");
|
||
}
|
||
|
||
let default = manager.default_profile_name().map(|s| s.as_str());
|
||
let default_idx = default
|
||
.and_then(|d| profiles.iter().position(|p| p == d))
|
||
.unwrap_or(0);
|
||
|
||
let selection = Select::new()
|
||
.with_prompt("Select profile to switch to")
|
||
.items(&profiles)
|
||
.default(default_idx)
|
||
.interact()?;
|
||
|
||
let selected = &profiles[selection];
|
||
|
||
manager.set_default_profile(Some(selected.clone()))?;
|
||
manager.save()?;
|
||
|
||
println!("{} Switched to profile '{}'", "✓".green(), selected.cyan());
|
||
|
||
if find_repo(".").is_ok() {
|
||
let apply = Confirm::new()
|
||
.with_prompt("Apply to current repository?")
|
||
.default(true)
|
||
.interact()?;
|
||
|
||
if apply {
|
||
self.apply_profile(Some(selected), false, config_path).await?;
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
async fn copy_profile(&self, from: &str, to: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||
let mut manager = self.get_manager(config_path)?;
|
||
|
||
let source = manager.get_profile(from)
|
||
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", from))?
|
||
.clone();
|
||
|
||
validate_profile_name(to)?;
|
||
|
||
let mut new_profile = source.clone();
|
||
new_profile.name = to.to_string();
|
||
new_profile.usage = Default::default();
|
||
|
||
manager.add_profile(to.to_string(), new_profile)?;
|
||
manager.save()?;
|
||
|
||
println!("{} Copied profile '{}' to '{}'", "✓".green(), from, to.cyan());
|
||
|
||
Ok(())
|
||
}
|
||
|
||
async fn handle_token_command(&self, cmd: &TokenSubcommand, config_path: &Option<PathBuf>) -> Result<()> {
|
||
match cmd {
|
||
TokenSubcommand::Add { profile, service } => self.add_token(profile, service, config_path).await,
|
||
TokenSubcommand::Remove { profile, service } => self.remove_token(profile, service, config_path).await,
|
||
TokenSubcommand::List { profile } => self.list_tokens(profile, config_path).await,
|
||
}
|
||
}
|
||
|
||
async fn add_token(&self, profile_name: &str, service: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||
let mut manager = self.get_manager(config_path)?;
|
||
|
||
if !manager.has_profile(profile_name) {
|
||
bail!("Profile '{}' not found", profile_name);
|
||
}
|
||
|
||
println!("{}", format!("\nAdd token to profile '{}'", profile_name).bold());
|
||
println!("{}", "─".repeat(40));
|
||
|
||
let token_value: String = Input::new()
|
||
.with_prompt(&format!("Token for {}", service))
|
||
.interact_text()?;
|
||
|
||
let token_type_options = vec!["Personal", "OAuth", "Deploy", "App"];
|
||
let selection = Select::new()
|
||
.with_prompt("Token type")
|
||
.items(&token_type_options)
|
||
.default(0)
|
||
.interact()?;
|
||
|
||
let token_type = match selection {
|
||
0 => TokenType::Personal,
|
||
1 => TokenType::OAuth,
|
||
2 => TokenType::Deploy,
|
||
3 => TokenType::App,
|
||
_ => TokenType::Personal,
|
||
};
|
||
|
||
let description: String = Input::new()
|
||
.with_prompt("Description (optional)")
|
||
.allow_empty(true)
|
||
.interact_text()?;
|
||
|
||
let mut token = TokenConfig::new(token_value, token_type);
|
||
if !description.is_empty() {
|
||
token.description = Some(description);
|
||
}
|
||
|
||
manager.add_token_to_profile(profile_name, service.to_string(), token)?;
|
||
manager.save()?;
|
||
|
||
println!("{} Token for '{}' added to profile '{}'", "✓".green(), service.cyan(), profile_name);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
async fn remove_token(&self, profile_name: &str, service: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||
let mut manager = self.get_manager(config_path)?;
|
||
|
||
if !manager.has_profile(profile_name) {
|
||
bail!("Profile '{}' not found", profile_name);
|
||
}
|
||
|
||
let confirm = Confirm::new()
|
||
.with_prompt(&format!("Remove token '{}' from profile '{}'?", service, profile_name))
|
||
.default(false)
|
||
.interact()?;
|
||
|
||
if !confirm {
|
||
println!("{}", "Cancelled.".yellow());
|
||
return Ok(());
|
||
}
|
||
|
||
manager.remove_token_from_profile(profile_name, service)?;
|
||
manager.save()?;
|
||
|
||
println!("{} Token '{}' removed from profile '{}'", "✓".green(), service, profile_name);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
async fn list_tokens(&self, profile_name: &str, config_path: &Option<PathBuf>) -> Result<()> {
|
||
let manager = self.get_manager(config_path)?;
|
||
|
||
let profile = manager.get_profile(profile_name)
|
||
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", profile_name))?;
|
||
|
||
if profile.tokens.is_empty() {
|
||
println!("{} No tokens configured for profile '{}'", "ℹ".yellow(), profile_name);
|
||
return Ok(());
|
||
}
|
||
|
||
println!("{}", format!("\nTokens for profile '{}':", profile_name).bold());
|
||
println!("{}", "─".repeat(40));
|
||
|
||
for (service, token) in &profile.tokens {
|
||
println!("{} ({})", service.cyan().bold(), token.token_type);
|
||
if let Some(ref desc) = token.description {
|
||
println!(" {}", desc);
|
||
}
|
||
if let Some(ref last_used) = token.last_used {
|
||
println!(" Last used: {}", last_used);
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
async fn check_profile(&self, name: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
|
||
let manager = self.get_manager(config_path)?;
|
||
|
||
let profile_name = if let Some(n) = name {
|
||
n.to_string()
|
||
} else {
|
||
manager.default_profile_name()
|
||
.ok_or_else(|| anyhow::anyhow!("No default profile set"))?
|
||
.clone()
|
||
};
|
||
|
||
let repo = find_repo(std::env::current_dir()?.as_path())?;
|
||
let comparison = manager.check_profile_config(&profile_name, repo.inner())?;
|
||
|
||
println!("{}", format!("\nChecking profile '{}' against git configuration", profile_name).bold());
|
||
println!("{}", "─".repeat(60));
|
||
|
||
if comparison.matches {
|
||
println!("{} Profile configuration matches git settings", "✓".green().bold());
|
||
} else {
|
||
println!("{} Profile configuration differs from git settings", "✗".red().bold());
|
||
println!("\n{}", "Differences:".bold());
|
||
|
||
for diff in &comparison.differences {
|
||
println!("\n {}:", diff.key.cyan());
|
||
println!(" Profile: {}", diff.profile_value.green());
|
||
println!(" Git: {}", diff.git_value.yellow());
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
async fn show_stats(&self, name: Option<&str>, config_path: &Option<PathBuf>) -> Result<()> {
|
||
let manager = self.get_manager(config_path)?;
|
||
|
||
if let Some(n) = name {
|
||
let profile = manager.get_profile(n)
|
||
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", n))?;
|
||
|
||
self.show_single_profile_stats(profile);
|
||
} else {
|
||
let profiles = manager.list_profiles();
|
||
|
||
if profiles.is_empty() {
|
||
println!("{}", "No profiles configured.".yellow());
|
||
return Ok(());
|
||
}
|
||
|
||
println!("{}", "\nProfile Usage Statistics:".bold());
|
||
println!("{}", "─".repeat(60));
|
||
|
||
for profile_name in profiles {
|
||
if let Some(profile) = manager.get_profile(profile_name) {
|
||
self.show_single_profile_stats(profile);
|
||
println!();
|
||
}
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn show_single_profile_stats(&self, profile: &GitProfile) {
|
||
println!("{}", format!("\n{}", profile.name).bold());
|
||
println!(" Total uses: {}", profile.usage.total_uses);
|
||
|
||
if let Some(ref last_used) = profile.usage.last_used {
|
||
println!(" Last used: {}", last_used);
|
||
}
|
||
|
||
if !profile.usage.repo_usage.is_empty() {
|
||
println!(" Repositories:");
|
||
for (repo, count) in &profile.usage.repo_usage {
|
||
println!(" {} ({} uses)", repo, count);
|
||
}
|
||
}
|
||
}
|
||
|
||
async fn setup_ssh_interactive(&self) -> 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("SSH private key path")
|
||
.default(ssh_dir.join("id_rsa").display().to_string())
|
||
.interact_text()?;
|
||
|
||
Ok(SshConfig {
|
||
private_key_path: Some(PathBuf::from(key_path)),
|
||
public_key_path: None,
|
||
passphrase: None,
|
||
agent_forwarding: false,
|
||
ssh_command: None,
|
||
known_hosts_file: None,
|
||
})
|
||
}
|
||
|
||
async fn setup_gpg_interactive(&self) -> Result<GpgConfig> {
|
||
let key_id: String = Input::new()
|
||
.with_prompt("GPG key ID")
|
||
.interact_text()?;
|
||
|
||
Ok(GpgConfig {
|
||
key_id,
|
||
program: "gpg".to_string(),
|
||
home_dir: None,
|
||
passphrase: None,
|
||
use_agent: true,
|
||
})
|
||
}
|
||
|
||
async fn setup_token_interactive(&self, profile: &mut GitProfile) -> Result<()> {
|
||
let service: String = Input::new()
|
||
.with_prompt("Service name (e.g., github, gitlab)")
|
||
.interact_text()?;
|
||
|
||
let token_value: String = Input::new()
|
||
.with_prompt("Token value")
|
||
.interact_text()?;
|
||
|
||
let token = TokenConfig::new(token_value, TokenType::Personal);
|
||
profile.add_token(service, token);
|
||
|
||
Ok(())
|
||
}
|
||
}
|