securegit 0.8.5

Zero-trust git replacement with 12 built-in security scanners, LLM redteam bridge, universal undo, durable backups, and a 50-tool MCP server
Documentation
use anyhow::Result;
use git2::Repository;
use std::path::Path;

/// Expand pathspecs: "." means all tracked files, directories expand to their contents.
fn expand_pathspecs(repo: &Repository, pathspecs: &[String]) -> Result<Vec<String>> {
    let index = repo.index()?;
    let mut expanded = Vec::new();

    for spec in pathspecs {
        if spec == "." {
            // "." means all tracked files — don't filter by path
            return Ok(Vec::new()); // empty = no path filter (restore everything)
        }

        // Check if spec is a directory prefix (matches multiple index entries)
        let prefix = if spec.ends_with('/') {
            spec.clone()
        } else {
            format!("{}/", spec)
        };

        let mut matched_dir = false;
        for entry in index.iter() {
            let entry_path = String::from_utf8_lossy(&entry.path);
            if entry_path.starts_with(&prefix) {
                expanded.push(entry_path.into_owned());
                matched_dir = true;
            }
        }

        if !matched_dir {
            // Treat as literal file path
            expanded.push(spec.clone());
        }
    }

    Ok(expanded)
}

pub fn execute(
    path: &Path,
    pathspecs: &[String],
    staged: bool,
    source: Option<&str>,
) -> Result<()> {
    let repo = crate::ops::open_repo(path)?;
    let expanded = expand_pathspecs(&repo, pathspecs)?;
    let restore_all = expanded.is_empty(); // "." was passed

    if staged {
        // Restore staged files (unstage) — reset index entries to HEAD (or source)
        let tree = if let Some(src) = source {
            let obj = repo.revparse_single(src)?;
            obj.peel_to_tree()?
        } else {
            repo.head()?.peel_to_tree()?
        };

        let mut index = repo.index()?;

        if restore_all {
            // Reset entire index to tree
            repo.reset(tree.as_object(), git2::ResetType::Mixed, None)?;
            println!("Updated index to match HEAD");
        } else {
            for spec in &expanded {
                let p = Path::new(spec);
                if let Ok(entry) = tree.get_path(p) {
                    let idx_entry = git2::IndexEntry {
                        ctime: git2::IndexTime::new(0, 0),
                        mtime: git2::IndexTime::new(0, 0),
                        dev: 0,
                        ino: 0,
                        mode: entry.filemode() as u32,
                        uid: 0,
                        gid: 0,
                        file_size: 0,
                        id: entry.id(),
                        flags: 0,
                        flags_extended: 0,
                        path: spec.as_bytes().to_vec(),
                    };
                    index.add(&idx_entry)?;
                } else {
                    // File doesn't exist in source — remove from index
                    index.remove_path(p)?;
                }
                println!("Updated '{}' in index", spec);
            }
            index.write()?;
        }
    } else {
        // Restore working tree files from index (default) or from source commit
        let mut checkout = git2::build::CheckoutBuilder::new();
        checkout.force();

        if !restore_all {
            for spec in &expanded {
                checkout.path(spec);
            }
        }

        if let Some(src) = source {
            let obj = repo.revparse_single(src)?;
            let tree = obj.peel_to_tree()?;
            repo.checkout_tree(tree.as_object(), Some(&mut checkout))?;
        } else {
            repo.checkout_index(None, Some(&mut checkout))?;
        }

        if restore_all {
            println!("Updated all tracked files");
        } else {
            for spec in &expanded {
                println!("Updated '{}'", spec);
            }
        }
    }

    Ok(())
}