guardy 0.2.4

Fast, secure git hooks in Rust with secret scanning and protected file synchronization
Documentation
use std::{fs, os::unix::fs::PermissionsExt};

use anyhow::Result;
use supercli::starbase_styles::color::owo::OwoColorize;

use crate::{cli::banner, cli::output::*, git::GitRepo};

/// Detect what hook system is installed based on hook content
fn detect_hook_type(content: &str) -> &'static str {
    let content_lower = content.to_lowercase();

    if content_lower.contains("lefthook") {
        "lefthook"
    } else if content_lower.contains("husky") {
        "husky"
    } else if content_lower.contains("pre-commit") && content_lower.contains("framework") {
        "pre-commit framework"
    } else if content_lower.contains("lint-staged") {
        "lint-staged"
    } else {
        "unknown"
    }
}

/// Install git hooks into the current repository
pub async fn install_hooks(force: bool, hooks: Option<Vec<String>>) -> Result<()> {
    install_hooks_at(force, hooks, None).await
}

/// Install git hooks into a specific repository path (for testing)
pub async fn install_hooks_at(
    force: bool,
    hooks: Option<Vec<String>>,
    repo_path: Option<&std::path::Path>,
) -> Result<()> {
    // Print banner without context
    banner::print_banner(None);
    info!("Installing guardy hooks...");

    // Check if we're in a git repository
    let repo = match repo_path {
        Some(path) => match GitRepo::open(path) {
            Ok(repo) => repo,
            Err(_) => {
                error!(&format!("Not a git repository: {}", path.display()));
                return Ok(());
            }
        },
        None => match GitRepo::discover() {
            Ok(repo) => repo,
            Err(_) => {
                error!("Not in a git repository. Run 'git init' first.");
                return Ok(());
            }
        },
    };

    let hooks_dir = repo.git_dir().join("hooks");

    // Validate .git/hooks directory exists
    if !hooks_dir.exists() {
        fs::create_dir_all(&hooks_dir)?;
        info!("Created .git/hooks directory");
    }

    if force {
        warning!("Force mode enabled - will overwrite existing hooks");
    }

    // Determine which hooks to install
    let hooks_to_install = hooks.unwrap_or_else(|| {
        vec![
            "pre-commit".to_string(),
            "commit-msg".to_string(),
            "post-checkout".to_string(),
            "pre-push".to_string(),
        ]
    });

    // Install each hook
    let mut installed_count = 0;
    let mut already_installed_count = 0;
    let total_hooks = hooks_to_install.len();

    for hook_name in hooks_to_install {
        let hook_path = hooks_dir.join(&hook_name);

        // Check if hook exists and handle based on force flag
        if hook_path.exists() && !force {
            // Read existing hook to check if it's already a guardy hook
            if let Ok(existing_content) = fs::read_to_string(&hook_path) {
                if existing_content.contains("# Guardy hook:") {
                    // Already a guardy hook
                    success!(&format!(
                        "{} {} {}",
                        "Hook".green(),
                        hook_name.yellow(),
                        "already installed".green()
                    ));
                    already_installed_count += 1;
                    continue;
                } else {
                    // Different hook system - detect what it is
                    let hook_type = detect_hook_type(&existing_content);
                    warning!(&format!(
                        "Hook '{hook_name}' already exists ({hook_type}). Use --force to overwrite."
                    ));
                    continue;
                }
            } else {
                warning!(&format!(
                    "Hook '{hook_name}' already exists. Use --force to overwrite."
                ));
                continue;
            }
        }

        // Create hook script that calls guardy
        // Include "$@" to pass all arguments from git to guardy (e.g., commit message file path)
        let hook_script = format!(
            "#!/bin/sh\n# Guardy hook: {hook_name}\nexec guardy hooks run {hook_name} \"$@\"\n"
        );

        fs::write(&hook_path, hook_script)?;

        // Make hook executable
        let mut permissions = fs::metadata(&hook_path)?.permissions();
        permissions.set_mode(0o755);
        fs::set_permissions(&hook_path, permissions)?;

        success!(&format!(
            "{} {} {}",
            "Installed".green(),
            hook_name.yellow(),
            "hook".green()
        ));
        installed_count += 1;
    }

    if installed_count > 0 {
        success!("Hook installation completed!");
    } else if already_installed_count > 0 && already_installed_count == total_hooks {
        success!(&format!(
            "{} {}",
            "All hooks".yellow(),
            "already installed!".green()
        ));
    }

    // Show next steps
    info!("Next steps:");
    println!("  - {} - Verify installation", "guardy status".bold());
    println!(
        "  - {} - Test hooks manually",
        "guardy hooks run pre-commit".bold()
    );
    println!("  - Configure patterns in .guardy.yml if needed");

    Ok(())
}