Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/storage-git-gix/parked-gix-2/gunnar-wire-gix/tests/real_git_interop.rs

//! **Interoperability against the real `git` binary, as a client.** Spec §15.
//!
//! gunnar never runs git. This test does, because the only honest proof that a
//! git server works is that stock `git` talks to it. A test where gunnar's
//! client agrees with gunnar's server proves nothing: both halves can be wrong
//! in the same way, and a hand-written fixture of "what git sends" is a
//! statement about what its author believed, not about what git does.
//!
//! The pipe, the fixtures and the `git` invocation builder live in
//! [`git_harness`]; this file is the case table and the cases. A second target,
//! `real_git_surface`, drives the same harness over the command surface this
//! one does not reach.
//!
//! # Red first
//!
//! Two cases at the end deliberately break the server and assert that `git`
//! **refuses**: one lies about `object-format`, the other strips the sideband
//! band byte off every packfile line. Without them, the passing cases only show
//! that something happened.

#[path = "git_harness/mod.rs"]
mod git_harness;

use std::path::Path;

use git_harness::*;
use gix_hash::Kind;
use gunnar_wire::ProtocolVersion;

fn main() {
    serve_if_asked();

    let cases: &[(&str, fn())] = &[
        ("v2_clone_sha1", v2_clone_sha1),
        ("v0_clone_sha1_unforced_fallback", v0_clone_sha1),
        ("v2_clone_sha256", v2_clone_sha256),
        ("v0_clone_sha256", v0_clone_sha256),
        ("v2_incremental_fetch", v2_incremental_fetch),
        ("v2_fetch_divergent_history_negotiates", v2_fetch_divergent),
        ("v0_fetch_divergent_history_negotiates", v0_fetch_divergent),
        ("fetch_already_up_to_date_is_a_no_op", fetch_up_to_date),
        ("v2_clone_reuses_deltas_P001", v2_clone_reuses_deltas),
        ("v2_shallow_clone_depth_1", v2_shallow_clone),
        // S-031: the three deepening variants, each compared against stock
        // `git-upload-pack` over the same fixture rather than against gunnar's
        // own understanding of them.
        ("v2_deepen_since_matches_stock_git", v2_deepen_since),
        ("v2_deepen_not_matches_stock_git", v2_deepen_not),
        ("v2_deepen_relative_matches_stock_git", v2_deepen_relative),
        ("v2_partial_clone_blob_none", v2_partial_clone),
        // Reachability bitmaps. Every one of these compares the bitmap answer
        // against the walk's, because "the clone worked" is true of a server
        // that ships the whole repository every time.
        ("v2_clone_with_a_bitmap", v2_clone_with_a_bitmap),
        ("v2_fetch_with_a_bitmap", v2_fetch_with_a_bitmap),
        (
            "v2_fetch_after_a_gc_with_a_bitmap",
            v2_fetch_after_a_gc_with_a_bitmap,
        ),
        (
            "v2_clone_with_a_bitmap_sha256",
            v2_clone_with_a_bitmap_sha256,
        ),
        (
            "v2_bitmap_pack_passes_index_pack_strict",
            v2_bitmap_pack_passes_index_pack_strict,
        ),
        ("v2_ls_remote_lists_symref_and_peeled_tag", v2_ls_remote),
        ("push_create_update_delete_sha1", push_lifecycle_sha1),
        ("push_sha256", push_sha256),
        (
            "push_rejected_by_a_gate_is_reported_to_git",
            push_rejected_by_gate,
        ),
        ("push_atomic_rejects_the_whole_batch", push_atomic),
        (
            "push_of_a_conflicting_refname_is_rejected_like_stock_git",
            push_refname_conflict,
        ),
        (
            "RED_wrong_object_format_breaks_the_clone",
            red_wrong_object_format,
        ),
        (
            "RED_dropped_sideband_marker_breaks_the_clone",
            red_dropped_sideband,
        ),
    ];

    // The empty-run trap, closed: "0 passed, 0 failed, out of 0" exits 0 and is
    // shaped exactly like a clean sweep. `S-005`/`S-006` cite the *count*, so
    // the count is asserted rather than reported.
    const EXPECTED_CASES: usize = 27;
    assert_eq!(
        cases.len(),
        EXPECTED_CASES,
        "the §15 case table changed size — update EXPECTED_CASES deliberately, \
         so a table that lost its entries cannot report a clean sweep"
    );
    run_cases("real_git_interop", cases, 27);
}

// ── cases: fetch ────────────────────────────────────────────────────────────

fn v2_clone_sha1() {
    let scene = Scene::new(Kind::Sha1);
    clone_and_verify(&scene, ProtocolVersion::V2, "clone-v2");
}

fn v0_clone_sha1() {
    let scene = Scene::new(Kind::Sha1);
    clone_and_verify(&scene, ProtocolVersion::V0, "clone-v0");
}

fn v2_clone_sha256() {
    let scene = Scene::new(Kind::Sha256);
    let target = clone_and_verify(&scene, ProtocolVersion::V2, "clone-v2-256");
    assert_eq!(
        git_in(&target).args(["rev-parse", "HEAD"]).ok().len(),
        64,
        "a sha256 repository must produce 64-hex object ids"
    );
}

fn v0_clone_sha256() {
    let scene = Scene::new(Kind::Sha256);
    clone_and_verify(&scene, ProtocolVersion::V0, "clone-v0-256");
}

fn v2_incremental_fetch() {
    let scene = Scene::new(Kind::Sha1);
    let target = clone_and_verify(&scene, ProtocolVersion::V2, "work");

    // Move the origin on, through real git, so the fetch has something to do.
    let seed = scene.root.join("seed");
    commit(&seed, "e.txt", "five", "after the clone");
    git_in(&seed)
        .args(["push", "-q", scene.origin.to_str().unwrap(), "main"])
        .ok();

    scene.clear_log();
    scene
        .client(ProtocolVersion::V2)
        .cwd(&target)
        .args(["fetch", "-q", "origin"])
        .ok();
    git_in(&target)
        .args(["merge", "-q", "--ff-only", "origin/main"])
        .ok();
    assert_eq!(
        git_in(&target).args(["rev-parse", "HEAD"]).ok(),
        scene.origin_ref("refs/heads/main")
    );
    git_in(&target).args(["fsck", "--no-progress"]).ok();
}

