use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GitHooksConfig {
#[serde(default = "default_true")]
pub enabled: bool,
pub framework: Option<HookFramework>,
#[serde(default = "default_true")]
pub auto_install: bool,
#[serde(default)]
pub auto_update: bool,
#[serde(default)]
pub run_after_install: bool,
#[serde(default)]
pub allow_remote: bool,
#[serde(default)]
pub pre_commit: Option<PreCommitConfig>,
#[serde(default)]
pub native: Option<NativeConfig>,
#[serde(skip)]
pub origin: crate::ai_hooks::ConfigOrigin,
}
impl Default for GitHooksConfig {
fn default() -> Self {
Self {
enabled: true,
framework: None,
auto_install: true,
auto_update: false,
run_after_install: false,
allow_remote: false,
pre_commit: None,
native: None,
origin: crate::ai_hooks::ConfigOrigin::Local,
}
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct NativeConfig {
#[serde(default)]
pub hooks: std::collections::BTreeMap<String, String>,
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum HookFramework {
PreCommit,
Husky,
Lefthook,
Native,
}
impl HookFramework {
pub fn as_str(self) -> &'static str {
match self {
HookFramework::PreCommit => "pre-commit",
HookFramework::Husky => "husky",
HookFramework::Lefthook => "lefthook",
HookFramework::Native => "native",
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PreCommitConfig {
pub version: Option<String>,
#[serde(default = "default_precommit_config")]
pub config: String,
#[serde(default = "default_true")]
pub install_hooks: bool,
}
impl Default for PreCommitConfig {
fn default() -> Self {
Self {
version: None,
config: default_precommit_config(),
install_hooks: true,
}
}
}
fn default_precommit_config() -> String {
".pre-commit-config.yaml".to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_enable_hooks_and_auto_install() {
let c = GitHooksConfig::default();
let parsed: GitHooksConfig = toml::from_str("").unwrap();
assert!(parsed.enabled);
assert!(parsed.auto_install);
assert!(!parsed.auto_update);
assert!(!parsed.run_after_install);
assert!(!parsed.allow_remote);
assert!(c.enabled, "Default::default must match serde defaults");
assert!(c.auto_install);
assert!(!c.auto_update);
assert!(!c.run_after_install);
assert!(!c.allow_remote);
}
#[test]
fn parses_pinned_pre_commit_version() {
let toml_str = r#"
[pre_commit]
version = "3.6.0"
"#;
let cfg: GitHooksConfig = toml::from_str(toml_str).unwrap();
let pc = cfg.pre_commit.expect("pre_commit block parsed");
assert_eq!(pc.version.as_deref(), Some("3.6.0"));
assert_eq!(pc.config, ".pre-commit-config.yaml");
assert!(pc.install_hooks);
}
#[test]
fn parses_framework_kebab_case() {
let toml_str = r#"framework = "pre-commit""#;
let cfg: GitHooksConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.framework, Some(HookFramework::PreCommit));
}
#[test]
fn parses_allow_remote_explicit() {
let toml_str = "allow_remote = true";
let cfg: GitHooksConfig = toml::from_str(toml_str).unwrap();
assert!(cfg.allow_remote);
}
#[test]
fn origin_defaults_to_local() {
let cfg: GitHooksConfig = toml::from_str("").unwrap();
assert_eq!(cfg.origin, crate::ai_hooks::ConfigOrigin::Local);
}
}