use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct GitConfig {
#[serde(default)]
pub user_name: Option<ConfigValue>,
#[serde(default)]
pub user_email: Option<ConfigValue>,
#[serde(default)]
pub signing: bool,
#[serde(default)]
pub signing_key: Option<String>,
#[serde(default)]
pub signing_format: Option<SigningFormat>,
#[serde(default)]
pub default_branch: Option<String>,
#[serde(default)]
pub pull_rebase: bool,
#[serde(default)]
pub auto_stash: bool,
#[serde(default)]
pub push_autosetup: bool,
#[serde(default)]
pub editor: Option<String>,
#[serde(default)]
pub autocrlf: Option<AutoCrlf>,
#[serde(default)]
pub eol: Option<String>,
#[serde(default)]
pub credential_helper: Option<String>,
#[serde(default)]
pub scope: ConfigScope,
#[serde(default)]
pub aliases: HashMap<String, String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum ConfigValue {
Plain(String),
FromEnv {
env: String,
#[serde(default)]
default: Option<String>,
},
}
impl ConfigValue {
pub fn resolve(&self) -> Option<String> {
match self {
ConfigValue::Plain(s) => Some(s.clone()),
ConfigValue::FromEnv { env, default } => {
std::env::var(env).ok().or_else(|| default.clone())
}
}
}
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ConfigScope {
#[default]
Global,
Local,
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SigningFormat {
Ssh,
Gpg,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum AutoCrlf {
True,
False,
Input,
}
impl AutoCrlf {
pub fn as_str(&self) -> &'static str {
match self {
AutoCrlf::True => "true",
AutoCrlf::False => "false",
AutoCrlf::Input => "input",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_value_plain() {
let value = ConfigValue::Plain("John Doe".to_string());
assert_eq!(value.resolve(), Some("John Doe".to_string()));
}
#[test]
#[allow(unsafe_code)]
fn test_config_value_from_env() {
unsafe { std::env::set_var("TEST_GIT_USER", "Jane Doe") };
let value = ConfigValue::FromEnv {
env: "TEST_GIT_USER".to_string(),
default: None,
};
assert_eq!(value.resolve(), Some("Jane Doe".to_string()));
unsafe { std::env::remove_var("TEST_GIT_USER") };
}
#[test]
#[allow(unsafe_code)]
fn test_config_value_from_env_with_default() {
unsafe { std::env::remove_var("TEST_GIT_USER_MISSING") };
let value = ConfigValue::FromEnv {
env: "TEST_GIT_USER_MISSING".to_string(),
default: Some("Default User".to_string()),
};
assert_eq!(value.resolve(), Some("Default User".to_string()));
}
#[test]
#[allow(unsafe_code)]
fn test_config_value_from_env_no_default() {
unsafe { std::env::remove_var("TEST_GIT_USER_NONE") };
let value = ConfigValue::FromEnv {
env: "TEST_GIT_USER_NONE".to_string(),
default: None,
};
assert_eq!(value.resolve(), None);
}
#[test]
fn test_autocrlf_as_str() {
assert_eq!(AutoCrlf::True.as_str(), "true");
assert_eq!(AutoCrlf::False.as_str(), "false");
assert_eq!(AutoCrlf::Input.as_str(), "input");
}
#[test]
fn test_config_scope_default() {
let scope = ConfigScope::default();
assert_eq!(scope, ConfigScope::Global);
}
#[test]
fn test_git_config_parsing() {
let toml_str = r#"
user_name = "John Doe"
user_email = { env = "GIT_EMAIL", default = "john@example.com" }
signing = true
signing_key = "~/.ssh/id_ed25519.pub"
default_branch = "main"
pull_rebase = true
auto_stash = true
push_autosetup = true
editor = "vim"
autocrlf = "input"
scope = "global"
[aliases]
co = "checkout"
br = "branch"
ci = "commit"
st = "status"
"#;
let config: GitConfig = toml::from_str(toml_str).expect("Failed to parse config");
assert!(matches!(config.user_name, Some(ConfigValue::Plain(_))));
assert!(matches!(
config.user_email,
Some(ConfigValue::FromEnv { .. })
));
assert!(config.signing);
assert_eq!(
config.signing_key,
Some("~/.ssh/id_ed25519.pub".to_string())
);
assert_eq!(config.default_branch, Some("main".to_string()));
assert!(config.pull_rebase);
assert!(config.auto_stash);
assert!(config.push_autosetup);
assert_eq!(config.editor, Some("vim".to_string()));
assert_eq!(config.autocrlf, Some(AutoCrlf::Input));
assert_eq!(config.scope, ConfigScope::Global);
assert_eq!(config.aliases.len(), 4);
assert_eq!(config.aliases.get("co"), Some(&"checkout".to_string()));
}
}