ferrous_forge/
git_hooks.rs

1//! Git hooks installation and management
2//!
3//! This module provides functionality to install and manage git hooks
4//! for automatic validation on commits.
5
6use crate::{Error, Result};
7use std::path::Path;
8use tokio::fs;
9
10/// Pre-commit hook script content
11const 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
66/// Pre-push hook script content
67const 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
101/// Commit message hook script content
102const 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
136/// Install git hooks in a project
137pub async fn install_git_hooks(project_path: &Path) -> Result<()> {
138    // Check if it's a git repository
139    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    // Create hooks directory if it doesn't exist
147    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 pre-commit hook
154    install_hook(&hooks_dir, "pre-commit", PRE_COMMIT_HOOK).await?;
155    println!("  โœ… Installed pre-commit hook");
156    
157    // Install pre-push hook
158    install_hook(&hooks_dir, "pre-push", PRE_PUSH_HOOK).await?;
159    println!("  โœ… Installed pre-push hook");
160    
161    // Install commit-msg hook
162    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
177/// Install a single hook
178async fn install_hook(hooks_dir: &Path, name: &str, content: &str) -> Result<()> {
179    let hook_path = hooks_dir.join(name);
180    
181    // Check if hook already exists
182    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            // Our hook is already installed
189            return Ok(());
190        }
191        
192        // Backup existing hook
193        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    // Write hook content
202    fs::write(&hook_path, content)
203        .await
204        .map_err(|e| Error::process(format!("Failed to write hook: {}", e)))?;
205    
206    // Make executable on Unix
207    #[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
223/// Remove git hooks from a project
224pub 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(()); // No git repo, nothing to uninstall
228    }
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            // Check if it's our hook
239            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                // Restore backup if it exists
250                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
266/// Check if git hooks are installed
267pub 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}