feat:(first commit)created repository and complete 0.1.0
This commit is contained in:
492
src/commands/profile.rs
Normal file
492
src/commands/profile.rs
Normal file
@@ -0,0 +1,492 @@
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use colored::Colorize;
|
||||
use dialoguer::{Confirm, Input, Select};
|
||||
|
||||
use crate::config::manager::ConfigManager;
|
||||
use crate::config::{GitProfile};
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
impl ProfileCommand {
|
||||
pub async fn execute(&self) -> Result<()> {
|
||||
match &self.command {
|
||||
Some(ProfileSubcommand::Add) => self.add_profile().await,
|
||||
Some(ProfileSubcommand::Remove { name }) => self.remove_profile(name).await,
|
||||
Some(ProfileSubcommand::List) => self.list_profiles().await,
|
||||
Some(ProfileSubcommand::Show { name }) => self.show_profile(name.as_deref()).await,
|
||||
Some(ProfileSubcommand::Edit { name }) => self.edit_profile(name).await,
|
||||
Some(ProfileSubcommand::SetDefault { name }) => self.set_default(name).await,
|
||||
Some(ProfileSubcommand::SetRepo { name }) => self.set_repo(name).await,
|
||||
Some(ProfileSubcommand::Apply { name, global }) => self.apply_profile(name.as_deref(), *global).await,
|
||||
Some(ProfileSubcommand::Switch) => self.switch_profile().await,
|
||||
Some(ProfileSubcommand::Copy { from, to }) => self.copy_profile(from, to).await,
|
||||
None => self.list_profiles().await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_profile(&self) -> Result<()> {
|
||||
let mut manager = ConfigManager::new()?;
|
||||
|
||||
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;
|
||||
|
||||
// SSH configuration
|
||||
let setup_ssh = Confirm::new()
|
||||
.with_prompt("Configure SSH key?")
|
||||
.default(false)
|
||||
.interact()?;
|
||||
|
||||
if setup_ssh {
|
||||
profile.ssh = Some(self.setup_ssh_interactive().await?);
|
||||
}
|
||||
|
||||
// GPG configuration
|
||||
let setup_gpg = Confirm::new()
|
||||
.with_prompt("Configure GPG signing?")
|
||||
.default(false)
|
||||
.interact()?;
|
||||
|
||||
if setup_gpg {
|
||||
profile.gpg = Some(self.setup_gpg_interactive().await?);
|
||||
}
|
||||
|
||||
manager.add_profile(name.clone(), profile)?;
|
||||
manager.save()?;
|
||||
|
||||
println!("{} Profile '{}' added successfully", "✓".green(), name.cyan());
|
||||
|
||||
// Offer to set as default
|
||||
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) -> Result<()> {
|
||||
let mut manager = ConfigManager::new()?;
|
||||
|
||||
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) -> Result<()> {
|
||||
let manager = ConfigManager::new()?;
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
println!();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn show_profile(&self, name: Option<&str>) -> Result<()> {
|
||||
let manager = ConfigManager::new()?;
|
||||
|
||||
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" });
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn edit_profile(&self, name: &str) -> Result<()> {
|
||||
let mut manager = ConfigManager::new()?;
|
||||
|
||||
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;
|
||||
|
||||
manager.update_profile(name, new_profile)?;
|
||||
manager.save()?;
|
||||
|
||||
println!("{} Profile '{}' updated", "✓".green(), name);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_default(&self, name: &str) -> Result<()> {
|
||||
let mut manager = ConfigManager::new()?;
|
||||
|
||||
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) -> Result<()> {
|
||||
let mut manager = ConfigManager::new()?;
|
||||
let repo = find_repo(".")?;
|
||||
|
||||
let repo_path = repo.path().to_string_lossy().to_string();
|
||||
|
||||
manager.set_repo_profile(repo_path, name.to_string())?;
|
||||
manager.save()?;
|
||||
|
||||
println!("{} Set '{}' for current repository", "✓".green(), name.cyan());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn apply_profile(&self, name: Option<&str>, global: bool) -> Result<()> {
|
||||
let manager = ConfigManager::new()?;
|
||||
|
||||
let profile = if let Some(n) = name {
|
||||
manager.get_profile(n)
|
||||
.ok_or_else(|| anyhow::anyhow!("Profile '{}' not found", n))?
|
||||
.clone()
|
||||
} else {
|
||||
manager.default_profile()
|
||||
.ok_or_else(|| anyhow::anyhow!("No default profile set"))?
|
||||
.clone()
|
||||
};
|
||||
|
||||
if global {
|
||||
profile.apply_global()?;
|
||||
println!("{} Applied profile '{}' globally", "✓".green(), profile.name.cyan());
|
||||
} else {
|
||||
let repo = find_repo(".")?;
|
||||
profile.apply_to_repo(repo.inner())?;
|
||||
println!("{} Applied profile '{}' to current repository", "✓".green(), profile.name.cyan());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn switch_profile(&self) -> Result<()> {
|
||||
let mut manager = ConfigManager::new()?;
|
||||
|
||||
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());
|
||||
|
||||
// Offer to apply to current repo
|
||||
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).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn copy_profile(&self, from: &str, to: &str) -> Result<()> {
|
||||
let mut manager = ConfigManager::new()?;
|
||||
|
||||
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();
|
||||
|
||||
manager.add_profile(to.to_string(), new_profile)?;
|
||||
manager.save()?;
|
||||
|
||||
println!("{} Copied profile '{}' to '{}'", "✓".green(), from, to.cyan());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user