/// The case spec §6 singles out: a client whose branch has genuinely diverged.
///
/// The assertion is not "the fetch succeeded": a server that ships the whole
/// repository every time also succeeds. It is that the pack is **small**, which
/// is only true if the `have`s were understood.
fn divergent(protocol: ProtocolVersion) {
    let scene = Scene::new(Kind::Sha1);
    let target = clone_and_verify(&scene, protocol, "work");
    let seed = scene.root.join("seed");

    // Forty commits on each side, so negotiation cannot finish in one round
    // with git's default `have` batch of 32.
    git_in(&target).args(["checkout", "-q", "-b", "mine"]).ok();
    for index in 0..40 {
        commit(&target, &format!("mine{index}.txt"), "mine", "local work");
        commit(
            &seed,
            &format!("theirs{index}.txt"),
            "theirs",
            "remote work",
        );
    }
    git_in(&seed)
        .args(["push", "-q", scene.origin.to_str().unwrap(), "main"])
        .ok();

    // What a fresh clone of the grown origin would cost.
    scene.clear_log();
    let probe = scene.root.join("probe");
    scene
        .client(protocol)
        .args(["clone", "-q", "--bare"])
        .arg(scene.url())
        .arg(probe.to_str().unwrap())
        .ok();
    let whole_repository = objects_sent(&scene);
    std::fs::remove_dir_all(&probe).ok();

    scene.clear_log();
    scene
        .client(protocol)
        .cwd(&target)
        .args(["fetch", "-q", "origin"])
        .ok();
    git_in(&target).args(["fsck", "--no-progress"]).ok();
    assert_eq!(
        git_in(&target).args(["rev-parse", "origin/main"]).ok(),
        scene.origin_ref("refs/heads/main")
    );

    let sent = objects_sent(&scene);
    assert!(
        sent < whole_repository,
        "the divergent fetch sent {sent} objects where a whole clone is \
         {whole_repository}: the `have`s were not used"
    );
    let rounds = fetch_rounds(&scene);
    assert!(
        rounds >= 2,
        "a 40-commit divergence negotiated in {rounds} round(s); the \
         have/want loop is not being exercised"
    );
}

fn v2_fetch_divergent() {
    divergent(ProtocolVersion::V2);
}

fn v0_fetch_divergent() {
    divergent(ProtocolVersion::V0);
}

/// An empty fetch has to be a clean no-op, not an error and not a full pack.
fn fetch_up_to_date() {
    let scene = Scene::new(Kind::Sha1);
    let target = clone_and_verify(&scene, ProtocolVersion::V2, "work");

    scene.clear_log();
    let (ok, _, stderr) = scene
        .client(ProtocolVersion::V2)
        .cwd(&target)
        .args(["fetch", "origin"])
        .run();
    assert!(ok, "an up-to-date fetch failed:\n{stderr}");
    assert_eq!(
        objects_sent(&scene),
        0,
        "a fetch with nothing to do still packed objects; log was {:?}",
        scene.server_log()
    );
    git_in(&target).args(["fsck", "--no-progress"]).ok();
}

/// **`P-001`, end to end and judged by the client.**
///
/// Every other clone case here runs against `Scene`'s six-object fixture, where
/// git's delta encoder has nothing to find, so they prove the pack-copy path is
/// *correct* and say nothing about whether it copied anything. This one runs
/// against a repository stock git delta-compressed itself and asserts three
/// things no clone-succeeded check can see:
///
/// 1. the server reports the objects **copied** rather than recompressed,
/// 2. `git` accepts the delta-carrying pack: `index-pack` resolved every
///    `ofs-delta` base distance gunnar re-pointed, and `fsck --strict` walked
///    every object,
/// 3. the clone's own pack is a fraction of the content it represents, which
///    is only possible if the deltas came across.
///
/// Point 2 is the reason this case exists at all: a wrong base distance
/// produces a smaller pack that no git can read, and *"smaller"* is exactly
/// what `P-001` is judged on.
fn v2_clone_reuses_deltas() {
    let scene = Scene::churned(Kind::Sha1);
    let target = scene.root.join("delta-clone");
    scene.clear_log();
    scene
        .client(ProtocolVersion::V2)
        .args(["clone", "-q"])
        .arg(scene.url())
        .arg(target.to_str().unwrap())
        .ok();

    // git's own strictest read of what gunnar sent.
    git_in(&target)
        .args(["fsck", "--strict", "--no-progress"])
        .ok();
    assert_eq!(
        git_in(&target).args(["rev-parse", "HEAD"]).ok(),
        scene.origin_ref("refs/heads/main")
    );

    let log = scene.server_log();
    let copied = log_field(&log, "copied=");
    let recompressed = log_field(&log, "recompressed=");
    let sent = objects_sent(&scene);
    assert!(
        copied > 0 && recompressed == 0,
        "the server served {sent} objects of a fully packed repository with \
         copied={copied} recompressed={recompressed} — it inflated and \
         re-deflated bytes it already had (P-001). Log: {log:?}"
    );
    assert_eq!(
        copied, sent as usize,
        "only {copied} of {sent} objects were copied. Log: {log:?}"
    );

    // The clone's pack against the raw content it stands for. Churned text
    // compresses well on its own, so this is a loose bound deliberately; the
    // tight one is the head-to-head bench, not a correctness suite. What it
    // catches is a pack that came across as whole objects.
    let pack_bytes: u64 = std::fs::read_dir(target.join(".git/objects/pack"))
        .expect("the clone has a pack directory")
        .flatten()
        .filter(|e| e.path().extension().and_then(|x| x.to_str()) == Some("pack"))
        .filter_map(|e| e.metadata().ok().map(|m| m.len()))
        .sum();
    assert!(
        pack_bytes > 0,
        "the clone has no packfile at all, so nothing above was measured"
    );
    let content_bytes = (CHURN_COMMITS * CHURN_LINES * 45) as u64;
    assert!(
        pack_bytes * 10 < content_bytes,
        "the clone's pack is {pack_bytes} B for ~{content_bytes} B of revisions \
         — under 10x, which is what whole-object deflate gets and delta reuse \
         beats by a wide margin"
    );
}

