ferrous_forge/
git_hooks.rs1use crate::{Error, Result};
7use std::path::Path;
8use tokio::fs;
9
10const PRE_COMMIT_HOOK: &str = r#"#!/bin/sh
12# Ferrous Forge pre-commit hook
13# Automatically validates code before allowing commits
14
15set -e
16
17echo "๐จ Running Ferrous Forge pre-commit validation..."
18
19# Check if ferrous-forge is installed
20if ! command -v ferrous-forge >/dev/null 2>&1; then
21 echo "โ ๏ธ Ferrous Forge not found in PATH"
22 echo " Install with: cargo install ferrous-forge"
23 echo " Skipping validation..."
24 exit 0
25fi
26
27# Run formatting check
28echo "๐ Checking code formatting..."
29if ! cargo fmt -- --check >/dev/null 2>&1; then
30 echo "โ Code is not formatted properly"
31 echo " Run 'cargo fmt' to fix formatting"
32 exit 1
33fi
34
35# Run Ferrous Forge validation
36echo "๐ Running standards validation..."
37if ! ferrous-forge validate --quiet; then
38 echo "โ Ferrous Forge validation failed"
39 echo " Run 'ferrous-forge validate' to see detailed errors"
40 echo " Fix all violations before committing"
41 exit 1
42fi
43
44# Run clippy
45echo "๐ Running clippy checks..."
46if ! cargo clippy -- -D warnings 2>/dev/null; then
47 echo "โ Clippy found issues"
48 echo " Run 'cargo clippy' to see warnings"
49 exit 1
50fi
51
52# Check for security vulnerabilities (if cargo-audit is installed)
53if command -v cargo-audit >/dev/null 2>&1; then
54 echo "๐ Checking for security vulnerabilities..."
55 if ! cargo audit --quiet 2>/dev/null; then
56 echo "โ ๏ธ Security vulnerabilities detected"
57 echo " Run 'cargo audit' for details"
58 echo " Consider fixing before committing"
59 # Don't block commit for security issues (just warn)
60 fi
61fi
62
63echo "โ
All pre-commit checks passed!"
64"#;
65
66const PRE_PUSH_HOOK: &str = r#"#!/bin/sh
68# Ferrous Forge pre-push hook
69# Runs comprehensive validation before pushing
70
71set -e
72
73echo "๐จ Running Ferrous Forge pre-push validation..."
74
75# Run all tests
76echo "๐งช Running tests..."
77if ! cargo test --quiet; then
78 echo "โ Tests failed"
79 echo " Fix failing tests before pushing"
80 exit 1
81fi
82
83# Run documentation check
84echo "๐ Checking documentation..."
85if ! cargo doc --no-deps --document-private-items >/dev/null 2>&1; then
86 echo "โ ๏ธ Documentation issues found"
87 echo " Run 'cargo doc' to see warnings"
88fi
89
90# Full validation
91echo "๐ Running full validation..."
92if ! ferrous-forge validate; then
93 echo "โ Validation failed"
94 echo " Fix all issues before pushing"
95 exit 1
96fi
97
98echo "โ
All pre-push checks passed!"
99"#;
100
101const COMMIT_MSG_HOOK: &str = r#"#!/bin/sh
103# Ferrous Forge commit message hook
104# Enforces conventional commit format
105
106COMMIT_MSG_FILE=$1
107COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
108
109# Check for conventional commit format
110if ! echo "$COMMIT_MSG" | grep -qE '^(feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert)(\([a-z0-9-]+\))?: .+'; then
111 echo "โ Invalid commit message format!"
112 echo ""
113 echo "๐ Please use conventional commit format:"
114 echo " <type>(<scope>): <description>"
115 echo ""
116 echo "Types: feat, fix, docs, style, refactor, perf, test, chore, build, ci, revert"
117 echo ""
118 echo "Examples:"
119 echo " feat: add new validation rule"
120 echo " fix(validation): correct line counting logic"
121 echo " docs: update README with new features"
122 echo ""
123 exit 1
124fi
125
126# Check message length
127FIRST_LINE=$(echo "$COMMIT_MSG" | head -n1)
128if [ ${#FIRST_LINE} -gt 72 ]; then
129 echo "โ ๏ธ Commit message first line is too long (${#FIRST_LINE} > 72 characters)"
130 echo " Consider making it more concise"
131fi
132
133echo "โ
Commit message format valid"
134"#;
135
136pub async fn install_git_hooks(project_path: &Path) -> Result<()> {
138 let git_dir = project_path.join(".git");
140 if !git_dir.exists() {
141 return Err(Error::validation("Not a git repository. Run 'git init' first."));
142 }
143
144 let hooks_dir = git_dir.join("hooks");
145
146 fs::create_dir_all(&hooks_dir)
148 .await
149 .map_err(|e| Error::process(format!("Failed to create hooks directory: {}", e)))?;
150
151 println!("๐ Installing git hooks...");
152
153 install_hook(&hooks_dir, "pre-commit", PRE_COMMIT_HOOK).await?;
155 println!(" โ
Installed pre-commit hook");
156
157 install_hook(&hooks_dir, "pre-push", PRE_PUSH_HOOK).await?;
159 println!(" โ
Installed pre-push hook");
160
161 install_hook(&hooks_dir, "commit-msg", COMMIT_MSG_HOOK).await?;
163 println!(" โ
Installed commit-msg hook");
164
165 println!("๐ Git hooks installed successfully!");
166 println!();
167 println!("Hooks will now run automatically:");
168 println!(" โข pre-commit: Validates code before each commit");
169 println!(" โข pre-push: Runs tests and full validation before push");
170 println!(" โข commit-msg: Ensures conventional commit format");
171 println!();
172 println!("To bypass hooks temporarily, use: git commit --no-verify");
173
174 Ok(())
175}
176
177async fn install_hook(hooks_dir: &Path, name: &str, content: &str) -> Result<()> {
179 let hook_path = hooks_dir.join(name);
180
181 if hook_path.exists() {
183 let existing = fs::read_to_string(&hook_path)
184 .await
185 .map_err(|e| Error::process(format!("Failed to read existing hook: {}", e)))?;
186
187 if existing.contains("Ferrous Forge") {
188 return Ok(());
190 }
191
192 let backup_path = hooks_dir.join(format!("{}.backup", name));
194 fs::rename(&hook_path, &backup_path)
195 .await
196 .map_err(|e| Error::process(format!("Failed to backup existing hook: {}", e)))?;
197
198 println!(" โ ๏ธ Backed up existing {} hook to {}", name, backup_path.display());
199 }
200
201 fs::write(&hook_path, content)
203 .await
204 .map_err(|e| Error::process(format!("Failed to write hook: {}", e)))?;
205
206 #[cfg(unix)]
208 {
209 use std::os::unix::fs::PermissionsExt;
210 let mut perms = fs::metadata(&hook_path)
211 .await
212 .map_err(|e| Error::process(format!("Failed to get hook metadata: {}", e)))?
213 .permissions();
214 perms.set_mode(0o755);
215 fs::set_permissions(&hook_path, perms)
216 .await
217 .map_err(|e| Error::process(format!("Failed to set hook permissions: {}", e)))?;
218 }
219
220 Ok(())
221}
222
223pub async fn uninstall_git_hooks(project_path: &Path) -> Result<()> {
225 let git_dir = project_path.join(".git");
226 if !git_dir.exists() {
227 return Ok(()); }
229
230 let hooks_dir = git_dir.join("hooks");
231
232 println!("๐๏ธ Removing git hooks...");
233
234 for hook_name in &["pre-commit", "pre-push", "commit-msg"] {
235 let hook_path = hooks_dir.join(hook_name);
236
237 if hook_path.exists() {
238 let content = fs::read_to_string(&hook_path)
240 .await
241 .unwrap_or_default();
242
243 if content.contains("Ferrous Forge") {
244 fs::remove_file(&hook_path)
245 .await
246 .map_err(|e| Error::process(format!("Failed to remove hook: {}", e)))?;
247 println!(" โ
Removed {} hook", hook_name);
248
249 let backup_path = hooks_dir.join(format!("{}.backup", hook_name));
251 if backup_path.exists() {
252 fs::rename(&backup_path, &hook_path)
253 .await
254 .map_err(|e| Error::process(format!("Failed to restore backup: {}", e)))?;
255 println!(" โ
Restored original {} hook", hook_name);
256 }
257 }
258 }
259 }
260
261 println!("โ
Git hooks removed");
262
263 Ok(())
264}
265
266pub async fn check_hooks_installed(project_path: &Path) -> Result<bool> {
268 let git_dir = project_path.join(".git");
269 if !git_dir.exists() {
270 return Ok(false);
271 }
272
273 let hooks_dir = git_dir.join("hooks");
274 let pre_commit = hooks_dir.join("pre-commit");
275
276 if pre_commit.exists() {
277 let content = fs::read_to_string(&pre_commit)
278 .await
279 .unwrap_or_default();
280 Ok(content.contains("Ferrous Forge"))
281 } else {
282 Ok(false)
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289 use tempfile::TempDir;
290
291 #[tokio::test]
292 async fn test_hooks_require_git_repo() {
293 let temp_dir = TempDir::new().expect("Failed to create temp dir for test");
294 let result = install_git_hooks(temp_dir.path()).await;
295 assert!(result.is_err());
296 assert!(result.expect_err("Should have failed").to_string().contains("Not a git repository"));
297 }
298
299 #[tokio::test]
300 async fn test_check_hooks_not_installed() {
301 let temp_dir = TempDir::new().expect("Failed to create temp dir for test");
302 let installed = check_hooks_installed(temp_dir.path()).await.expect("Check should succeed");
303 assert!(!installed);
304 }
305}