use super::scripts::{COMMIT_MSG_HOOK, PRE_COMMIT_HOOK, PRE_PUSH_HOOK};
use crate::{Error, Result};
use std::path::Path;
use tokio::fs;
pub async fn install_git_hooks(project_path: &Path) -> Result<()> {
let git_dir = project_path.join(".git");
if !git_dir.exists() {
return Err(Error::validation(
"Not a git repository. Run 'git init' first.".to_string(),
));
}
let hooks_dir = git_dir.join("hooks");
if !hooks_dir.exists() {
fs::create_dir_all(&hooks_dir)
.await
.map_err(|e| Error::process(format!("Failed to create hooks directory: {}", e)))?;
}
println!("🔒 Installing mandatory safety hooks...");
install_hook(&hooks_dir, "pre-commit", PRE_COMMIT_HOOK).await?;
println!(" ✅ Installed blocking pre-commit hook");
install_hook(&hooks_dir, "pre-push", PRE_PUSH_HOOK).await?;
println!(" ✅ Installed blocking pre-push hook");
install_hook(&hooks_dir, "commit-msg", COMMIT_MSG_HOOK).await?;
println!(" ✅ Installed commit-msg hook");
println!();
println!("🛡️ Mandatory safety hooks installed!");
println!();
println!("These hooks will BLOCK commits/pushes that fail checks.");
println!("To bypass temporarily: ferrous-forge safety bypass --stage=...");
Ok(())
}
async fn install_hook(hooks_dir: &Path, name: &str, content: &str) -> Result<()> {
let hook_path = hooks_dir.join(name);
if hook_path.exists() {
let existing = fs::read_to_string(&hook_path)
.await
.map_err(|e| Error::process(format!("Failed to read existing hook: {}", e)))?;
if existing.contains("Ferrous Forge") {
if existing.contains("🛡️ FERROUS FORGE BLOCKED") {
return Ok(());
}
println!(" 🔄 Upgrading {} hook to blocking version", name);
} else {
let backup_path = hooks_dir.join(format!("{}.backup", name));
fs::rename(&hook_path, &backup_path)
.await
.map_err(|e| Error::process(format!("Failed to backup existing hook: {}", e)))?;
println!(
" ⚠️ Backed up existing {} hook to {}",
name,
backup_path.display()
);
}
}
fs::write(&hook_path, content)
.await
.map_err(|e| Error::process(format!("Failed to write hook: {}", e)))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&hook_path)
.await
.map_err(|e| Error::process(format!("Failed to get hook metadata: {}", e)))?
.permissions();
perms.set_mode(0o755);
fs::set_permissions(&hook_path, perms)
.await
.map_err(|e| Error::process(format!("Failed to set hook permissions: {}", e)))?;
}
Ok(())
}
pub async fn uninstall_git_hooks(project_path: &Path) -> Result<()> {
let git_dir = project_path.join(".git");
if !git_dir.exists() {
return Ok(()); }
let hooks_dir = git_dir.join("hooks");
println!("🗑️ Removing safety hooks...");
for hook_name in &["pre-commit", "pre-push", "commit-msg"] {
let hook_path = hooks_dir.join(hook_name);
if hook_path.exists() {
let content = fs::read_to_string(&hook_path).await.unwrap_or_default();
if content.contains("Ferrous Forge") {
fs::remove_file(&hook_path)
.await
.map_err(|e| Error::process(format!("Failed to remove hook: {}", e)))?;
println!(" ✅ Removed {} hook", hook_name);
let backup_path = hooks_dir.join(format!("{}.backup", hook_name));
if backup_path.exists() {
fs::rename(&backup_path, &hook_path)
.await
.map_err(|e| Error::process(format!("Failed to restore backup: {}", e)))?;
println!(" ✅ Restored original {} hook", hook_name);
}
}
}
}
println!("🎉 Safety hooks removed successfully!");
Ok(())
}
pub fn check_hooks_status(project_path: &Path) -> (bool, bool) {
let hooks_dir = project_path.join(".git").join("hooks");
let pre_commit_installed =
if let Ok(content) = std::fs::read_to_string(hooks_dir.join("pre-commit")) {
content.contains("Ferrous Forge") && content.contains("🛡️ FERROUS FORGE BLOCKED")
} else {
false
};
let pre_push_installed =
if let Ok(content) = std::fs::read_to_string(hooks_dir.join("pre-push")) {
content.contains("Ferrous Forge") && content.contains("🛡️ FERROUS FORGE BLOCKED")
} else {
false
};
(pre_commit_installed, pre_push_installed)
}