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, } #[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, }, /// 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, /// 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, }, /// Show usage statistics Stats { /// Profile name name: Option, }, } #[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) -> 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) -> Result { match config_path { Some(path) => ConfigManager::with_path(path), None => ConfigManager::new(), } } async fn add_profile(&self, config_path: &Option) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> Result<()> { let mut manager = self.get_manager(config_path)?; let profiles: Vec = 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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 { 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 { 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(()) } }