guardy 0.2.4

Fast, secure git hooks in Rust with secret scanning and protected file synchronization
Documentation
use std::{
    path::PathBuf,
    sync::{Arc, LazyLock, OnceLock},
    thread,
};

use anyhow::Result;

use crate::git::GitRepo;

/// Lazy cached file lists to avoid repeated git operations within a hook execution
/// Similar to lefthook's sync.OnceValue approach - only loads when first accessed
pub struct HookFileCache {
    pub repo: Arc<GitRepo>,
    hook_name: String,
    staged_files: OnceLock<Vec<PathBuf>>,
    all_files: OnceLock<Vec<PathBuf>>,
    push_files: OnceLock<Vec<PathBuf>>,
}

impl HookFileCache {
    /// Create new lazy file cache for a specific hook type
    pub fn new(repo: Arc<GitRepo>, hook_name: &str) -> Self {
        HookFileCache {
            repo,
            hook_name: hook_name.to_string(),
            staged_files: OnceLock::new(),
            all_files: OnceLock::new(),
            push_files: OnceLock::new(),
        }
    }

    /// Start parallel precomputation of git data (similar to lefthook's Precompute)
    /// Returns immediately, computation happens in background
    pub fn precompute(self: &Arc<Self>) {
        // Only precompute for hooks that commonly use these files
        if self.hook_name == "pre-commit" {
            let cache_clone = self.clone();

            // Spawn thread to preload staged files in background
            thread::spawn(move || {
                tracing::trace!("🚀 Starting parallel git precomputation for pre-commit");
                let _ = cache_clone.get_staged_files();
            });
        }
    }

    pub fn get_staged_files(&self) -> &[PathBuf] {
        self.staged_files.get_or_init(|| {
            if self.hook_name == "pre-commit" {
                let start = std::time::Instant::now();
                tracing::trace!("📁 Loading staged files for pre-commit hook");
                let result = self.repo.get_staged_files().unwrap_or_default();
                let elapsed = start.elapsed();

                // Log in microseconds when under 1ms for better precision
                if elapsed.as_millis() == 0 {
                    tracing::trace!(
                        "⚡ Staged files loaded in {}μs ({} files)",
                        elapsed.as_micros(),
                        result.len()
                    );
                } else {
                    tracing::trace!(
                        "⚡ Staged files loaded in {:?} ({} files)",
                        elapsed,
                        result.len()
                    );
                }
                result
            } else {
                Vec::new()
            }
        })
    }

    pub fn get_all_files(&self) -> &[PathBuf] {
        self.all_files.get_or_init(|| {
            if self.hook_name == "pre-commit" || self.hook_name == "pre-push" {
                let start = std::time::Instant::now();
                tracing::trace!("📁 Loading all files for {} hook", self.hook_name);
                let result = self.repo.get_all_files().unwrap_or_default();
                let elapsed = start.elapsed();

                if elapsed.as_millis() == 0 {
                    tracing::trace!(
                        "⚡ All files loaded in {}μs ({} files)",
                        elapsed.as_micros(),
                        result.len()
                    );
                } else {
                    tracing::trace!(
                        "⚡ All files loaded in {:?} ({} files)",
                        elapsed,
                        result.len()
                    );
                }
                result
            } else {
                Vec::new()
            }
        })
    }

    pub fn get_push_files(&self) -> &[PathBuf] {
        self.push_files.get_or_init(|| {
            if self.hook_name == "pre-push" {
                let start = std::time::Instant::now();
                tracing::trace!("📁 Loading push files for pre-push hook");
                let result = self
                    .repo
                    .get_push_files("origin", "main")
                    .unwrap_or_default();
                tracing::trace!(
                    "⚡ Lazy push files loaded in {:?} ({})",
                    start.elapsed(),
                    result.len()
                );
                result
            } else {
                Vec::new()
            }
        })
    }
}

/// Static git repo instance cached for performance using LazyLock
/// Now using gix for microsecond-level performance
static GIT_REPO: LazyLock<Option<Arc<GitRepo>>> = LazyLock::new(|| {
    let start = std::time::Instant::now();
    let result = GitRepo::discover();
    match &result {
        Ok(_) => {
            let elapsed = start.elapsed();
            if elapsed.as_millis() == 0 {
                tracing::trace!(
                    "⚡ Git repo discovery (gix) completed in {}μs",
                    elapsed.as_micros()
                );
            } else {
                tracing::trace!("⚡ Git repo discovery (gix) completed in {:?}", elapsed);
            }
        }
        Err(e) => tracing::debug!("Git repo discovery failed: {}", e),
    }
    result.ok().map(Arc::new)
});

/// Get cached git repository instance
pub fn get_cached_git_repo() -> Result<Arc<GitRepo>> {
    GIT_REPO
        .as_ref()
        .ok_or_else(|| anyhow::anyhow!("Not in a git repository"))
        .map(Arc::clone)
}