guardy 0.2.4

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

use anyhow::Result;

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

/// Remove guardy hooks from the current repository
pub async fn uninstall_hooks(hooks: Option<Vec<String>>, force: bool) -> Result<()> {
    uninstall_hooks_at(hooks, force, None).await
}

/// Remove guardy hooks from a specific repository path (for testing)
pub async fn uninstall_hooks_at(
    hooks: Option<Vec<String>>,
    force: bool,
    repo_path: Option<&std::path::Path>,
) -> Result<()> {
    // Print banner without context
    banner::print_banner(None);

    // 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");
                return Ok(());
            }
        },
    };

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

    // Determine which hooks to check
    let hook_names: Vec<String> = hooks.unwrap_or_else(|| {
        vec![
            "pre-commit".to_string(),
            "commit-msg".to_string(),
            "post-checkout".to_string(),
            "pre-push".to_string(),
        ]
    });

    // Find guardy hooks
    let mut guardy_hooks = Vec::new();
    for hook_name in &hook_names {
        let hook_path = hooks_dir.join(hook_name);
        if hook_path.exists()
            && let Ok(content) = fs::read_to_string(&hook_path)
            && content.contains("guardy hooks run")
        {
            guardy_hooks.push((hook_name.clone(), hook_path));
        }
    }

    if guardy_hooks.is_empty() {
        info!("No guardy hooks found to remove");
        return Ok(());
    }

    if !force {
        warning!(&format!(
            "This will remove {} guardy hooks:",
            guardy_hooks.len()
        ));
        for (hook_name, _) in &guardy_hooks {
            println!("  - {hook_name}");
        }

        // Prompt for confirmation
        print!("Are you sure you want to continue? [y/N]: ");
        std::io::Write::flush(&mut std::io::stdout())?;

        let mut input = String::new();
        std::io::stdin().read_line(&mut input)?;
        let input = input.trim().to_lowercase();

        if input != "y" && input != "yes" {
            info!("Uninstall cancelled");
            return Ok(());
        }
    }

    info!("Removing guardy hooks...");

    let mut removed_count = 0;
    for (hook_name, hook_path) in guardy_hooks {
        match fs::remove_file(&hook_path) {
            Ok(_) => {
                success!(&format!("Removed '{hook_name}' hook"));
                removed_count += 1;
            }
            Err(e) => {
                error!(&format!("Failed to remove '{hook_name}' hook: {e}"));
            }
        }
    }

    if removed_count > 0 {
        success!(&format!(
            "Successfully removed {removed_count} guardy hooks"
        ));
    } else {
        warning!("No hooks were removed");
    }

    Ok(())
}