fn v2_shallow_clone() {
    let scene = Scene::new(Kind::Sha1);
    let target = scene.root.join("shallow");
    scene
        .client(ProtocolVersion::V2)
        .args(["clone", "-q", "--depth=1"])
        .arg(scene.url())
        .arg(target.to_str().unwrap())
        .ok();

    git_in(&target).args(["fsck", "--no-progress"]).ok();
    assert!(
        target.join(".git").join("shallow").exists(),
        "a --depth=1 clone produced no shallow file, so no boundary was sent"
    );
    let commits: usize = git_in(&target)
        .args(["rev-list", "--count", "HEAD"])
        .ok()
        .parse()
        .unwrap();
    assert_eq!(commits, 1, "--depth=1 produced {commits} commits");
    assert_eq!(
        git_in(&target).args(["rev-parse", "HEAD"]).ok(),
        scene.origin_ref("refs/heads/main")
    );
}

/// Everything a clone ended up holding: object ids, and its shallow boundary.
///
/// Ids rather than `rev-list` output verbatim, because the path column differs
/// between a tree walked from one root and the same tree walked from another.
fn clone_contents(dir: &Path) -> (Vec<String>, Vec<String>) {
    let listing = git_in(dir).args(["rev-list", "--objects", "--all"]).ok();
    let mut ids: Vec<String> = listing
        .lines()
        .filter_map(|line| line.split_whitespace().next())
        .map(str::to_owned)
        .collect();
    ids.sort();
    ids.dedup();

    let mut shallow: Vec<String> = std::fs::read_to_string(dir.join(".git").join("shallow"))
        .unwrap_or_default()
        .lines()
        .map(str::to_owned)
        .collect();
    shallow.sort();
    (ids, shallow)
}

/// Clone `scene`'s repository twice with the same `flags`, once from gunnar
/// over `ext::` and once from stock `git-upload-pack` over `file://`, then
/// assert the two clones are indistinguishable.
///
/// This is the assertion `S-031` exists for. gunnar's own unit tests for
/// deepening were written from the same understanding as `shallow.rs`, so they
/// can only confirm it; another implementation of the same protocol cannot.
///
/// Returns what arrived, so the caller can assert the request did something
/// *specific*. Two implementations that both ignore an argument agree
/// perfectly.
fn deepening_matches_stock_git(scene: &Scene, name: &str, flags: &[&str]) -> Deepened {
    let ours = scene.root.join(format!("{name}-gunnar"));
    let theirs = scene.root.join(format!("{name}-git"));

    let base = ["clone", "-q", "--single-branch", "--branch", "main"];
    scene
        .client(ProtocolVersion::V2)
        .args(base)
        .args(flags.iter().copied())
        .arg(scene.url())
        .arg(ours.to_str().unwrap())
        .ok();
    Git::new()
        .cwd(&scene.root)
        .args(base)
        .args(flags.iter().copied())
        .arg(scene.local_url())
        .arg(theirs.to_str().unwrap())
        .ok();

    // A clone gunnar produced that git cannot check is not a clone.
    git_in(&ours).args(["fsck", "--no-progress"]).ok();

    let (our_ids, our_shallow) = clone_contents(&ours);
    let (their_ids, their_shallow) = clone_contents(&theirs);

    assert_eq!(
        our_shallow, their_shallow,
        "{name}: gunnar cut the history at a different boundary than stock git.\n\
         gunnar: {our_shallow:?}\nstock:  {their_shallow:?}"
    );
    assert_eq!(
        our_ids.len(),
        their_ids.len(),
        "{name}: gunnar sent {} objects and stock git sent {}",
        our_ids.len(),
        their_ids.len()
    );
    assert_eq!(
        our_ids, their_ids,
        "{name}: the two clones hold different objects"
    );

    let commits: usize = git_in(&ours)
        .args(["rev-list", "--count", "HEAD"])
        .ok()
        .parse()
        .unwrap();
    assert_eq!(
        commits,
        git_in(&theirs)
            .args(["rev-list", "--count", "HEAD"])
            .ok()
            .parse::<usize>()
            .unwrap(),
        "{name}: the two clones disagree on how much history arrived"
    );
    Deepened {
        commits,
        boundary: our_shallow,
    }
}

/// What a deepening request delivered, once gunnar and stock git have been
/// proven to have delivered the same thing.
struct Deepened {
    /// Commits reachable from `HEAD` in the resulting clone.
    commits: usize,
    /// The clone's `.git/shallow`, sorted. This is the field that tells
    /// `--depth`, `--shallow-since` and `--shallow-exclude` apart; the commit
    /// count does not (see [`Scene::dated`]).
    boundary: Vec<String>,
}

