ferrous_forge/git_hooks/
installer.rs1use super::scripts::{COMMIT_MSG_HOOK, PRE_COMMIT_HOOK, PRE_PUSH_HOOK};
7use crate::{Error, Result};
8use std::path::Path;
9use tokio::fs;
10
11pub async fn install_git_hooks(project_path: &Path) -> Result<()> {
21 let git_dir = project_path.join(".git");
23 if !git_dir.exists() {
24 return Err(Error::validation(
25 "Not a git repository. Run 'git init' first.".to_string(),
26 ));
27 }
28
29 let hooks_dir = git_dir.join("hooks");
31 if !hooks_dir.exists() {
32 fs::create_dir_all(&hooks_dir)
33 .await
34 .map_err(|e| Error::process(format!("Failed to create hooks directory: {}", e)))?;
35 }
36
37 println!("🔒 Installing mandatory safety hooks...");
38
39 install_hook(&hooks_dir, "pre-commit", PRE_COMMIT_HOOK).await?;
41 println!(" ✅ Installed blocking pre-commit hook");
42
43 install_hook(&hooks_dir, "pre-push", PRE_PUSH_HOOK).await?;
45 println!(" ✅ Installed blocking pre-push hook");
46
47 install_hook(&hooks_dir, "commit-msg", COMMIT_MSG_HOOK).await?;
49 println!(" ✅ Installed commit-msg hook");
50
51 println!();
52 println!("🛡️ Mandatory safety hooks installed!");
53 println!();
54 println!("These hooks will BLOCK commits/pushes that fail checks.");
55 println!("To bypass temporarily: ferrous-forge safety bypass --stage=...");
56
57 Ok(())
58}
59
60async fn install_hook(hooks_dir: &Path, name: &str, content: &str) -> Result<()> {
62 let hook_path = hooks_dir.join(name);
63
64 if hook_path.exists() {
66 let existing = fs::read_to_string(&hook_path)
67 .await
68 .map_err(|e| Error::process(format!("Failed to read existing hook: {}", e)))?;
69
70 if existing.contains("Ferrous Forge") {
71 if existing.contains("🛡️ FERROUS FORGE BLOCKED") {
73 return Ok(());
75 }
76 println!(" 🔄 Upgrading {} hook to blocking version", name);
78 } else {
79 let backup_path = hooks_dir.join(format!("{}.backup", name));
81 fs::rename(&hook_path, &backup_path)
82 .await
83 .map_err(|e| Error::process(format!("Failed to backup existing hook: {}", e)))?;
84
85 println!(
86 " ⚠️ Backed up existing {} hook to {}",
87 name,
88 backup_path.display()
89 );
90 }
91 }
92
93 fs::write(&hook_path, content)
95 .await
96 .map_err(|e| Error::process(format!("Failed to write hook: {}", e)))?;
97
98 #[cfg(unix)]
100 {
101 use std::os::unix::fs::PermissionsExt;
102 let mut perms = fs::metadata(&hook_path)
103 .await
104 .map_err(|e| Error::process(format!("Failed to get hook metadata: {}", e)))?
105 .permissions();
106 perms.set_mode(0o755);
107 fs::set_permissions(&hook_path, perms)
108 .await
109 .map_err(|e| Error::process(format!("Failed to set hook permissions: {}", e)))?;
110 }
111
112 Ok(())
113}
114
115pub async fn uninstall_git_hooks(project_path: &Path) -> Result<()> {
121 let git_dir = project_path.join(".git");
122 if !git_dir.exists() {
123 return Ok(()); }
125
126 let hooks_dir = git_dir.join("hooks");
127
128 println!("🗑️ Removing safety hooks...");
129
130 for hook_name in &["pre-commit", "pre-push", "commit-msg"] {
132 let hook_path = hooks_dir.join(hook_name);
133 if hook_path.exists() {
134 let content = fs::read_to_string(&hook_path).await.unwrap_or_default();
135
136 if content.contains("Ferrous Forge") {
137 fs::remove_file(&hook_path)
138 .await
139 .map_err(|e| Error::process(format!("Failed to remove hook: {}", e)))?;
140 println!(" ✅ Removed {} hook", hook_name);
141
142 let backup_path = hooks_dir.join(format!("{}.backup", hook_name));
144 if backup_path.exists() {
145 fs::rename(&backup_path, &hook_path)
146 .await
147 .map_err(|e| Error::process(format!("Failed to restore backup: {}", e)))?;
148 println!(" ✅ Restored original {} hook", hook_name);
149 }
150 }
151 }
152 }
153
154 println!("🎉 Safety hooks removed successfully!");
155 Ok(())
156}
157
158pub fn check_hooks_status(project_path: &Path) -> (bool, bool) {
162 let hooks_dir = project_path.join(".git").join("hooks");
163
164 let pre_commit_installed =
165 if let Ok(content) = std::fs::read_to_string(hooks_dir.join("pre-commit")) {
166 content.contains("Ferrous Forge") && content.contains("🛡️ FERROUS FORGE BLOCKED")
167 } else {
168 false
169 };
170
171 let pre_push_installed =
172 if let Ok(content) = std::fs::read_to_string(hooks_dir.join("pre-push")) {
173 content.contains("Ferrous Forge") && content.contains("🛡️ FERROUS FORGE BLOCKED")
174 } else {
175 false
176 };
177
178 (pre_commit_installed, pre_push_installed)
179}