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::{ChangeKind, HistoryOptions, RepositorySet};

struct Fixture {
    root: PathBuf,
    left: PathBuf,
    right: PathBuf,
    shared: String,
}

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

impl Fixture {
    fn new() -> Self {
        let unique = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let root = std::env::temp_dir().join(format!(
            "weavatrix-set-{}-{unique}-{}",
            std::process::id(),
            FIXTURE_SEQUENCE.fetch_add(1, Ordering::Relaxed)
        ));
        let left = root.join("left");
        let right = root.join("right");
        fs::create_dir_all(&left).unwrap();
        git(&left, &["init", "-q"]);
        configure(&left);
        fs::write(left.join("shared.txt"), "shared").unwrap();
        commit(&left, "shared");
        let shared = git_output(&left, &["rev-parse", "HEAD"]).trim().to_owned();
        git(&root, &["clone", "-q", path_text(&left), path_text(&right)]);
        configure(&right);

        fs::write(left.join("left.txt"), "left").unwrap();
        commit(&left, "left");
        fs::write(right.join("right.txt"), "right").unwrap();
        commit(&right, "right");
        for path in [&left, &right] {
            git(path, &["commit-graph", "write", "--reachable"]);
        }
        Self {
            root,
            left,
            right,
            shared,
        }
    }
}

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

#[test]
fn correlates_histories_objects_and_diffs_across_repositories() {
    let fixture = Fixture::new();
    let set = RepositorySet::open([
        ("left", fixture.left.as_path()),
        ("right", fixture.right.as_path()),
    ])
    .unwrap();
    assert_eq!(set.len(), 2);
    assert!(!set.is_empty());
    let left = set.id("left").unwrap();
    let right = set.id("right").unwrap();
    assert_eq!(set.name(left), Some("left"));
    assert!(set.repository(left).is_some());

    let options = HistoryOptions {
        max_commits: 10,
        ..HistoryOptions::default()
    };
    let serial = set.histories(options).unwrap();
    let parallel = set.histories_parallel(options).unwrap();
    assert_eq!(serial[0].commits, parallel[0].commits);
    assert_eq!(serial[1].commits, parallel[1].commits);
    assert_eq!(set.resolve(left, "HEAD").unwrap(), serial[0].head);

    let shared = fixture.shared.parse().unwrap();
    assert_eq!(set.find_object(shared), vec![left, right]);
    let common = set.shared_commits(options).unwrap();
    assert!(
        common
            .iter()
            .any(|commit| { commit.id == shared && commit.repositories == vec![left, right] })
    );

    let changes = set.diff_commits(left, "HEAD", right, "HEAD").unwrap();
    assert!(
        changes
            .iter()
            .any(|change| { change.path == b"left.txt" && change.kind == ChangeKind::Deleted })
    );
    assert!(
        changes
            .iter()
            .any(|change| { change.path == b"right.txt" && change.kind == ChangeKind::Added })
    );
}

#[test]
fn rejects_invalid_repository_names_and_ids() {
    let fixture = Fixture::new();
    assert!(
        RepositorySet::open([
            ("same", fixture.left.as_path()),
            ("same", fixture.right.as_path()),
        ])
        .is_err()
    );
    assert!(RepositorySet::open([("bad:name", fixture.left.as_path())]).is_err());
    let empty = RepositorySet::open(std::iter::empty::<(&str, &Path)>()).unwrap();
    assert!(empty.is_empty());
}

#[test]
fn builds_revision_aware_snapshots_timeline_and_change_sets() {
    let fixture = Fixture::new();
    let set = RepositorySet::open([
        ("left", fixture.left.as_path()),
        ("right", fixture.right.as_path()),
    ])
    .unwrap();
    let options = HistoryOptions {
        max_commits: 10,
        first_parent: true,
        ..HistoryOptions::default()
    };

    let histories = set.histories_from(&fixture.shared, options).unwrap();
    assert_eq!(histories.len(), 2);
    assert!(histories.iter().all(|history| history.commits.len() == 1));

    let snapshots = set.snapshots_parallel("HEAD").unwrap();
    assert_eq!(snapshots.len(), 2);
    assert!(
        snapshots[0]
            .snapshot
            .entries
            .iter()
            .any(|entry| entry.path == b"left.txt")
    );
    assert!(
        snapshots[1]
            .snapshot
            .entries
            .iter()
            .any(|entry| entry.path == b"right.txt")
    );

    let timeline = set.timeline("HEAD", options).unwrap();
    assert_eq!(timeline.len(), 4);
    assert!(
        timeline
            .windows(2)
            .all(|pair| pair[0].committer_time >= pair[1].committer_time)
    );

    let changes = set.changes_parallel(&fixture.shared, "HEAD").unwrap();
    assert_eq!(changes.len(), 2);
    assert!(changes.iter().all(|change_set| {
        change_set.changes.len() == 1 && change_set.changes[0].kind == ChangeKind::Added
    }));
}

#[test]
fn parallel_open_preserves_repository_order() {
    let fixture = Fixture::new();
    let set = RepositorySet::open_parallel([
        ("left", fixture.left.as_path()),
        ("right", fixture.right.as_path()),
    ])
    .unwrap();
    assert_eq!(
        set.ids()
            .map(|id| set.name(id).unwrap())
            .collect::<Vec<_>>(),
        ["left", "right"]
    );
}

fn configure(path: &Path) {
    git(path, &["config", "user.name", "Weavatrix Test"]);
    git(path, &["config", "user.email", "test@weavatrix.local"]);
}

fn commit(path: &Path, message: &str) {
    git(path, &["add", "-A"]);
    git(path, &["commit", "-q", "-m", message]);
}

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 git_output(path: &Path, args: &[&str]) -> String {
    let output = Command::new("git")
        .args(args)
        .current_dir(path)
        .output()
        .unwrap();
    assert!(output.status.success());
    String::from_utf8(output.stdout).unwrap()
}

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