Files
QuiCommit/src/config/profile.rs

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());
}
}