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<()> {
15 let git_dir = project_path.join(".git");
17 if !git_dir.exists() {
18 return Err(Error::validation(
19 "Not a git repository. Run 'git init' first.".to_string(),
20 ));
21 }
22
23 let hooks_dir = git_dir.join("hooks");
25 if !hooks_dir.exists() {
26 fs::create_dir_all(&hooks_dir)
27 .await
28 .map_err(|e| Error::process(format!("Failed to create hooks directory: {}", e)))?;
29 }
30
31 println!("📎 Installing git hooks...");
32
33 install_hook(&hooks_dir, "pre-commit", PRE_COMMIT_HOOK).await?;
35 println!(" ✅ Installed pre-commit hook");
36
37 install_hook(&hooks_dir, "pre-push", PRE_PUSH_HOOK).await?;
39 println!(" ✅ Installed pre-push hook");
40
41 install_hook(&hooks_dir, "commit-msg", COMMIT_MSG_HOOK).await?;
43 println!(" ✅ Installed commit-msg hook");
44
45 println!("🎉 Git hooks installed successfully!");
46 println!();
47 println!("Hooks will now run automatically:");
48 println!(" • pre-commit: Validates code before each commit");
49 println!(" • pre-push: Runs tests and full validation before push");
50 println!(" • commit-msg: Ensures conventional commit format");
51 println!();
52 println!("To bypass hooks temporarily, use: git commit --no-verify");
53
54 Ok(())
55}
56
57async fn install_hook(hooks_dir: &Path, name: &str, content: &str) -> Result<()> {
59 let hook_path = hooks_dir.join(name);
60
61 if hook_path.exists() {
63 let existing = fs::read_to_string(&hook_path)
64 .await
65 .map_err(|e| Error::process(format!("Failed to read existing hook: {}", e)))?;
66
67 if existing.contains("Ferrous Forge") {
68 return Ok(());
70 }
71
72 let backup_path = hooks_dir.join(format!("{}.backup", name));
74 fs::rename(&hook_path, &backup_path)
75 .await
76 .map_err(|e| Error::process(format!("Failed to backup existing hook: {}", e)))?;
77
78 println!(
79 " ⚠️ Backed up existing {} hook to {}",
80 name,
81 backup_path.display()
82 );
83 }
84
85 fs::write(&hook_path, content)
87 .await
88 .map_err(|e| Error::process(format!("Failed to write hook: {}", e)))?;
89
90 #[cfg(unix)]
92 {
93 use std::os::unix::fs::PermissionsExt;
94 let mut perms = fs::metadata(&hook_path)
95 .await
96 .map_err(|e| Error::process(format!("Failed to get hook metadata: {}", e)))?
97 .permissions();
98 perms.set_mode(0o755);
99 fs::set_permissions(&hook_path, perms)
100 .await
101 .map_err(|e| Error::process(format!("Failed to set hook permissions: {}", e)))?;
102 }
103
104 Ok(())
105}
106
107pub async fn uninstall_git_hooks(project_path: &Path) -> Result<()> {
113 let git_dir = project_path.join(".git");
114 if !git_dir.exists() {
115 return Ok(()); }
117
118 let hooks_dir = git_dir.join("hooks");
119
120 println!("🗑️ Removing git hooks...");
121
122 for hook_name in &["pre-commit", "pre-push", "commit-msg"] {
124 let hook_path = hooks_dir.join(hook_name);
125 if hook_path.exists() {
126 let content = fs::read_to_string(&hook_path).await.unwrap_or_default();
127
128 if content.contains("Ferrous Forge") {
129 fs::remove_file(&hook_path)
130 .await
131 .map_err(|e| Error::process(format!("Failed to remove hook: {}", e)))?;
132 println!(" ✅ Removed {} hook", hook_name);
133
134 let backup_path = hooks_dir.join(format!("{}.backup", hook_name));
136 if backup_path.exists() {
137 fs::rename(&backup_path, &hook_path)
138 .await
139 .map_err(|e| Error::process(format!("Failed to restore backup: {}", e)))?;
140 println!(" ✅ Restored original {} hook", hook_name);
141 }
142 }
143 }
144 }
145
146 println!("🎉 Git hooks removed successfully!");
147 Ok(())
148}