use std::fs;
use std::path::{Path, PathBuf};
pub fn hook_path(git_root: &Path) -> PathBuf {
git_root.join(".git").join("hooks").join("commit-msg")
}
pub fn generate_hook_script() -> String {
r#"#!/bin/sh
# Hook para validar Conventional Commits
# Gerado automaticamente pelo cc-hook
cc-hook validate "$1"
"#
.to_string()
}
pub fn install_hook(git_root: &Path) -> Result<Option<PathBuf>, HookInstallError> {
let hooks_dir = git_root.join(".git").join("hooks");
let hook_file = hooks_dir.join("commit-msg");
fs::create_dir_all(&hooks_dir)
.map_err(|e| HookInstallError::CreateDirFailed(e.to_string()))?;
let backup_path = if hook_file.exists() {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let backup = hooks_dir.join(format!("commit-msg.backup.{timestamp}"));
fs::copy(&hook_file, &backup)
.map_err(|e| HookInstallError::WriteFailed(e.to_string()))?;
Some(backup)
} else {
None
};
fs::write(&hook_file, generate_hook_script())
.map_err(|e| HookInstallError::WriteFailed(e.to_string()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o755);
fs::set_permissions(&hook_file, perms)
.map_err(|e| HookInstallError::WriteFailed(e.to_string()))?;
}
Ok(backup_path)
}
#[derive(Debug, PartialEq)]
pub enum HookInstallError {
CreateDirFailed(String),
WriteFailed(String),
}
impl std::fmt::Display for HookInstallError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HookInstallError::CreateDirFailed(e) => write!(f, "failed to create hooks dir: {e}"),
HookInstallError::WriteFailed(e) => write!(f, "failed to write hook file: {e}"),
}
}
}
impl std::error::Error for HookInstallError {}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn create_temp_dir() -> tempfile::TempDir {
tempfile::tempdir().expect("failed to create temp dir")
}
fn setup_fake_repo(tmp: &tempfile::TempDir) -> PathBuf {
let root = tmp.path().to_path_buf();
fs::create_dir_all(root.join(".git").join("hooks")).unwrap();
root
}
#[test]
fn hook_path_points_to_commit_msg() {
let path = hook_path(Path::new("/some/repo"));
assert_eq!(path, PathBuf::from("/some/repo/.git/hooks/commit-msg"));
}
#[test]
fn hook_script_contains_shebang() {
let script = generate_hook_script();
assert!(script.starts_with("#!/"));
}
#[test]
fn hook_script_invokes_cc_hook_validate() {
let script = generate_hook_script();
assert!(
script.contains("cc-hook validate") || script.contains("cc-hook\" validate"),
"hook script should invoke `cc-hook validate`"
);
}
#[test]
fn hook_script_passes_commit_msg_file_arg() {
let script = generate_hook_script();
assert!(
script.contains("$1"),
"hook script should pass the commit message file path ($1)"
);
}
#[test]
fn install_creates_hook_file() {
let tmp = create_temp_dir();
let root = setup_fake_repo(&tmp);
let result = install_hook(&root);
assert!(result.is_ok());
assert_eq!(result.unwrap(), None);
let hook = hook_path(&root);
assert!(hook.exists(), "hook file should exist after install");
}
#[test]
fn installed_hook_has_correct_content() {
let tmp = create_temp_dir();
let root = setup_fake_repo(&tmp);
install_hook(&root).unwrap();
let content = fs::read_to_string(hook_path(&root)).unwrap();
let expected = generate_hook_script();
assert_eq!(content, expected);
}
#[test]
fn install_creates_hooks_dir_if_missing() {
let tmp = create_temp_dir();
let root = tmp.path().to_path_buf();
fs::create_dir(root.join(".git")).unwrap();
let result = install_hook(&root);
assert!(result.is_ok());
assert!(root.join(".git").join("hooks").exists());
}
#[test]
fn install_backs_up_existing_hook() {
let tmp = create_temp_dir();
let root = setup_fake_repo(&tmp);
let hook = hook_path(&root);
fs::write(&hook, "#!/bin/bash\necho old hook").unwrap();
let result = install_hook(&root);
assert!(result.is_ok());
let backup = result.unwrap();
assert!(backup.is_some(), "should return backup path");
let backup_path = backup.unwrap();
assert!(backup_path.exists(), "backup file should exist");
let backup_content = fs::read_to_string(&backup_path).unwrap();
assert_eq!(backup_content, "#!/bin/bash\necho old hook");
}
#[test]
fn install_overwrites_existing_hook_with_new_content() {
let tmp = create_temp_dir();
let root = setup_fake_repo(&tmp);
let hook = hook_path(&root);
fs::write(&hook, "#!/bin/bash\necho old hook").unwrap();
install_hook(&root).unwrap();
let content = fs::read_to_string(&hook).unwrap();
assert_ne!(content, "#!/bin/bash\necho old hook");
assert_eq!(content, generate_hook_script());
}
#[cfg(unix)]
#[test]
fn installed_hook_is_executable() {
use std::os::unix::fs::PermissionsExt;
let tmp = create_temp_dir();
let root = setup_fake_repo(&tmp);
install_hook(&root).unwrap();
let hook = hook_path(&root);
let perms = fs::metadata(&hook).unwrap().permissions();
assert!(
perms.mode() & 0o111 != 0,
"hook file should be executable"
);
}
}