/// `deepen-since`, i.e. `git clone --shallow-since`, agrees with stock git and
/// says something `--depth` does not (`S-031`).
///
/// The cut sits between `C`(+3000) and `B`(+2000). Both requests land the same
/// two commits — `D` and `M` — and stop at a **different boundary**: `--depth=2`
/// grafts `M` alone, the date cut grafts `C` as well, because `C` is inside the
/// window while `M`'s other parent `B` is not. The boundary is asserted
/// commit-by-commit, because "gunnar agrees with git" is also true of two
/// servers that both ignore the argument.
fn v2_deepen_since() {
    let scene = Scene::dated(Kind::Sha1);
    let depth2 = deepening_matches_stock_git(&scene, "depth2", &["--depth=2"]);
    let since = deepening_matches_stock_git(&scene, "since", &[SHALLOW_SINCE]);

    let merge = scene.origin_ref("refs/heads/main^");
    let older = scene.origin_ref("refs/heads/side");
    assert_eq!(
        (depth2.commits, since.commits),
        (2, 2),
        "--depth=2 gave {} commits and --shallow-since gave {}; both land D and M",
        depth2.commits,
        since.commits
    );
    assert_eq!(
        depth2.boundary,
        vec![merge.clone()],
        "--depth=2 must graft the merge and nothing else"
    );
    assert_eq!(
        since.boundary,
        sorted(vec![merge, older]),
        "--shallow-since must graft the merge *and* name C as a cut point; a \
         boundary equal to --depth=2's means the date was ignored"
    );
    assert_ne!(
        depth2.boundary, since.boundary,
        "--depth=2 and --shallow-since cut at the same place, so this fixture \
         cannot tell the two arguments apart"
    );
}

/// `deepen-not` (`git clone --shallow-exclude`) agrees with stock git on the
/// merge case `graph.rs` calls "a bug worth naming".
///
/// Excluding `refs/heads/side` cuts `M` off from the parent that leads to `C`
/// while keeping the parent that leads to `B` — which is why `B`, and not `C`,
/// is the cut point named alongside the merge. A server that treated the
/// exclusion as a *boundary* rather than a parent restriction never reaches `B`
/// at all and cannot produce that boundary.
fn v2_deepen_not() {
    let scene = Scene::dated(Kind::Sha1);
    let excluded =
        deepening_matches_stock_git(&scene, "exclude", &["--shallow-exclude=refs/heads/side"]);
    let merge = scene.origin_ref("refs/heads/main^");
    let kept_parent = scene.origin_ref("refs/heads/main^^");
    assert_eq!(excluded.commits, 2, "--shallow-exclude lands D and M");
    assert_eq!(
        excluded.boundary,
        sorted(vec![merge, kept_parent]),
        "--shallow-exclude=refs/heads/side must cut at B — the parent the \
         exclusion *kept* — and not at C, which the exclusion removed"
    );

    // ...and it is genuinely a different answer from the other two variants,
    // which is what makes all three cases worth running. Same fixture, same
    // client, three arguments.
    let since_dir = scene.root.join("since-cmp");
    scene
        .client(ProtocolVersion::V2)
        .args(["clone", "-q", "--single-branch", "--branch", "main"])
        .arg(SHALLOW_SINCE)
        .arg(scene.url())
        .arg(since_dir.to_str().unwrap())
        .ok();
    let (_, since_shallow) = clone_contents(&since_dir);
    assert_ne!(
        excluded.boundary, since_shallow,
        "--shallow-exclude and --shallow-since cut at the same place, so the \
         fixture cannot tell the two arguments apart and neither case proves \
         anything"
    );
}

/// `values`, sorted — the order [`clone_contents`] returns a boundary in.
fn sorted(mut values: Vec<String>) -> Vec<String> {
    values.sort();
    values
}

/// `deepen-relative` (`git fetch --deepen=<n>` on an already-shallow clone)
/// agrees with stock git.
///
/// The only way to exercise `deepen-relative` at all: it counts from the
/// *client's* existing boundary rather than from the wants, so it needs a
/// client that already has one. Both clones start at `--depth=1` and are
/// deepened by one, and the fetch is asserted to have actually moved the
/// boundary, since a `--deepen` that did nothing would leave the two clones equal
/// and pass a comparison on its own.
fn v2_deepen_relative() {
    let scene = Scene::dated(Kind::Sha1);
    let before = deepening_matches_stock_git(&scene, "rel", &["--depth=1"]);
    assert_eq!(
        before.commits, 1,
        "--depth=1 gave {} commits",
        before.commits
    );

    let ours = scene.root.join("rel-gunnar");
    let theirs = scene.root.join("rel-git");
    scene
        .client(ProtocolVersion::V2)
        .cwd(&ours)
        .args(["fetch", "-q", "--deepen=1", "origin"])
        .ok();
    Git::new()
        .cwd(&theirs)
        .args(["fetch", "-q", "--deepen=1", "origin"])
        .ok();

    git_in(&ours).args(["fsck", "--no-progress"]).ok();
    let (our_ids, our_shallow) = clone_contents(&ours);
    let (their_ids, their_shallow) = clone_contents(&theirs);
    assert_eq!(
        our_shallow, their_shallow,
        "after --deepen=1 gunnar's boundary is {our_shallow:?} and stock git's \
         is {their_shallow:?}"
    );
    assert_eq!(
        our_ids, their_ids,
        "after --deepen=1 the two clones hold different objects"
    );

    let commits: usize = git_in(&ours)
        .args(["rev-list", "--count", "HEAD"])
        .ok()
        .parse()
        .unwrap();
    assert_eq!(
        commits, 2,
        "--deepen=1 from a depth-1 clone left {commits} commits; the boundary \
         did not move, so this case never exercised deepen-relative"
    );
}

fn v2_partial_clone() {
    let scene = Scene::new(Kind::Sha1);
    let target = scene.root.join("partial");
    scene
        .client(ProtocolVersion::V2)
        .args(["clone", "-q", "--no-checkout", "--filter=blob:none"])
        .arg(scene.url())
        .arg(target.to_str().unwrap())
        .ok();

    // The point of the filter is that blobs did NOT arrive. A server that
    // parsed `filter` and ignored it produces a clone that works perfectly.
    let objects = git_in(&target)
        .args(["cat-file", "--batch-check", "--batch-all-objects"])
        .ok();
    let blobs = objects
        .lines()
        .filter(|line| line.contains(" blob "))
        .count();
    assert_eq!(
        blobs, 0,
        "a blob:none clone received {blobs} blobs, so the filter did nothing"
    );
    assert!(
        objects.lines().any(|line| line.contains(" commit ")),
        "a blob:none clone received no commits either"
    );
    // And it must still be a usable partial clone: checking out fetches the
    // missing blobs back through the same server.
    scene
        .client(ProtocolVersion::V2)
        .cwd(&target)
        .args(["checkout", "-q", "main"])
        .ok();
    assert_eq!(
        std::fs::read_to_string(target.join("a.txt"))
            .unwrap()
            .trim(),
        "one"
    );
}

