amiss-git 0.1.0

Read-only Git object store behind a no-follow handle boundary, for Amiss
Documentation
use std::fs;
use std::path::Path;
use std::process::Command;

use amiss_git::{Error, GitLimits, GitResources, ObjectKind, Repository, ValueCap};
use amiss_wire::controls::ResourceName;
use amiss_wire::model::{ObjectFormat, Oid};
use tempfile::TempDir;

#[expect(clippy::expect_used, reason = "test fixture helper")]
fn git(dir: &Path, args: &[&str]) -> String {
    let output = Command::new("git")
        .args(args)
        .current_dir(dir)
        .env("GIT_CONFIG_NOSYSTEM", "1")
        .env("GIT_CONFIG_GLOBAL", dir.join("absent-global-config"))
        .env("GIT_AUTHOR_NAME", "t")
        .env("GIT_AUTHOR_EMAIL", "t@example.invalid")
        .env("GIT_AUTHOR_DATE", "2026-01-01T00:00:00Z")
        .env("GIT_COMMITTER_NAME", "t")
        .env("GIT_COMMITTER_EMAIL", "t@example.invalid")
        .env("GIT_COMMITTER_DATE", "2026-01-01T00:00:00Z")
        .output()
        .expect("run git");
    assert!(
        output.status.success(),
        "git {args:?} failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    String::from_utf8(output.stdout).expect("git output utf-8")
}

fn body(version: usize) -> String {
    (0..200_usize)
        .map(|line| {
            if line == 100 {
                format!("changed line for version {version}\n")
            } else {
                format!("stable line {line} shared by both versions\n")
            }
        })
        .collect()
}

fn doc_cap(limit: u64) -> ValueCap {
    ValueCap {
        resource: ResourceName::DocumentBlobBytes,
        limit,
    }
}

#[expect(clippy::unwrap_used, reason = "test fixture helper")]
fn read_capped(dir: &Path, hex: &str, limit: u64) -> Result<Vec<u8>, Error> {
    let repo = Repository::open(dir, ObjectFormat::Sha1).unwrap();
    let oid = Oid::new(ObjectFormat::Sha1, hex.to_owned()).unwrap();
    let mut res = GitResources::new(GitLimits::CONTRACT);
    repo.read_expected_capped(&mut res, &oid, ObjectKind::Blob, doc_cap(limit))
        .map(|object| object.body)
}

#[test]
fn a_loose_header_past_the_cap_reports_the_smaller_resource() {
    let dir = TempDir::new().unwrap();
    git(dir.path(), &["init", "-q"]);
    fs::write(dir.path().join("doc.md"), body(1)).unwrap();
    git(dir.path(), &["add", "."]);
    git(dir.path(), &["commit", "-qm", "one"]);
    let blob = git(dir.path(), &["rev-parse", "HEAD:doc.md"])
        .trim()
        .to_owned();
    let declared = u64::try_from(body(1).len()).unwrap();

    assert_eq!(
        read_capped(dir.path(), &blob, 64),
        Err(Error::ResourceLimit {
            resource: ResourceName::DocumentBlobBytes,
            configured_limit: 64,
            observed_lower_bound: declared,
        }),
        "a loose header declares its size, so the crossing observes it exactly"
    );
    assert_eq!(
        read_capped(dir.path(), &blob, declared).unwrap(),
        body(1).into_bytes()
    );
}

#[test]
fn packed_and_deltified_objects_honor_the_cap() {
    let dir = TempDir::new().unwrap();
    git(dir.path(), &["init", "-q"]);
    fs::write(dir.path().join("doc.md"), body(1)).unwrap();
    git(dir.path(), &["add", "."]);
    git(dir.path(), &["commit", "-qm", "one"]);
    let first = git(dir.path(), &["rev-parse", "HEAD:doc.md"])
        .trim()
        .to_owned();
    fs::write(dir.path().join("doc.md"), body(2)).unwrap();
    git(dir.path(), &["add", "."]);
    git(dir.path(), &["commit", "-qm", "two"]);
    let second = git(dir.path(), &["rev-parse", "HEAD:doc.md"])
        .trim()
        .to_owned();
    git(
        dir.path(),
        &["repack", "-adfq", "--window=10", "--depth=16"],
    );

    for (blob, version) in [(&first, 1_usize), (&second, 2_usize)] {
        let declared = u64::try_from(body(version).len()).unwrap_or(u64::MAX);
        assert_eq!(
            read_capped(dir.path(), blob, 64),
            Err(Error::ResourceLimit {
                resource: ResourceName::DocumentBlobBytes,
                configured_limit: 64,
                observed_lower_bound: declared,
            }),
            "a packed entry declares its final size, deltified or not"
        );
    }
    assert_eq!(
        read_capped(dir.path(), &first, 1 << 20).unwrap(),
        body(1).into_bytes()
    );
    assert_eq!(
        read_capped(dir.path(), &second, 1 << 20).unwrap(),
        body(2).into_bytes()
    );
}