ferrous_forge/git_hooks/
installer.rs1use super::scripts::{COMMIT_MSG_HOOK, PRE_COMMIT_HOOK, PRE_PUSH_HOOK};
4use crate::{Error, Result};
5use std::path::Path;
6use tokio::fs;
7
8pub async fn install_git_hooks(project_path: &Path) -> Result<()> {
10 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 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_hook(&hooks_dir, "pre-commit", PRE_COMMIT_HOOK).await?;
30 println!(" ✅ Installed pre-commit hook");
31
32 install_hook(&hooks_dir, "pre-push", PRE_PUSH_HOOK).await?;
34 println!(" ✅ Installed pre-push hook");
35
36 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
52async fn install_hook(hooks_dir: &Path, name: &str, content: &str) -> Result<()> {
54 let hook_path = hooks_dir.join(name);
55
56 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 return Ok(());
65 }
66
67 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 fs::write(&hook_path, content)
82 .await
83 .map_err(|e| Error::process(format!("Failed to write hook: {}", e)))?;
84
85 #[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
102pub 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(()); }
108
109 let hooks_dir = git_dir.join("hooks");
110
111 println!("🗑️ Removing git hooks...");
112
113 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 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}