fn v2_ls_remote() {
    let scene = Scene::new(Kind::Sha1);
    scene.clear_log();
    let output = scene
        .client(ProtocolVersion::V2)
        .args(["ls-remote", "--symref"])
        .arg(scene.url())
        .ok();

    assert!(
        output.contains("ref: refs/heads/main\tHEAD"),
        "ls-remote --symref did not report the symref:\n{output}"
    );
    assert!(
        output.contains("refs/tags/v1^{}"),
        "ls-remote did not report the peeled tag:\n{output}"
    );
    assert!(
        output.contains("refs/heads/side"),
        "ls-remote lost a branch:\n{output}"
    );
}

// ── cases: push ─────────────────────────────────────────────────────────────

fn push_lifecycle(hash_kind: Kind) {
    let scene = Scene::new(hash_kind);
    let target = clone_and_verify(&scene, ProtocolVersion::V0, "work");

    // create
    commit(&target, "pushed.txt", "pushed", "a pushed commit");
    scene.clear_log();
    scene
        .client(ProtocolVersion::V0)
        .cwd(&target)
        .args(["push", "-q", "origin", "HEAD:refs/heads/pushed"])
        .ok();
    let local = git_in(&target).args(["rev-parse", "HEAD"]).ok();
    assert_eq!(scene.origin_ref("refs/heads/pushed"), local);
    scene.origin_git().args(["fsck", "--no-progress"]).ok();

    // update. A second commit onto the same branch, which is the thin-pack
    // case: its deltas resolve against objects only the server has.
    commit(&target, "pushed2.txt", "again", "a second pushed commit");
    scene
        .client(ProtocolVersion::V0)
        .cwd(&target)
        .args(["push", "-q", "origin", "HEAD:refs/heads/pushed"])
        .ok();
    assert_eq!(
        scene.origin_ref("refs/heads/pushed"),
        git_in(&target).args(["rev-parse", "HEAD"]).ok()
    );
    scene.origin_git().args(["fsck", "--no-progress"]).ok();

    // and the pushed history is servable back out again
    let readback = scene.root.join("readback");
    scene
        .client(ProtocolVersion::V2)
        .args(["clone", "-q", "--branch", "pushed"])
        .arg(scene.url())
        .arg(readback.to_str().unwrap())
        .ok();
    assert_eq!(git_in(&readback).args(["rev-parse", "HEAD"]).ok(), {
        let mut expected = scene.origin_ref("refs/heads/pushed");
        expected.truncate(if hash_kind == Kind::Sha256 { 64 } else { 40 });
        expected
    });

    // delete
    scene
        .client(ProtocolVersion::V0)
        .cwd(&target)
        .args(["push", "-q", "origin", ":refs/heads/pushed"])
        .ok();
    let (exists, _, _) = scene
        .origin_git()
        .args(["rev-parse", "--verify", "refs/heads/pushed"])
        .run();
    assert!(!exists, "the branch survived a delete push");
}

fn push_lifecycle_sha1() {
    push_lifecycle(Kind::Sha1);
}

fn push_sha256() {
    push_lifecycle(Kind::Sha256);
}

/// **`refs/heads/x` and `refs/heads/x/y` cannot both exist**, and the client
/// has to be told so rather than handed a repository no loose-ref backend can
/// hold.
///
/// Stock git 2.53.0 into a bare repository already holding `refs/heads/x`,
/// measured:
///
/// ```text
///  ! [remote rejected] HEAD -> x/y (refname conflict)
///  error: failed to push some refs
/// ```
///
/// gunnar's `fs` backend inherited the rule from gix and its three in-process
/// backends did not — MEASURED 2026-08-05, `mem`, the Arrow log and the
/// in-memory Arrow log all accepted the pair. This case is the client-facing
/// half: the refusal has to arrive as an `ng`, and the reference must not be
/// there afterwards. The per-backend half is `gunnar-store`'s conformance
/// check `ref_directory_file_conflict`, which runs on all four.
fn push_refname_conflict() {
    let scene = Scene::new(Kind::Sha1);
    let target = clone_and_verify(&scene, ProtocolVersion::V0, "work-df");

    // The file first. This one must land, or the conflict below would be
    // green on a server that refuses every push.
    scene
        .client(ProtocolVersion::V0)
        .cwd(&target)
        .args(["push", "origin", "HEAD:refs/heads/dfx"])
        .ok();

    let (ok, stdout, stderr) = scene
        .client(ProtocolVersion::V0)
        .cwd(&target)
        .args(["push", "origin", "HEAD:refs/heads/dfx/y"])
        .run();
    assert!(
        !ok,
        "gunnar accepted refs/heads/dfx/y beside refs/heads/dfx; stock git \
         refuses the identical push:\n{stdout}\n{stderr}"
    );
    assert!(
        stderr.contains("[remote rejected]") || stdout.contains("[remote rejected]"),
        "the refusal did not reach git as an `ng`:\n{stdout}\n{stderr}"
    );

    // Applied output: what the server actually holds. A refusal that still
    // moved the reference, or one that dropped `dfx` on the way, would both
    // satisfy the exit-status assertion above.
    let refs = scene
        .origin_git()
        .args(["for-each-ref", "--format=%(refname)"])
        .ok();
    assert!(
        refs.lines().any(|r| r == "refs/heads/dfx"),
        "the refused push took refs/heads/dfx with it:\n{refs}"
    );
    assert!(
        !refs.lines().any(|r| r == "refs/heads/dfx/y"),
        "the refused reference is on the server anyway:\n{refs}"
    );
}

