413 lines
10 KiB
Rust
413 lines
10 KiB
Rust
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<SshConfig>,
|
|
|
|
/// GPG configuration
|
|
#[serde(default)]
|
|
pub gpg: Option<GpgConfig>,
|
|
|
|
/// Signing key (for commit/tag signing)
|
|
#[serde(default)]
|
|
pub signing_key: Option<String>,
|
|
|
|
/// Profile description
|
|
#[serde(default)]
|
|
pub description: Option<String>,
|
|
|
|
/// Is this a work profile
|
|
#[serde(default)]
|
|
pub is_work: bool,
|
|
|
|
/// Company/Organization name (for work profiles)
|
|
#[serde(default)]
|
|
pub organization: Option<String>,
|
|
}
|
|
|
|
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<super::CommitFormat>,
|
|
|
|
/// Use this profile for specific repositories (path patterns)
|
|
#[serde(default)]
|
|
pub repo_patterns: Vec<String>,
|
|
|
|
/// Preferred LLM provider for this profile
|
|
#[serde(default)]
|
|
pub llm_provider: Option<String>,
|
|
|
|
/// Custom commit message template
|
|
#[serde(default)]
|
|
pub commit_template: Option<String>,
|
|
}
|
|
|
|
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<std::path::PathBuf>,
|
|
|
|
/// SSH public key path
|
|
pub public_key_path: Option<std::path::PathBuf>,
|
|
|
|
/// SSH key passphrase (encrypted)
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub passphrase: Option<String>,
|
|
|
|
/// SSH agent forwarding
|
|
#[serde(default)]
|
|
pub agent_forwarding: bool,
|
|
|
|
/// Custom SSH command
|
|
#[serde(default)]
|
|
pub ssh_command: Option<String>,
|
|
|
|
/// Known hosts file
|
|
#[serde(default)]
|
|
pub known_hosts_file: Option<std::path::PathBuf>,
|
|
}
|
|
|
|
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<String> {
|
|
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<std::path::PathBuf>,
|
|
|
|
/// Key passphrase (encrypted)
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub passphrase: Option<String>,
|
|
|
|
/// 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<String>,
|
|
user_name: Option<String>,
|
|
user_email: Option<String>,
|
|
settings: ProfileSettings,
|
|
ssh: Option<SshConfig>,
|
|
gpg: Option<GpgConfig>,
|
|
signing_key: Option<String>,
|
|
description: Option<String>,
|
|
is_work: bool,
|
|
organization: Option<String>,
|
|
}
|
|
|
|
impl GitProfileBuilder {
|
|
pub fn name(mut self, name: impl Into<String>) -> Self {
|
|
self.name = Some(name.into());
|
|
self
|
|
}
|
|
|
|
pub fn user_name(mut self, user_name: impl Into<String>) -> Self {
|
|
self.user_name = Some(user_name.into());
|
|
self
|
|
}
|
|
|
|
pub fn user_email(mut self, user_email: impl Into<String>) -> 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<String>) -> Self {
|
|
self.signing_key = Some(key.into());
|
|
self
|
|
}
|
|
|
|
pub fn description(mut self, desc: impl Into<String>) -> 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<String>) -> Self {
|
|
self.organization = Some(org.into());
|
|
self
|
|
}
|
|
|
|
pub fn build(self) -> Result<GitProfile> {
|
|
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());
|
|
}
|
|
}
|