feat:(first commit)created repository and complete 0.1.0

This commit is contained in:
2026-01-30 14:18:32 +08:00
commit 5d4156e5e0
36 changed files with 8686 additions and 0 deletions

492
src/commands/profile.rs Normal file
View 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,
})
}
}