guardy 0.2.4

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

use anyhow::{Context, Result};
use gix::bstr::ByteSlice;

use super::GitRepo;

impl GitRepo {
    /// Get list of files that are staged for commit (primary use case for pre-commit hooks)
    /// Uses pure gix for maximum performance - compares index with HEAD directly
    pub fn get_staged_files(&self) -> Result<Vec<PathBuf>> {
        let repo = self.gix_repo();
        let mut staged_files = Vec::new();

        // Get the current index
        let index = repo.index().context("Failed to get repository 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)
                    .context("Failed to find HEAD tree")?,
            ),
            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(self.path.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(self.path.join(path.to_path_lossy().as_ref()));
                    }
                }
                Ok(None) | Err(_) => {
                    // File doesn't exist in HEAD tree = newly added (staged)
                    staged_files.push(self.path.join(path.to_path_lossy().as_ref()));
                }
            }
        }

        Ok(staged_files)
    }

    /// Get list of modified files (not staged)
    /// Uses gix for microsecond-level performance
    pub fn get_modified_files(&self) -> Result<Vec<PathBuf>> {
        // This would need proper status implementation
        // For now, returning empty as we focus on staged files
        Ok(Vec::new())
    }

    /// Get current branch name - already implemented in mod.rs
    pub fn get_current_branch(&self) -> Result<String> {
        self.current_branch()
    }

    /// Get list of files to be pushed (for pre-push hook)
    pub fn get_push_files(&self, remote: &str, branch: &str) -> Result<Vec<PathBuf>> {
        let repo = self.gix_repo();
        // Get remote ref
        let remote_ref = format!("refs/remotes/{remote}/{branch}");

        // Check if remote branch exists
        let remote_exists = repo.find_reference(&remote_ref).is_ok();

        if remote_exists {
            // TODO: Implement proper diff between commits
            // For now, return empty
            Ok(Vec::new())
        } else {
            // Remote branch doesn't exist yet - all files would be pushed
            self.get_all_files()
        }
    }

    /// Get all tracked files in the repository
    /// Uses gix index for microsecond-level performance (100-500μs vs 3-5ms)
    pub fn get_all_files(&self) -> Result<Vec<PathBuf>> {
        let repo = self.gix_repo();
        let index = repo.index().context("Failed to get repository index")?;

        let mut files = Vec::with_capacity(index.entries().len());
        for entry in index.entries() {
            let path = entry.path(&index);
            // path is already a BStr which can be converted to Path
            files.push(self.path.join(path.to_path_lossy().as_ref()));
        }

        Ok(files)
    }

    /// Get repository status - returns list of files with uncommitted changes
    /// Includes staged files, modified files, and untracked files
    /// Uses gix for pure Rust implementation
    pub fn get_status(&self) -> Result<Vec<String>> {
        let repo = self.gix_repo();
        let mut status_files = Vec::new();

        // Use gix's high-level status API with empty patterns (all files)
        let status_platform = repo
            .status(gix::progress::Discard)
            .context("Failed to create status platform")?;

        // Create iterator for index-worktree changes
        let iter = status_platform
            .into_index_worktree_iter(Vec::new())
            .context("Failed to create status iterator")?;

        // Iterate through status items
        for item in iter {
            let item = item.context("Failed to read status item")?;

            // Format status output similar to git status --porcelain
            if let Some(summary) = item.summary() {
                use gix::status::index_worktree::iter::Summary;
                let path = item.rela_path().to_str_lossy();

                let status_code = match summary {
                    Summary::Modified => " M",
                    Summary::Removed => " D",
                    Summary::Added => "??",
                    Summary::TypeChange => " T",
                    Summary::Renamed => "R ",
                    Summary::Copied => "C ",
                    Summary::IntentToAdd => "A ",
                    Summary::Conflict => "UU",
                };

                status_files.push(format!("{} {}", status_code, path));
            }
        }

        Ok(status_files)
    }
}