/// A refusal that comes from the **server** has to reach the client as a
/// refusal, not as a hang-up.
///
/// This is driven through the policy gate rather than through a stale old
/// value, because stock git checks the old value on its own side and never
/// sends a push it expects to be refused. The server-side compare-and-swap
/// itself is pinned by `tests/protocol.rs`, where the bytes can be built by
/// hand; what only a real client can prove is that `ng <ref> <reason>` is
/// understood.
fn push_rejected_by_gate() {
    let scene = Scene::new(Kind::Sha1);
    let target = clone_and_verify(&scene, ProtocolVersion::V0, "work");
    commit(
        &target,
        "gated.txt",
        "gated",
        "a commit the gate will refuse",
    );

    scene.clear_log();
    let (ok, stdout, stderr) = scene
        .client(ProtocolVersion::V0)
        .env("GUNNAR_WIRE_BREAK", "reject-push")
        .cwd(&target)
        .args(["push", "origin", "HEAD:refs/heads/gated"])
        .run();
    assert!(
        !ok,
        "a gate-refused push reported success:\n{stdout}\n{stderr}"
    );
    assert!(
        stderr.contains("[remote rejected]") || stdout.contains("[remote rejected]"),
        "git did not parse gunnar's refusal:\n{stdout}\n{stderr}"
    );
    assert!(
        stderr.contains("the gunnar test gate says no")
            || stdout.contains("the gunnar test gate says no"),
        "the reason gunnar sent did not reach the user:\n{stdout}\n{stderr}"
    );
    let log = scene.server_log();
    assert!(
        log.iter().any(|line| line.contains("ng:")),
        "the server did not record a refusal; it logged {log:?}"
    );
    let (created, _, _) = scene
        .origin_git()
        .args(["rev-parse", "--verify", "refs/heads/gated"])
        .run();
    assert!(!created, "a refused push created the reference anyway");

    // ...and the identical push lands once the gate is out of the way, so the
    // failure above is the gate and not the fixture.
    scene
        .client(ProtocolVersion::V0)
        .cwd(&target)
        .args(["push", "-q", "origin", "HEAD:refs/heads/gated"])
        .ok();
    assert_eq!(
        scene.origin_ref("refs/heads/gated"),
        git_in(&target).args(["rev-parse", "HEAD"]).ok()
    );
}

/// `--atomic`: one refused reference fails the batch, and the good one must
/// not land.
///
/// The refusal is made to happen **on the server**, so the batch really is
/// sent and really is applied-or-not by `RefStore::transaction`. A per-
/// reference loop that stopped at the first refusal would leave the earlier
/// one applied, and only the second assertion here would notice.
fn push_atomic() {
    let scene = Scene::new(Kind::Sha1);
    let target = clone_and_verify(&scene, ProtocolVersion::V0, "work");
    commit(&target, "atomic.txt", "atomic", "an atomic push");

    scene.clear_log();
    let (ok, stdout, stderr) = scene
        .client(ProtocolVersion::V0)
        .env("GUNNAR_WIRE_BREAK", "reject-side")
        .cwd(&target)
        .args([
            "push",
            "--atomic",
            "origin",
            "HEAD:refs/heads/atomic-new",
            "HEAD:refs/heads/side",
        ])
        .run();
    assert!(
        !ok,
        "an atomic push with a refused reference reported success:\n{stdout}\n{stderr}"
    );

    let log = scene.server_log();
    let statuses = log
        .iter()
        .find_map(|line| line.split("statuses=").nth(1))
        .unwrap_or("");
    assert_eq!(
        statuses.matches("ng:").count(),
        2,
        "an atomic batch reported a partial result: {statuses:?}"
    );

    let (created, _, _) = scene
        .origin_git()
        .args(["rev-parse", "--verify", "refs/heads/atomic-new"])
        .run();
    assert!(
        !created,
        "the good half of an atomic push landed anyway; it is not atomic"
    );

    // Without --atomic the same batch must apply what it can, or `atomic` is
    // not a capability but the only behaviour.
    scene.clear_log();
    let (_, _, _) = scene
        .client(ProtocolVersion::V0)
        .env("GUNNAR_WIRE_BREAK", "reject-side")
        .cwd(&target)
        .args([
            "push",
            "origin",
            "HEAD:refs/heads/loose-new",
            "HEAD:refs/heads/side",
        ])
        .run();
    let (created, _, _) = scene
        .origin_git()
        .args(["rev-parse", "--verify", "refs/heads/loose-new"])
        .run();
    assert!(
        created,
        "a non-atomic batch refused everything because one reference was bad"
    );
}

// ── cases: red first ────────────────────────────────────────────────────────

/// Spec §6's documented, session-costing bug: answer the wrong
/// `object-format`. If this case passes, every green above is meaningless.
fn red_wrong_object_format() {
    let scene = Scene::new(Kind::Sha256);
    let target = scene.root.join("should-not-exist");
    let (ok, stdout, stderr) = scene
        .client(ProtocolVersion::V2)
        .env("GUNNAR_WIRE_BREAK", "hash-lie")
        .args(["clone", "-q"])
        .arg(scene.url())
        .arg(target.to_str().unwrap())
        .run();
    assert!(
        !ok,
        "git accepted a server that advertised sha1 for a sha256 \
         repository:\n{stdout}\n{stderr}"
    );

    // ...and the same clone works once the lie is removed, so the failure is
    // the lie and not the harness.
    let honest = scene.root.join("honest");
    scene
        .client(ProtocolVersion::V2)
        .args(["clone", "-q"])
        .arg(scene.url())
        .arg(honest.to_str().unwrap())
        .ok();
}

/// Drop the sideband band byte from every packfile line. The bytes stay
/// well-formed pkt-lines, so only a client that really demultiplexes band 1
/// notices, which is the point.
fn red_dropped_sideband() {
    let scene = Scene::new(Kind::Sha1);
    let target = scene.root.join("should-not-exist");
    let (ok, stdout, stderr) = scene
        .client(ProtocolVersion::V2)
        .env("GUNNAR_WIRE_BREAK", "no-sideband")
        .args(["clone", "-q"])
        .arg(scene.url())
        .arg(target.to_str().unwrap())
        .run();
    assert!(
        !ok,
        "git accepted a v2 packfile section with no band byte:\n{stdout}\n{stderr}"
    );

    let honest = scene.root.join("honest");
    scene
        .client(ProtocolVersion::V2)
        .args(["clone", "-q"])
        .arg(scene.url())
        .arg(honest.to_str().unwrap())
        .ok();
}

