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::{Context, Result};
use sha1::{Digest, Sha1};
use std::path::Path;
use walkdir::WalkDir;

/// Verify that the contents of an extracted ZIP match the git HEAD tree.
///
/// Computes a deterministic hash over all files in `zip_path` and compares
/// it against the tree hash at HEAD in the git repository at `git_dir`.
pub fn verify_tree_integrity(zip_path: &Path, git_dir: &Path) -> Result<bool> {
    let repo = git2::Repository::open(git_dir).context("Failed to open git repository")?;
    let head = repo.head().context("Failed to get HEAD reference")?;
    let commit = head
        .peel_to_commit()
        .context("Failed to peel HEAD to commit")?;
    let tree = commit
        .tree()
        .context("Failed to get tree from HEAD commit")?;

    // Collect all files from the ZIP extraction, sorted for determinism
    let mut zip_files: Vec<(String, Vec<u8>)> = Vec::new();
    for entry in WalkDir::new(zip_path)
        .follow_links(false)
        .into_iter()
        .filter_map(|e| e.ok())
    {
        if !entry.file_type().is_file() {
            continue;
        }
        let rel = entry.path().strip_prefix(zip_path).unwrap_or(entry.path());
        let rel_str = rel.to_string_lossy().replace('\\', "/");
        if let Ok(content) = std::fs::read(entry.path()) {
            zip_files.push((rel_str, content));
        }
    }
    zip_files.sort_by(|a, b| a.0.cmp(&b.0));

    // Compute a digest over the zip files
    let mut zip_hasher = Sha1::new();
    for (path, content) in &zip_files {
        zip_hasher.update(path.as_bytes());
        zip_hasher.update((content.len() as u64).to_le_bytes());
        zip_hasher.update(content);
    }
    let zip_digest = zip_hasher.finalize();

    // Collect all files from the git tree, sorted
    let mut git_files: Vec<(String, Vec<u8>)> = Vec::new();
    tree.walk(git2::TreeWalkMode::PreOrder, |dir, entry| {
        if let Some(git2::ObjectType::Blob) = entry.kind() {
            let path = if dir.is_empty() {
                entry.name().unwrap_or("").to_string()
            } else {
                format!("{}{}", dir, entry.name().unwrap_or(""))
            };
            if let Ok(blob) = repo.find_blob(entry.id()) {
                git_files.push((path, blob.content().to_vec()));
            }
        }
        git2::TreeWalkResult::Ok
    })?;
    git_files.sort_by(|a, b| a.0.cmp(&b.0));

    // Compute the same digest over git files
    let mut git_hasher = Sha1::new();
    for (path, content) in &git_files {
        git_hasher.update(path.as_bytes());
        git_hasher.update((content.len() as u64).to_le_bytes());
        git_hasher.update(content);
    }
    let git_digest = git_hasher.finalize();

    Ok(zip_digest == git_digest)
}