ferrous_forge/git_hooks/
installer.rs

1//! Git hooks installation and removal logic
2
3use super::scripts::{COMMIT_MSG_HOOK, PRE_COMMIT_HOOK, PRE_PUSH_HOOK};
4use crate::{Error, Result};
5use std::path::Path;
6use tokio::fs;
7
8/// Install git hooks for a project
9pub async fn install_git_hooks(project_path: &Path) -> Result<()> {
10    // Check if we're in a git repository
11    let git_dir = project_path.join(".git");
12    if !git_dir.exists() {
13        return Err(Error::validation(
14            "Not a git repository. Run 'git init' first.".to_string(),
15        ));
16    }
17
18    // Ensure hooks directory exists
19    let hooks_dir = git_dir.join("hooks");
20    if !hooks_dir.exists() {
21        fs::create_dir_all(&hooks_dir)
22            .await
23            .map_err(|e| Error::process(format!("Failed to create hooks directory: {}", e)))?;
24    }
25
26    println!("📎 Installing git hooks...");
27
28    // Install pre-commit hook
29    install_hook(&hooks_dir, "pre-commit", PRE_COMMIT_HOOK).await?;
30    println!("  ✅ Installed pre-commit hook");
31
32    // Install pre-push hook
33    install_hook(&hooks_dir, "pre-push", PRE_PUSH_HOOK).await?;
34    println!("  ✅ Installed pre-push hook");
35
36    // Install commit-msg hook
37    install_hook(&hooks_dir, "commit-msg", COMMIT_MSG_HOOK).await?;
38    println!("  ✅ Installed commit-msg hook");
39
40    println!("🎉 Git hooks installed successfully!");
41    println!();
42    println!("Hooks will now run automatically:");
43    println!("  • pre-commit: Validates code before each commit");
44    println!("  • pre-push: Runs tests and full validation before push");
45    println!("  • commit-msg: Ensures conventional commit format");
46    println!();
47    println!("To bypass hooks temporarily, use: git commit --no-verify");
48
49    Ok(())
50}
51
52/// Install a single hook
53async fn install_hook(hooks_dir: &Path, name: &str, content: &str) -> Result<()> {
54    let hook_path = hooks_dir.join(name);
55
56    // Check if hook already exists
57    if hook_path.exists() {
58        let existing = fs::read_to_string(&hook_path)
59            .await
60            .map_err(|e| Error::process(format!("Failed to read existing hook: {}", e)))?;
61
62        if existing.contains("Ferrous Forge") {
63            // Our hook is already installed
64            return Ok(());
65        }
66
67        // Backup existing hook
68        let backup_path = hooks_dir.join(format!("{}.backup", name));
69        fs::rename(&hook_path, &backup_path)
70            .await
71            .map_err(|e| Error::process(format!("Failed to backup existing hook: {}", e)))?;
72
73        println!(
74            "  ⚠️  Backed up existing {} hook to {}",
75            name,
76            backup_path.display()
77        );
78    }
79
80    // Write hook content
81    fs::write(&hook_path, content)
82        .await
83        .map_err(|e| Error::process(format!("Failed to write hook: {}", e)))?;
84
85    // Make executable on Unix
86    #[cfg(unix)]
87    {
88        use std::os::unix::fs::PermissionsExt;
89        let mut perms = fs::metadata(&hook_path)
90            .await
91            .map_err(|e| Error::process(format!("Failed to get hook metadata: {}", e)))?
92            .permissions();
93        perms.set_mode(0o755);
94        fs::set_permissions(&hook_path, perms)
95            .await
96            .map_err(|e| Error::process(format!("Failed to set hook permissions: {}", e)))?;
97    }
98
99    Ok(())
100}
101
102/// Remove git hooks from a project
103pub async fn uninstall_git_hooks(project_path: &Path) -> Result<()> {
104    let git_dir = project_path.join(".git");
105    if !git_dir.exists() {
106        return Ok(()); // No git repo, nothing to uninstall
107    }
108
109    let hooks_dir = git_dir.join("hooks");
110
111    println!("🗑️  Removing git hooks...");
112
113    // Remove our hooks
114    for hook_name in &["pre-commit", "pre-push", "commit-msg"] {
115        let hook_path = hooks_dir.join(hook_name);
116        if hook_path.exists() {
117            let content = fs::read_to_string(&hook_path).await.unwrap_or_default();
118
119            if content.contains("Ferrous Forge") {
120                fs::remove_file(&hook_path)
121                    .await
122                    .map_err(|e| Error::process(format!("Failed to remove hook: {}", e)))?;
123                println!("  ✅ Removed {} hook", hook_name);
124
125                // Restore backup if exists
126                let backup_path = hooks_dir.join(format!("{}.backup", hook_name));
127                if backup_path.exists() {
128                    fs::rename(&backup_path, &hook_path)
129                        .await
130                        .map_err(|e| Error::process(format!("Failed to restore backup: {}", e)))?;
131                    println!("  ✅ Restored original {} hook", hook_name);
132                }
133            }
134        }
135    }
136
137    println!("🎉 Git hooks removed successfully!");
138    Ok(())
139}