// ───────────────────────────────────────────────────────────────────────────
// Reachability bitmaps, end to end, with stock git as the judge
// ───────────────────────────────────────────────────────────────────────────

/// Repack `scene.origin` so it carries a `.bitmap`, and prove it got one.
///
/// The `assert` is not decoration. Every case below would pass with the whole
/// reader deleted if the repository quietly had no bitmap to read — which is
/// precisely how the previous lane's writer shipped broken on 18 of 19 real
/// repositories with every fixture green.
///
/// Note which branch of `repack` this takes: `Scene` builds its origin with one
/// `git clone --bare`, so the origin holds **one** pack and the repack is the
/// `already_consolidated` no-op that writes only the sidecar. That is the shape
/// a real served repository is in, and the shape the fixtures that missed the
/// bug were not.
fn bitmap_the_origin(scene: &Scene) -> std::path::PathBuf {
    let hash_kind = detect_hash_kind(&scene.origin);
    let store = gunnar_store::fs::FsStore::open_or_init(&scene.origin, hash_kind).expect("open");
    let outcome = store
        .repack(&gunnar_store::RepackOptions::new())
        .expect("repack");
    let bitmap = outcome
        .bitmap
        .expect("the fixture got no .bitmap, so nothing below tests the bitmap path");
    assert!(
        bitmap.exists(),
        "repack reported a .bitmap that is not on disk"
    );
    // The store holds the repository's exclusive lock; the server process that
    // is about to serve it needs it.
    drop(store);
    bitmap
}

/// Every object id in a repository, sorted. The comparison unit for the cases
/// below — a **set**, because two packs of the same size can hold different
/// objects and both index cleanly.
fn objects_of(git_dir: &Path) -> Vec<String> {
    let mut ids: Vec<String> = git_in(git_dir)
        .args(["rev-list", "--objects", "--all"])
        .ok()
        .lines()
        .filter_map(|line| line.split_whitespace().next().map(str::to_owned))
        .collect();
    ids.sort();
    ids
}

/// A clone off a bitmapped repository lands the same objects as one off the
/// walk, and stock git accepts both.
///
/// Three judges, and each can pass while another fails:
///
/// * `fsck --full --strict` guards the **pack** — and the previous lane
///   recorded it staying green through a bitmap that was flatly wrong, because
///   a wrong object *set* is still a well-formed pack;
/// * the object-set comparison is what catches that, and it is the one this
///   case exists for;
/// * `rev-parse HEAD` catches a clone that landed a tree nobody asked for.
fn v2_clone_with_a_bitmap() {
    let scene = Scene::new(Kind::Sha1);
    bitmap_the_origin(&scene);

    let with = clone_and_verify(&scene, ProtocolVersion::V2, "with");
    git_in(&with).args(["fsck", "--full", "--strict"]).ok();

    // The same clone with the reader switched off, so the comparison is
    // between the two paths rather than between gunnar and its own memory.
    let without = scene.root.join("without");
    scene
        .client(ProtocolVersion::V2)
        .env("GUNNAR_BITMAP", "off")
        .args(["clone", "-q"])
        .arg(scene.url())
        .arg(without.to_str().unwrap())
        .ok();
    git_in(&without).args(["fsck", "--full", "--strict"]).ok();

    assert_eq!(
        objects_of(&with),
        objects_of(&without),
        "the bitmap clone and the walk clone landed different objects"
    );
    assert_eq!(
        git_in(&with).args(["rev-parse", "HEAD"]).ok(),
        git_in(&without).args(["rev-parse", "HEAD"]).ok(),
    );
}

/// A negotiated fetch off a bitmapped repository, which is the exclusion side.
///
/// A clone has no `have`s at all, so it exercises the OR and nothing else. This
/// exercises the `AND NOT` — the half the README's "exclusion is
/// `O(client history)`" was about — and the bounded walk, because the commits
/// pushed after the repack are in a second pack with no bitmap of their own.
fn v2_fetch_with_a_bitmap() {
    let scene = Scene::new(Kind::Sha1);
    bitmap_the_origin(&scene);

    let with = clone_and_verify(&scene, ProtocolVersion::V2, "with");
    let without = scene.root.join("without");
    scene
        .client(ProtocolVersion::V2)
        .env("GUNNAR_BITMAP", "off")
        .args(["clone", "-q"])
        .arg(scene.url())
        .arg(without.to_str().unwrap())
        .ok();

    // Move the origin on **after** the repack, so HEAD is a commit with no
    // stored bitmap and the fetch has to walk down to one that has.
    let seed = scene.root.join("seed");
    for index in 0..3 {
        commit(
            &seed,
            &format!("late{index}.txt"),
            "late",
            "after the repack",
        );
    }
    git_in(&seed)
        .args(["push", "-q", scene.origin.to_str().unwrap(), "main"])
        .ok();

    let mut sent = Vec::new();
    for (target, bitmaps) in [(&with, "on"), (&without, "off")] {
        scene.clear_log();
        let mut client = scene.client(ProtocolVersion::V2).cwd(target);
        if bitmaps == "off" {
            client = client.env("GUNNAR_BITMAP", "off");
        }
        client.args(["fetch", "-q", "origin"]).ok();
        git_in(target)
            .args(["merge", "-q", "--ff-only", "origin/main"])
            .ok();
        git_in(target).args(["fsck", "--full", "--strict"]).ok();
        sent.push(objects_sent(&scene));
    }

    // The object **count**, because nothing above can see the exclusion at all.
    // MEASURED 2026-08-04 with the `AND NOT` deliberately removed: this whole
    // file stayed green while the server shipped the client's own history back
    // to it. The pack was bigger, valid, `fsck`ed clean and landed identical
    // objects — because the client already had the surplus. Only this number
    // moves.
    // No count assertion here, deliberately, and the reason is worth recording
    // rather than leaving as a gap somebody re-adds. On this fixture the want
    // is a commit with **no** stored bitmap, so the bounded walk answers it —
    // and that walk already turns away anything in the `have` set as it goes
    // (git's `include_data.seen`). The final `AND NOT` therefore has nothing
    // left to subtract, and MEASURED 2026-08-04 with it deliberately removed
    // this case reported 7 objects either way. A guard that cannot move is not
    // a guard; `v2_fetch_after_a_gc_with_a_bitmap` is where the subtraction is
    // load-bearing, and that is where the count is asserted.
    let _ = sent;

    assert_eq!(
        objects_of(&with),
        objects_of(&without),
        "the bitmap fetch and the walk fetch landed different objects"
    );
    assert_eq!(
        git_in(&with).args(["rev-parse", "HEAD"]).ok(),
        scene.origin_ref("refs/heads/main"),
        "the bitmap fetch did not land the origin's main"
    );
}

