weavatrix-git 0.3.1

Fast, bounded, evidence-carrying Git reader with an optional read-only MCP server
Documentation
use std::{
    fs,
    path::{Path, PathBuf},
    process::Command,
    sync::atomic::{AtomicU64, Ordering},
    time::{SystemTime, UNIX_EPOCH},
};

use weavatrix_git::{GitError, HistoryOptions, Limits, Repository};

struct Layouts {
    root: PathBuf,
    source: PathBuf,
    bare: PathBuf,
    linked: PathBuf,
}

static FIXTURE_SEQUENCE: AtomicU64 = AtomicU64::new(0);

impl Layouts {
    fn new() -> Self {
        let unique = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let root = std::env::temp_dir().join(format!(
            "weavatrix-layout-{}-{unique}-{}",
            std::process::id(),
            FIXTURE_SEQUENCE.fetch_add(1, Ordering::Relaxed)
        ));
        let source = root.join("source");
        let bare = root.join("bare.git");
        let linked = root.join("linked");
        fs::create_dir_all(&source).unwrap();
        git(&source, &["init", "-q"]);
        git(&source, &["config", "user.name", "Weavatrix Test"]);
        git(&source, &["config", "user.email", "test@weavatrix.local"]);
        fs::write(source.join("evidence.txt"), "evidence").unwrap();
        git(&source, &["add", "evidence.txt"]);
        git(&source, &["commit", "-q", "-m", "initial"]);
        git(
            &root,
            &[
                "clone",
                "-q",
                "--bare",
                path_text(&source),
                path_text(&bare),
            ],
        );
        git(
            &source,
            &["worktree", "add", "-q", "-b", "linked", path_text(&linked)],
        );
        Self {
            root,
            source,
            bare,
            linked,
        }
    }
}

impl Drop for Layouts {
    fn drop(&mut self) {
        fs::remove_dir_all(&self.root).ok();
    }
}

#[test]
fn discovers_bare_and_linked_worktree_layouts() {
    let fixture = Layouts::new();
    let source = Repository::open(&fixture.source).unwrap();
    let expected = source.resolve("HEAD").unwrap();

    let bare = Repository::open(&fixture.bare).unwrap();
    assert!(bare.work_dir().is_none());
    assert_eq!(bare.resolve("HEAD").unwrap(), expected);
    assert_eq!(bare.commit(expected).unwrap().summary_lossy(), "initial");
    assert!(bare.status().is_err());

    let linked = Repository::open(&fixture.linked).unwrap();
    let linked_path = fixture.linked.canonicalize().unwrap();
    assert_eq!(linked.work_dir(), Some(linked_path.as_path()));
    assert_ne!(linked.git_dir(), linked.common_dir());
    assert_eq!(linked.resolve("HEAD").unwrap(), expected);

    git(&fixture.source, &["checkout", "-q", "--detach"]);
    let detached = Repository::open(&fixture.source).unwrap().head().unwrap();
    assert!(detached.symbolic.is_none());
    assert_eq!(detached.target, Some(expected));
    assert!(Repository::open(&fixture.root).is_err());
}

#[test]
fn enforces_configured_object_limit() {
    let fixture = Layouts::new();
    let repository = Repository::open_with_limits(
        &fixture.source,
        Limits {
            max_object_bytes: 8,
            ..Limits::default()
        },
    )
    .unwrap();
    let error = repository
        .commit(repository.resolve("HEAD").unwrap())
        .unwrap_err();
    assert!(matches!(error, GitError::LimitExceeded { .. }));

    let repository = Repository::open_with_limits(
        &fixture.source,
        Limits {
            max_history_commits: 1,
            ..Limits::default()
        },
    )
    .unwrap();
    let error = repository
        .history(
            repository.resolve("HEAD").unwrap(),
            HistoryOptions {
                max_commits: 2,
                ..HistoryOptions::default()
            },
        )
        .unwrap_err();
    assert!(matches!(error, GitError::LimitExceeded { .. }));
}

fn git(path: &Path, args: &[&str]) {
    let output = Command::new("git")
        .args(args)
        .current_dir(path)
        .output()
        .unwrap();
    assert!(
        output.status.success(),
        "git {args:?}: {}",
        String::from_utf8_lossy(&output.stderr)
    );
}

fn path_text(path: &Path) -> &str {
    path.to_str().expect("test path is Unicode")
}