guardy 0.2.4

Fast, secure git hooks in Rust with secret scanning and protected file synchronization
Documentation
use std::{path::PathBuf, process::Command, time::Instant};

use anyhow::Result;
use gix::bstr::ByteSlice;

// Command implementation
fn get_staged_files_command(repo_path: &std::path::Path) -> Result<Vec<PathBuf>> {
    let output = Command::new("git")
        .args(["diff", "--cached", "--name-only"])
        .current_dir(repo_path)
        .output()?;

    if !output.status.success() {
        return Ok(Vec::new());
    }

    let stdout = String::from_utf8(output.stdout)?;
    Ok(stdout
        .lines()
        .filter(|line| !line.trim().is_empty())
        .map(|line| repo_path.join(line.trim()))
        .collect())
}

// libgit2 implementation
fn get_staged_files_libgit2(repo: &git2::Repository) -> Result<Vec<PathBuf>> {
    let mut opts = git2::StatusOptions::new();
    opts.show(git2::StatusShow::Index)
        .include_untracked(false)
        .include_ignored(false);

    let statuses = repo.statuses(Some(&mut opts))?;
    let workdir = repo.workdir().unwrap();

    let mut files = Vec::new();
    for entry in statuses.iter() {
        let status = entry.status();

        if (status.contains(git2::Status::INDEX_NEW)
            || status.contains(git2::Status::INDEX_MODIFIED)
            || status.contains(git2::Status::INDEX_DELETED)
            || status.contains(git2::Status::INDEX_RENAMED)
            || status.contains(git2::Status::INDEX_TYPECHANGE))
            && let Some(path) = entry.path()
        {
            files.push(workdir.join(path));
        }
    }

    Ok(files)
}

// gix implementation - proper staged files detection like production code
fn get_staged_files_gix(repo: &gix::Repository) -> Result<Vec<PathBuf>> {
    let mut staged_files = Vec::new();
    let workdir = repo.workdir().unwrap();

    // Get the current index
    let index = repo.index()?;

    // Get HEAD tree (or None for initial commit)
    let head_tree = match repo.head_tree_id() {
        Ok(tree_id) => Some(repo.find_tree(tree_id)?),
        Err(_) => None, // Initial commit - all index entries are staged
    };

    // If no HEAD tree (initial commit), all files in index are staged
    if head_tree.is_none() {
        for entry in index.entries() {
            let path = entry.path(&index);
            staged_files.push(workdir.join(path.to_path_lossy().as_ref()));
        }
        return Ok(staged_files);
    }

    let tree = head_tree.unwrap();

    // Compare each index entry with the corresponding tree entry
    for entry in index.entries() {
        let path = entry.path(&index);

        // Convert BStr to Path for lookup
        let path_str = path.to_path_lossy();

        // Look up this path in the HEAD tree
        match tree.lookup_entry_by_path(&*path_str) {
            Ok(Some(tree_entry)) => {
                // File exists in both - check if content differs
                if entry.id != tree_entry.object_id() {
                    // Different content = staged change
                    staged_files.push(workdir.join(path.to_path_lossy().as_ref()));
                }
            }
            Ok(None) | Err(_) => {
                // File doesn't exist in HEAD tree = newly added (staged)
                staged_files.push(workdir.join(path.to_path_lossy().as_ref()));
            }
        }
    }

    Ok(staged_files)
}

fn main() {
    println!("\n=== Git Performance Comparison ===\n");

    let repo_path = std::env::current_dir().unwrap();

    // Warm up
    println!("Warming up...");
    for _ in 0..5 {
        let _ = get_staged_files_command(&repo_path);
    }

    // Test Command implementation
    println!("\nTesting external git command:");
    let mut command_times = Vec::new();
    for i in 0..10 {
        let start = Instant::now();
        let files = get_staged_files_command(&repo_path).unwrap();
        let elapsed = start.elapsed();
        command_times.push(elapsed);
        if i == 0 {
            println!("  Files found: {}", files.len());
        }
    }
    let command_avg =
        command_times.iter().sum::<std::time::Duration>() / command_times.len() as u32;
    println!("  Average time: {command_avg:?}");
    if command_avg.as_millis() == 0 {
        println!("  (in microseconds: {}μs)", command_avg.as_micros());
    }

    // Test libgit2
    println!("\nTesting libgit2:");
    let git2_repo = git2::Repository::discover(&repo_path).unwrap();
    let mut libgit2_times = Vec::new();
    for i in 0..10 {
        let start = Instant::now();
        let files = get_staged_files_libgit2(&git2_repo).unwrap();
        let elapsed = start.elapsed();
        libgit2_times.push(elapsed);
        if i == 0 {
            println!("  Files found: {}", files.len());
        }
    }
    let libgit2_avg =
        libgit2_times.iter().sum::<std::time::Duration>() / libgit2_times.len() as u32;
    println!("  Average time: {libgit2_avg:?}");
    if libgit2_avg.as_millis() == 0 {
        println!("  (in microseconds: {}μs)", libgit2_avg.as_micros());
    }

    // Test gix
    println!("\nTesting gix (gitoxide):");
    let gix_repo = gix::discover(&repo_path).unwrap();
    let mut gix_times = Vec::new();
    for i in 0..10 {
        let start = Instant::now();
        let files = get_staged_files_gix(&gix_repo).unwrap();
        let elapsed = start.elapsed();
        gix_times.push(elapsed);
        if i == 0 {
            println!("  Files found: {}", files.len());
        }
    }
    let gix_avg = gix_times.iter().sum::<std::time::Duration>() / gix_times.len() as u32;
    println!("  Average time: {gix_avg:?}");
    if gix_avg.as_millis() == 0 {
        println!("  (in microseconds: {}μs)", gix_avg.as_micros());
    }

    // Summary
    println!("\n=== Performance Summary ===");
    println!("Command:  {command_avg:?}");
    println!("libgit2:  {libgit2_avg:?}");
    println!("gix:      {gix_avg:?}");

    println!("\nSpeedup vs Command:");
    println!(
        "  libgit2: {:.1}x faster",
        command_avg.as_nanos() as f64 / libgit2_avg.as_nanos() as f64
    );
    println!(
        "  gix:     {:.1}x faster",
        command_avg.as_nanos() as f64 / gix_avg.as_nanos() as f64
    );

    println!("\nRecommendation:");
    if libgit2_avg < gix_avg {
        println!("  → Use libgit2 for best performance");
    } else {
        println!("  → Use gix for best performance (and it's pure Rust!)");
    }
}