/// A fetch where **both sides are bitmapped**, which is the steady state and
/// the only shape in which the `AND NOT` is load-bearing.
///
/// `v2_fetch_with_a_bitmap` fetches commits pushed *after* the repack, so its
/// want has no stored bitmap and the bounded walk answers it — turning away the
/// client's objects as it goes, leaving the subtraction nothing to do. Repack a
/// second time and the want has a bitmap: the whole closure is OR'd in at once,
/// and the only thing standing between the client and its own history coming
/// back to it is the `AND NOT`.
///
/// MEASURED 2026-08-04, with the subtraction deliberately removed: this case
/// goes from 7 objects to the whole repository. Without it the removal is
/// invisible to all 25 cases in this file.
fn v2_fetch_after_a_gc_with_a_bitmap() {
    let scene = Scene::new(Kind::Sha1);
    bitmap_the_origin(&scene);
    let with = clone_and_verify(&scene, ProtocolVersion::V2, "with");
    let without = scene.root.join("without");
    scene
        .client(ProtocolVersion::V2)
        .env("GUNNAR_BITMAP", "off")
        .args(["clone", "-q"])
        .arg(scene.url())
        .arg(without.to_str().unwrap())
        .ok();

    let seed = scene.root.join("seed");
    for index in 0..3 {
        commit(
            &seed,
            &format!("late{index}.txt"),
            "late",
            "after the first repack",
        );
    }
    git_in(&seed)
        .args(["push", "-q", scene.origin.to_str().unwrap(), "main"])
        .ok();
    // The second repack is the point: now the new tip has a bitmap too.
    bitmap_the_origin(&scene);

    let mut sent = Vec::new();
    for (target, bitmaps) in [(&with, "on"), (&without, "off")] {
        scene.clear_log();
        let mut client = scene.client(ProtocolVersion::V2).cwd(target);
        if bitmaps == "off" {
            client = client.env("GUNNAR_BITMAP", "off");
        }
        client.args(["fetch", "-q", "origin"]).ok();
        git_in(target)
            .args(["merge", "-q", "--ff-only", "origin/main"])
            .ok();
        git_in(target).args(["fsck", "--full", "--strict"]).ok();
        sent.push(objects_sent(&scene));
    }

    assert_eq!(
        objects_of(&with),
        objects_of(&without),
        "the bitmap fetch and the walk fetch landed different objects"
    );
    assert!(
        sent[0] > 0 && sent[1] > 0,
        "a fetch sent nothing, so the comparison below is vacuous: {sent:?}"
    );
    assert!(
        sent[0] <= sent[1],
        "the bitmap fetch sent {} objects where the walk sent {}; \
         the client is being sent history it already has",
        sent[0],
        sent[1]
    );
}

/// The same, on **sha256**, because the `.bitmap` header's checksum width
/// follows the repository hash and a hardcoded 20 parses a sha256 file into
/// garbage that still looks like an index.
fn v2_clone_with_a_bitmap_sha256() {
    let scene = Scene::new(Kind::Sha256);
    bitmap_the_origin(&scene);
    let target = clone_and_verify(&scene, ProtocolVersion::V2, "work");
    git_in(&target).args(["fsck", "--full", "--strict"]).ok();
}

/// The pack a bitmapped clone produces is one **stock `index-pack --strict`**
/// accepts on its own, away from the clone that would fix it up.
///
/// `git clone` runs `index-pack` itself, but with `--fix-thin`, which repairs a
/// pack whose deltas point at bases it does not carry. This asks the harsher
/// question, on the raw bytes: is the pack self-contained and internally
/// consistent as written? A wrong object *order* — a delta emitted before its
/// base — shows up here and nowhere above.
fn v2_bitmap_pack_passes_index_pack_strict() {
    let scene = Scene::new(Kind::Sha1);
    bitmap_the_origin(&scene);

    let bundle = scene.root.join("bundle");
    std::fs::create_dir_all(&bundle).expect("mkdir");
    // A clone into a scratch directory, then re-index the pack it received.
    let target = clone_and_verify(&scene, ProtocolVersion::V2, "work");
    let packs: Vec<std::path::PathBuf> = target
        .join(".git/objects/pack")
        .read_dir()
        .expect("pack dir")
        .filter_map(|entry| entry.ok())
        .map(|entry| entry.path())
        .filter(|path| path.extension().is_some_and(|ext| ext == "pack"))
        .collect();
    assert_eq!(
        packs.len(),
        1,
        "expected one received pack, found {packs:?}"
    );

    let (ok, _out, err) = git_in(&bundle)
        .args(["index-pack", "--strict", "-o"])
        .arg(bundle.join("copy.idx").to_str().unwrap())
        .arg(packs[0].to_str().unwrap())
        .run();
    assert!(
        ok,
        "index-pack --strict refused the bitmap clone's pack: {err}"
    );
}