use anyhow::{bail, Result}; use serde::{Deserialize, Serialize}; /// Git profile containing user identity and authentication settings #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GitProfile { /// Profile display name pub name: String, /// Git user name pub user_name: String, /// Git user email pub user_email: String, /// Profile settings #[serde(default)] pub settings: ProfileSettings, /// SSH configuration #[serde(default)] pub ssh: Option, /// GPG configuration #[serde(default)] pub gpg: Option, /// Signing key (for commit/tag signing) #[serde(default)] pub signing_key: Option, /// Profile description #[serde(default)] pub description: Option, /// Is this a work profile #[serde(default)] pub is_work: bool, /// Company/Organization name (for work profiles) #[serde(default)] pub organization: Option, } impl GitProfile { /// Create a new basic profile pub fn new(name: String, user_name: String, user_email: String) -> Self { Self { name, user_name, user_email, settings: ProfileSettings::default(), ssh: None, gpg: None, signing_key: None, description: None, is_work: false, organization: None, } } /// Create a builder for fluent API pub fn builder() -> GitProfileBuilder { GitProfileBuilder::default() } /// Validate the profile pub fn validate(&self) -> Result<()> { if self.user_name.is_empty() { bail!("User name cannot be empty"); } if self.user_email.is_empty() { bail!("User email cannot be empty"); } crate::utils::validators::validate_email(&self.user_email)?; if let Some(ref ssh) = self.ssh { ssh.validate()?; } if let Some(ref gpg) = self.gpg { gpg.validate()?; } Ok(()) } /// Check if profile has SSH configured pub fn has_ssh(&self) -> bool { self.ssh.is_some() } /// Check if profile has GPG configured pub fn has_gpg(&self) -> bool { self.gpg.is_some() || self.signing_key.is_some() } /// Get signing key (from GPG config or direct) pub fn signing_key(&self) -> Option<&str> { self.signing_key .as_ref() .map(|s| s.as_str()) .or_else(|| self.gpg.as_ref().map(|g| g.key_id.as_str())) } /// Apply this profile to a git repository pub fn apply_to_repo(&self, repo: &git2::Repository) -> Result<()> { let mut config = repo.config()?; // Set user info config.set_str("user.name", &self.user_name)?; config.set_str("user.email", &self.user_email)?; // Set signing key if available if let Some(key) = self.signing_key() { config.set_str("user.signingkey", key)?; if self.settings.auto_sign_commits { config.set_bool("commit.gpgsign", true)?; } if self.settings.auto_sign_tags { config.set_bool("tag.gpgsign", true)?; } } // Set SSH if configured if let Some(ref ssh) = self.ssh { if let Some(ref key_path) = ssh.private_key_path { config.set_str("core.sshCommand", &format!("ssh -i {}", key_path.display()))?; } } Ok(()) } /// Apply this profile globally pub fn apply_global(&self) -> Result<()> { let mut config = git2::Config::open_default()?; config.set_str("user.name", &self.user_name)?; config.set_str("user.email", &self.user_email)?; if let Some(key) = self.signing_key() { config.set_str("user.signingkey", key)?; } Ok(()) } } /// Profile settings #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProfileSettings { /// Automatically sign commits #[serde(default)] pub auto_sign_commits: bool, /// Automatically sign tags #[serde(default)] pub auto_sign_tags: bool, /// Default commit format for this profile #[serde(default)] pub default_commit_format: Option, /// Use this profile for specific repositories (path patterns) #[serde(default)] pub repo_patterns: Vec, /// Preferred LLM provider for this profile #[serde(default)] pub llm_provider: Option, /// Custom commit message template #[serde(default)] pub commit_template: Option, } impl Default for ProfileSettings { fn default() -> Self { Self { auto_sign_commits: false, auto_sign_tags: false, default_commit_format: None, repo_patterns: vec![], llm_provider: None, commit_template: None, } } } /// SSH configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SshConfig { /// SSH private key path pub private_key_path: Option, /// SSH public key path pub public_key_path: Option, /// SSH key passphrase (encrypted) #[serde(skip_serializing_if = "Option::is_none")] pub passphrase: Option, /// SSH agent forwarding #[serde(default)] pub agent_forwarding: bool, /// Custom SSH command #[serde(default)] pub ssh_command: Option, /// Known hosts file #[serde(default)] pub known_hosts_file: Option, } impl SshConfig { /// Validate SSH configuration pub fn validate(&self) -> Result<()> { if let Some(ref path) = self.private_key_path { if !path.exists() { bail!("SSH private key does not exist: {:?}", path); } } if let Some(ref path) = self.public_key_path { if !path.exists() { bail!("SSH public key does not exist: {:?}", path); } } Ok(()) } /// Get SSH command for git pub fn git_ssh_command(&self) -> Option { if let Some(ref cmd) = self.ssh_command { Some(cmd.clone()) } else if let Some(ref key_path) = self.private_key_path { Some(format!("ssh -i '{}'", key_path.display())) } else { None } } } /// GPG configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GpgConfig { /// GPG key ID pub key_id: String, /// GPG executable path #[serde(default = "default_gpg_program")] pub program: String, /// GPG home directory #[serde(default)] pub home_dir: Option, /// Key passphrase (encrypted) #[serde(skip_serializing_if = "Option::is_none")] pub passphrase: Option, /// Use GPG agent #[serde(default = "default_true")] pub use_agent: bool, } impl GpgConfig { /// Validate GPG configuration pub fn validate(&self) -> Result<()> { crate::utils::validators::validate_gpg_key_id(&self.key_id)?; Ok(()) } /// Get GPG program path pub fn program(&self) -> &str { &self.program } } fn default_gpg_program() -> String { "gpg".to_string() } fn default_true() -> bool { true } /// Git profile builder #[derive(Default)] pub struct GitProfileBuilder { name: Option, user_name: Option, user_email: Option, settings: ProfileSettings, ssh: Option, gpg: Option, signing_key: Option, description: Option, is_work: bool, organization: Option, } impl GitProfileBuilder { pub fn name(mut self, name: impl Into) -> Self { self.name = Some(name.into()); self } pub fn user_name(mut self, user_name: impl Into) -> Self { self.user_name = Some(user_name.into()); self } pub fn user_email(mut self, user_email: impl Into) -> Self { self.user_email = Some(user_email.into()); self } pub fn settings(mut self, settings: ProfileSettings) -> Self { self.settings = settings; self } pub fn ssh(mut self, ssh: SshConfig) -> Self { self.ssh = Some(ssh); self } pub fn gpg(mut self, gpg: GpgConfig) -> Self { self.gpg = Some(gpg); self } pub fn signing_key(mut self, key: impl Into) -> Self { self.signing_key = Some(key.into()); self } pub fn description(mut self, desc: impl Into) -> Self { self.description = Some(desc.into()); self } pub fn work(mut self, is_work: bool) -> Self { self.is_work = is_work; self } pub fn organization(mut self, org: impl Into) -> Self { self.organization = Some(org.into()); self } pub fn build(self) -> Result { let name = self.name.ok_or_else(|| anyhow::anyhow!("Name is required"))?; let user_name = self.user_name.ok_or_else(|| anyhow::anyhow!("User name is required"))?; let user_email = self.user_email.ok_or_else(|| anyhow::anyhow!("User email is required"))?; Ok(GitProfile { name, user_name, user_email, settings: self.settings, ssh: self.ssh, gpg: self.gpg, signing_key: self.signing_key, description: self.description, is_work: self.is_work, organization: self.organization, }) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_profile_builder() { let profile = GitProfile::builder() .name("personal") .user_name("John Doe") .user_email("john@example.com") .description("Personal profile") .build() .unwrap(); assert_eq!(profile.name, "personal"); assert_eq!(profile.user_name, "John Doe"); assert_eq!(profile.user_email, "john@example.com"); assert!(profile.validate().is_ok()); } #[test] fn test_profile_validation() { let profile = GitProfile::new( "test".to_string(), "".to_string(), "invalid-email".to_string(), ); assert!(profile.validate().is_err()); } }