weavatrix-git 0.3.1

Fast, bounded, evidence-carrying Git reader with an optional read-only MCP server
Documentation
use std::{
    collections::BTreeSet,
    env,
    path::Path,
    process::Command,
    time::{Duration, Instant},
};

use weavatrix_git::Repository;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = env::args().skip(1).collect::<Vec<_>>();
    let path = args.first().map_or(".", String::as_str);
    let iterations = args
        .get(1)
        .map(|value| value.parse())
        .transpose()?
        .unwrap_or(50);
    let repository = Repository::open(path)?;
    let head = repository.resolve("HEAD")?;

    let expected_objects = git_object_ids(path)?;
    let actual_objects = repository
        .bitmap_reachable(head)?
        .ok_or("repository has no reachability bitmap")?
        .into_iter()
        .map(|id| id.to_string())
        .collect::<BTreeSet<_>>();
    if actual_objects != expected_objects {
        return Err("bitmap reachability differs from git rev-list --objects".into());
    }
    let expected_paths = git_paths(path)?;
    let actual_paths = repository
        .index()?
        .entries()
        .iter()
        .map(|entry| entry.path.clone())
        .collect::<Vec<_>>();
    if actual_paths != expected_paths {
        return Err("index paths differ from git ls-files".into());
    }
    let expected_status = git_status(path)?;
    let actual_status = repository.status()?;
    if !expected_status.is_empty() || !actual_status.is_empty() {
        return Err(format!(
            "status benchmark requires a clean tracked worktree (git={} bytes, weavatrix={actual_status:?})",
            expected_status.len()
        )
        .into());
    }

    println!("operation,engine,p50_ms,p95_ms,items");
    row(
        "reachability",
        "weavatrix-git",
        &measure(iterations, || {
            repository
                .bitmap_reachable(head)
                .map(|value| value.map_or(0, |ids| ids.len()))
        })?,
        actual_objects.len(),
    );
    row(
        "reachability",
        "git.exe",
        &measure(iterations, || git_object_ids(path).map(|ids| ids.len()))?,
        expected_objects.len(),
    );
    row(
        "index",
        "weavatrix-git",
        &measure(iterations, || {
            repository.index().map(|index| index.entries().len())
        })?,
        actual_paths.len(),
    );
    row(
        "index",
        "git.exe",
        &measure(iterations, || git_paths(path).map(|paths| paths.len()))?,
        expected_paths.len(),
    );
    row(
        "tracked-status",
        "weavatrix-git",
        &measure(iterations, || {
            repository.status().map(|entries| entries.len())
        })?,
        0,
    );
    row(
        "tracked-status",
        "git.exe",
        &measure(iterations, || git_status(path).map(|entries| entries.len()))?,
        0,
    );
    row(
        "cached-commit",
        "weavatrix-git",
        &measure(iterations, || repository.commit(head).map(|_| 1))?,
        1,
    );
    row(
        "cached-commit",
        "git.exe",
        &measure(iterations, || git_exists(path, &head.to_string()))?,
        1,
    );
    Ok(())
}

fn git_object_ids(path: impl AsRef<Path>) -> Result<BTreeSet<String>, Box<dyn std::error::Error>> {
    Ok(git(path, &["rev-list", "--objects", "HEAD"])?
        .lines()
        .filter_map(|line| line.split_ascii_whitespace().next())
        .map(str::to_owned)
        .collect())
}

fn git_paths(path: impl AsRef<Path>) -> Result<Vec<Vec<u8>>, Box<dyn std::error::Error>> {
    Ok(git_bytes(path, &["ls-files", "-z"])?
        .split(|byte| *byte == 0)
        .filter(|path| !path.is_empty())
        .map(<[u8]>::to_vec)
        .collect())
}

fn git_status(path: impl AsRef<Path>) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
    git_bytes(path, &["status", "--porcelain=v1", "-uno"])
}

fn git_exists(path: impl AsRef<Path>, id: &str) -> Result<usize, Box<dyn std::error::Error>> {
    let _ = git_bytes(path, &["cat-file", "-e", id])?;
    Ok(1)
}

fn git(path: impl AsRef<Path>, args: &[&str]) -> Result<String, Box<dyn std::error::Error>> {
    Ok(String::from_utf8(git_bytes(path, args)?)?)
}

fn git_bytes(path: impl AsRef<Path>, args: &[&str]) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
    let output = Command::new("git")
        .arg("-C")
        .arg(path.as_ref())
        .args(args)
        .output()?;
    if !output.status.success() {
        return Err(String::from_utf8_lossy(&output.stderr).into_owned().into());
    }
    Ok(output.stdout)
}

fn measure<T, E>(
    iterations: usize,
    mut operation: impl FnMut() -> Result<T, E>,
) -> Result<Vec<Duration>, E> {
    for _ in 0..3 {
        let _ = operation()?;
    }
    let mut values = Vec::with_capacity(iterations);
    for _ in 0..iterations {
        let start = Instant::now();
        let _ = operation()?;
        values.push(start.elapsed());
    }
    values.sort_unstable();
    Ok(values)
}

fn row(operation: &str, engine: &str, values: &[Duration], items: usize) {
    let p50 = values[values.len() / 2].as_secs_f64() * 1_000.0;
    let p95 = values[(values.len() * 95 / 100).min(values.len() - 1)].as_secs_f64() * 1_000.0;
    println!("{operation},{engine},{p50:.3},{p95:.3},{items}");
}