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_surface.rs

//! **The rest of the git command surface, against the real `git` binary.**
//!
//! `real_git_interop` proves clone, fetch, negotiation, deepening and push.
//! This binary drives the same [`git_harness`] pipe over the commands and
//! options that suite never issues: the whole-namespace clones (`--bare`,
//! `--mirror`, `--reference`), pruning in both directions, the refspec forms
//! (`+`, globs, `HEAD:`), `refs/notes/*` and unusual ref namespaces, the two
//! object filters that had no end-to-end test, and `git bundle`.
//!
//! # The judge is stock git's own inventory
//!
//! Where a case claims "the repository came across", it is not enough that the
//! clone succeeded. [`assert_same_graph`] compares **every object id** from
//! `cat-file --batch-all-objects` and **every ref** from `for-each-ref`,
//! oid by oid, between the origin and the clone.
//!
//! Both halves carry a floor. A comparison of two empty repositories is equal,
//! and would let a server that shipped nothing pass every case in this file;
//! `min_objects` and `min_refs` are what make an empty fixture FAIL rather than
//! sail through. The floors are asserted against the **origin**, before the
//! comparison, so a fixture that quietly stopped building its own history is
//! reported as a broken fixture and not as a passing test.
//!
//! # Refusals are asserted against an acceptance
//!
//! A server that refuses everything passes any test that only checks a refusal.
//! Each case that asserts git was turned away also runs the **legitimate form
//! of the same request against the same server** and asserts it succeeds:
//! `push_non_fast_forward_needs_the_plus` rejects the bare refspec and then
//! accepts `+`, and the pruning cases assert the un-pruned fetch *leaves* the
//! ref that `--prune` then removes. Without the second half, "the ref is gone"
//! is equally true of a server that never sent it.

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

use std::path::{Path, PathBuf};

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

fn main() {
    serve_if_asked();

    let cases: &[(&str, fn())] = &[
        // whole-namespace clones
        ("clone_bare_holds_the_whole_graph", clone_bare),
        ("clone_mirror_holds_every_ref", clone_mirror),
        ("clone_reference_dissociate_is_complete", clone_reference),
        // pruning, in both directions
        ("fetch_prune_removes_a_deleted_branch", fetch_prune),
        ("fetch_prune_tags_removes_a_deleted_tag", fetch_prune_tags),
        ("push_mirror_makes_the_origin_match", push_mirror),
        ("push_prune_deletes_what_has_no_local_ref", push_prune),
        // refspec forms
        (
            "push_non_fast_forward_needs_the_plus",
            push_non_fast_forward,
        ),
        ("push_glob_refspec_moves_every_branch", push_glob_refspec),
        ("push_head_colon_ref_creates_the_branch", push_head_colon),
        ("fetch_glob_refspec_into_a_private_namespace", fetch_glob),
        // ref namespaces
        ("notes_round_trip_through_the_server", notes_round_trip),
        (
            "unusual_ref_namespaces_survive_a_round_trip",
            odd_namespaces,
        ),
        ("ls_remote_refs_hides_the_peeled_tag", ls_remote_refs),
        // the two filters that had no end-to-end test
        ("filter_tree_0_sends_no_trees_and_no_blobs", filter_tree_0),
        ("filter_tree_1_sends_the_root_tree_only", filter_tree_1),
        (
            "filter_blob_limit_sends_only_small_blobs",
            filter_blob_limit,
        ),
        (
            "RED_unsupported_filter_is_refused_not_ignored",
            red_unsupported_filter,
        ),
        // bundles
        ("bundle_from_a_gunnar_clone_rebuilds_the_graph", bundle),
        // empty repositories
        (
            "clone_of_an_empty_repository_succeeds_and_is_empty",
            clone_of_an_empty_repository_succeeds_and_is_empty,
        ),
        (
            "first_push_into_an_empty_repository",
            first_push_into_an_empty_repository,
        ),
        // content the server must not touch
        (
            "a_signed_tag_survives_verbatim",
            a_signed_tag_survives_verbatim,
        ),
        // not implemented, said out loud
        (
            "archive_over_the_protocol_is_refused_not_hung",
            archive_over_the_protocol_is_refused_not_hung,
        ),
        ("combined_filters_are_refused", combined_filters_are_refused),
    ];

    run_cases("real_git_surface", cases, 24);
}

// ── the judge ───────────────────────────────────────────────────────────────

/// Every object id in a repository, sorted: stock git's own inventory of what
/// it holds, not gunnar re-reading its own bytes.
fn all_objects(dir: &Path) -> Vec<String> {
    let out = git_in(dir)
        .args(["cat-file", "--batch-check", "--batch-all-objects"])
        .ok();
    let mut ids: Vec<String> = out
        .lines()
        // `<oid> <type> <size>`; the size is dropped because a repacked copy
        // may store the same object at a different on-disk size, while the id
        // and the type are what "the same graph" means.
        .filter_map(|line| {
            let mut parts = line.split_whitespace();
            let id = parts.next()?;
            let kind = parts.next()?;
            Some(format!("{id} {kind}"))
        })
        .collect();
    ids.sort();
    ids
}

/// Every reference and the object it resolves to, sorted.
///
/// `%(objectname)` rather than the peeled id, so an annotated tag compares as
/// the tag object it is. A comparison that peeled would call a repository that
/// lost its tag objects equal to one that kept them.
fn all_refs(dir: &Path) -> Vec<String> {
    let out = git_in(dir)
        .args(["for-each-ref", "--format=%(refname) %(objectname)"])
        .ok();
    let mut refs: Vec<String> = out.lines().map(str::to_owned).collect();
    refs.sort();
    refs
}

/// The two repositories hold the same objects and the same references.
///
/// The floors are the whole point: `assert_eq!(vec![], vec![])` passes, so
/// without them every case here would be green against a server that sent
/// nothing at all. They are checked against `origin` first, so a fixture that
/// stopped building its history fails as a broken fixture rather than passing
/// as a clean comparison.
fn assert_same_graph(
    label: &str,
    origin: &Path,
    clone: &Path,
    min_objects: usize,
    min_refs: usize,
) {
    let origin_objects = all_objects(origin);
    let origin_refs = all_refs(origin);
    assert!(
        origin_objects.len() >= min_objects,
        "{label}: the ORIGIN holds {} objects, fewer than the floor of {min_objects} — \
         the fixture is broken, and an empty fixture would make this comparison vacuous",
        origin_objects.len()
    );
    assert!(
        origin_refs.len() >= min_refs,
        "{label}: the ORIGIN holds {} refs, fewer than the floor of {min_refs} — \
         the fixture is broken, and a repository with no refs compares equal to anything",
        origin_refs.len()
    );

    let clone_objects = all_objects(clone);
    let clone_refs = all_refs(clone);

    if origin_objects != clone_objects {
        let missing: Vec<_> = origin_objects
            .iter()
            .filter(|id| !clone_objects.contains(id))
            .take(10)
            .collect();
        let extra: Vec<_> = clone_objects
            .iter()
            .filter(|id| !origin_objects.contains(id))
            .take(10)
            .collect();
        panic!(
            "{label}: the object graphs differ — origin has {} objects, the clone {}.\n\
             missing from the clone: {missing:#?}\nextra in the clone: {extra:#?}",
            origin_objects.len(),
            clone_objects.len()
        );
    }
    assert_eq!(
        origin_refs, clone_refs,
        "{label}: the reference sets differ"
    );
}

/// The refs a repository holds, as bare names.
fn ref_names(dir: &Path) -> Vec<String> {
    git_in(dir)
        .args(["for-each-ref", "--format=%(refname)"])
        .ok()
        .lines()
        .map(str::to_owned)
        .collect()
}

fn has_ref(dir: &Path, name: &str) -> bool {
    ref_names(dir).iter().any(|line| line == name)
}

/// Every object of one type, by id.
fn objects_of_kind(dir: &Path, kind: &str) -> Vec<String> {
    all_objects(dir)
        .into_iter()
        .filter(|line| line.ends_with(&format!(" {kind}")))
        .collect()
}

// ── whole-namespace clones ──────────────────────────────────────────────────

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

    // A bare clone maps `refs/heads/*` to `refs/heads/*` and keeps tags, so
    // its ref set is the origin's. Its object graph must be the origin's too.
    assert_same_graph("clone --bare", &scene.origin, &target, 12, 3);
    git_in(&target).args(["fsck", "--no-progress"]).ok();
}

fn clone_mirror() {
    let scene = Scene::new(Kind::Sha1);

    // A ref stock git would never put in `refs/heads` or `refs/tags`. `--bare`
    // would not carry it; `--mirror` must.
    seed_extra_ref(&scene, "refs/custom/thing");

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

    assert_same_graph("clone --mirror", &scene.origin, &target, 12, 4);
    assert!(
        has_ref(&target, "refs/custom/thing"),
        "a mirror clone dropped the ref outside refs/heads and refs/tags: {:#?}",
        ref_names(&target)
    );
}

/// A clone whose objects the client **already has**, taken through the
/// negotiation path and then cut loose from its reference.
///
/// `--dissociate` is what makes this a real assertion: without it the second
/// clone borrows the first one's objects through `alternates`, and a server
/// that sent nothing at all would still produce a repository that passes
/// `fsck`. With it, git must end up holding every object itself.
fn clone_reference() {
    let scene = Scene::new(Kind::Sha1);
    let first = scene.root.join("first.git");
    scene
        .client(ProtocolVersion::V2)
        .args(["clone", "-q", "--bare"])
        .arg(scene.url())
        .arg(first.to_str().unwrap())
        .ok();

    let second = scene.root.join("second.git");
    scene.clear_log();
    scene
        .client(ProtocolVersion::V2)
        .args(["clone", "-q", "--bare", "--dissociate", "--reference"])
        .arg(first.to_str().unwrap())
        .arg(scene.url())
        .arg(second.to_str().unwrap())
        .ok();

    assert!(
        !second.join("objects/info/alternates").exists(),
        "--dissociate left the alternates file in place, so this proves nothing"
    );
    assert_same_graph(
        "clone --reference --dissociate",
        &scene.origin,
        &second,
        12,
        3,
    );
}

// ── pruning ─────────────────────────────────────────────────────────────────

/// `--prune` removes a remote-tracking branch whose upstream is gone.
///
/// Discriminating: the plain fetch immediately before it is asserted to
/// **leave** the ref. Without that half, "the ref is absent" would also be true
/// of a server that never advertised it.
fn fetch_prune() {
    let scene = Scene::new(Kind::Sha1);
    let target = clone_and_verify(&scene, ProtocolVersion::V2, "work");
    assert!(
        has_ref(&target, "refs/remotes/origin/side"),
        "the clone never tracked refs/remotes/origin/side, so there is nothing to prune"
    );

    // Delete the branch on the origin directly, not through gunnar: what is
    // under test is the fetch, not the delete.
    scene
        .origin_git()
        .args(["update-ref", "-d", "refs/heads/side"])
        .ok();

    scene.clear_log();
    scene
        .client(ProtocolVersion::V2)
        .cwd(&target)
        .args(["fetch", "-q", "origin"])
        .ok();
    assert!(
        has_ref(&target, "refs/remotes/origin/side"),
        "a fetch WITHOUT --prune already removed refs/remotes/origin/side, so the \
         --prune assertion below would be vacuous"
    );

    scene
        .client(ProtocolVersion::V2)
        .cwd(&target)
        .args(["fetch", "-q", "--prune", "origin"])
        .ok();
    assert!(
        !has_ref(&target, "refs/remotes/origin/side"),
        "--prune left refs/remotes/origin/side behind: {:#?}",
        ref_names(&target)
    );
    // And it pruned only what was gone.
    assert!(
        has_ref(&target, "refs/remotes/origin/main"),
        "--prune took refs/remotes/origin/main with it"
    );
}

/// `--prune-tags` is a different question from `--prune`, and the case asserts
/// the difference rather than assuming it: a plain `--prune` is required to
/// leave the tag, because tags do not live under the remote's refspec.
fn fetch_prune_tags() {
    let scene = Scene::new(Kind::Sha1);
    let target = clone_and_verify(&scene, ProtocolVersion::V2, "work");
    assert!(
        has_ref(&target, "refs/tags/v1"),
        "the clone has no tag to prune"
    );

    scene
        .origin_git()
        .args(["update-ref", "-d", "refs/tags/v1"])
        .ok();

    scene.clear_log();
    scene
        .client(ProtocolVersion::V2)
        .cwd(&target)
        .args(["fetch", "-q", "--prune", "origin"])
        .ok();
    assert!(
        has_ref(&target, "refs/tags/v1"),
        "--prune alone removed refs/tags/v1; then --prune-tags below proves nothing"
    );

    scene
        .client(ProtocolVersion::V2)
        .cwd(&target)
        .args(["fetch", "-q", "--prune", "--prune-tags", "origin"])
        .ok();
    assert!(
        !has_ref(&target, "refs/tags/v1"),
        "--prune-tags left refs/tags/v1 behind: {:#?}",
        ref_names(&target)
    );
}

/// `push --mirror` makes the origin's ref namespace **exactly** the local one,
/// which means it has to carry deletions as well as creations.
fn push_mirror() {
    let scene = Scene::new(Kind::Sha1);
    let mirror = scene.root.join("mirror.git");
    scene
        .client(ProtocolVersion::V2)
        .args(["clone", "-q", "--mirror"])
        .arg(scene.url())
        .arg(mirror.to_str().unwrap())
        .ok();

    // One ref appears, one disappears. A push that only creates would pass a
    // test that only looked for the new branch.
    let main = scene.origin_ref("refs/heads/main");
    git_in(&mirror)
        .args(["update-ref", "refs/heads/added", &main])
        .ok();
    git_in(&mirror)
        .args(["update-ref", "-d", "refs/heads/side"])
        .ok();

    scene.clear_log();
    scene
        .client(ProtocolVersion::V0)
        .cwd(&mirror)
        .args(["push", "-q", "--mirror", "origin"])
        .ok();

    assert_eq!(
        all_refs(&scene.origin),
        all_refs(&mirror),
        "after push --mirror the origin's refs are not the mirror's"
    );
    assert!(
        has_ref(&scene.origin, "refs/heads/added"),
        "push --mirror did not create refs/heads/added"
    );
    assert!(
        !has_ref(&scene.origin, "refs/heads/side"),
        "push --mirror did not delete refs/heads/side"
    );
    scene.origin_git().args(["fsck", "--no-progress"]).ok();
}

/// `push --prune` with an explicit refspec deletes the remote refs the refspec
/// covers and the local side no longer has.
fn push_prune() {
    let scene = Scene::new(Kind::Sha1);
    let target = clone_and_verify(&scene, ProtocolVersion::V2, "work");

    // A branch that exists only on the origin, under the refspec's reach.
    let main = scene.origin_ref("refs/heads/main");
    scene
        .origin_git()
        .args(["update-ref", "refs/heads/doomed", &main])
        .ok();

    scene.clear_log();
    scene
        .client(ProtocolVersion::V0)
        .cwd(&target)
        .args([
            "push",
            "-q",
            "--prune",
            "origin",
            "refs/heads/*:refs/heads/*",
        ])
        .ok();

    assert!(
        !has_ref(&scene.origin, "refs/heads/doomed"),
        "push --prune left refs/heads/doomed behind: {:#?}",
        ref_names(&scene.origin)
    );
    // Discriminating: it deleted the unmatched ref and NOT the matched ones.
    assert!(
        has_ref(&scene.origin, "refs/heads/main"),
        "push --prune deleted refs/heads/main, which the local side still has"
    );
}

// ── refspec forms ───────────────────────────────────────────────────────────

/// The `+` is what makes a non-fast-forward legal, and this asserts both
/// halves: the same push is **refused** without it and **accepted** with it,
/// against the same server, one after the other.
fn push_non_fast_forward() {
    let scene = Scene::new(Kind::Sha1);
    let target = clone_and_verify(&scene, ProtocolVersion::V2, "work");

    let before = scene.origin_ref("refs/heads/main");
    // Rewind to a commit that is not a descendant, so the push is genuinely a
    // non-fast-forward rather than a no-op.
    git_in(&target)
        .args(["reset", "-q", "--hard", "HEAD~2"])
        .ok();
    let rewound = git_in(&target).args(["rev-parse", "HEAD"]).ok();
    assert_ne!(
        rewound, before,
        "the reset did not move HEAD, so nothing is being tested"
    );

    scene.clear_log();
    let (ok, _, stderr) = scene
        .client(ProtocolVersion::V0)
        .cwd(&target)
        .args(["push", "origin", "refs/heads/main:refs/heads/main"])
        .run();
    assert!(
        !ok,
        "a non-fast-forward push without + was accepted:\n{stderr}"
    );
    assert_eq!(
        scene.origin_ref("refs/heads/main"),
        before,
        "the refused push moved the ref anyway"
    );

    // The acceptance half. Without it, the refusal above is equally true of a
    // server that refuses every push.
    scene
        .client(ProtocolVersion::V0)
        .cwd(&target)
        .args(["push", "-q", "origin", "+refs/heads/main:refs/heads/main"])
        .ok();
    assert_eq!(
        scene.origin_ref("refs/heads/main"),
        rewound,
        "the forced push did not move the ref"
    );
    scene.origin_git().args(["fsck", "--no-progress"]).ok();
}

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

    // Three local branches the glob must carry in one push.
    for name in ["alpha", "beta", "gamma"] {
        git_in(&target)
            .args(["branch", name, "refs/remotes/origin/main"])
            .ok();
    }

    scene.clear_log();
    scene
        .client(ProtocolVersion::V0)
        .cwd(&target)
        .args(["push", "-q", "origin", "refs/heads/*:refs/heads/mirrored/*"])
        .ok();

    for name in ["alpha", "beta", "gamma"] {
        assert!(
            has_ref(&scene.origin, &format!("refs/heads/mirrored/{name}")),
            "the glob refspec did not create refs/heads/mirrored/{name}: {:#?}",
            ref_names(&scene.origin)
        );
    }
    scene.origin_git().args(["fsck", "--no-progress"]).ok();
}

fn push_head_colon() {
    let scene = Scene::new(Kind::Sha1);
    let target = clone_and_verify(&scene, ProtocolVersion::V2, "work");
    commit(
        &target,
        "head-colon.txt",
        "content",
        "a commit to push by HEAD",
    );
    let local = git_in(&target).args(["rev-parse", "HEAD"]).ok();

    scene.clear_log();
    scene
        .client(ProtocolVersion::V0)
        .cwd(&target)
        .args(["push", "-q", "origin", "HEAD:refs/heads/from-head"])
        .ok();

    assert_eq!(
        scene.origin_ref("refs/heads/from-head"),
        local,
        "HEAD:refs/heads/from-head did not land on the commit HEAD names"
    );
    scene.origin_git().args(["fsck", "--no-progress"]).ok();
}

/// A fetch refspec into a namespace no default clone would use, which is the
/// case that catches a server that only ever answers the refs it advertised
/// under their advertised names.
fn fetch_glob() {
    let scene = Scene::new(Kind::Sha1);
    let target = scene.root.join("private");
    Git::new()
        .cwd(&scene.root)
        .args(["init", "-q", "--bare"])
        .arg(target.to_str().unwrap())
        .ok();

    scene.clear_log();
    scene
        .client(ProtocolVersion::V2)
        .cwd(&target)
        .args(["fetch", "-q"])
        .arg(scene.url())
        .arg("refs/heads/*:refs/private/upstream/*")
        .ok();

    for name in ["main", "side"] {
        assert!(
            has_ref(&target, &format!("refs/private/upstream/{name}")),
            "the fetch refspec did not create refs/private/upstream/{name}: {:#?}",
            ref_names(&target)
        );
    }
    // And it created nothing under the names the server advertised.
    assert!(
        !has_ref(&target, "refs/heads/main"),
        "the fetch wrote refs/heads/main, which the refspec did not ask for"
    );
    git_in(&target).args(["fsck", "--no-progress"]).ok();
}

// ── ref namespaces ──────────────────────────────────────────────────────────

/// `refs/notes/*` is an ordinary ref namespace carrying an ordinary commit, and
/// that is exactly why it is worth a case: a server that special-cases
/// `refs/heads` and `refs/tags` serves it wrong.
fn notes_round_trip() {
    let scene = Scene::new(Kind::Sha1);
    let target = clone_and_verify(&scene, ProtocolVersion::V2, "work");

    git_in(&target)
        .args(["notes", "add", "-m", "a note on the tip", "HEAD"])
        .ok();
    let note_ref = git_in(&target)
        .args(["rev-parse", "refs/notes/commits"])
        .ok();

    scene.clear_log();
    scene
        .client(ProtocolVersion::V0)
        .cwd(&target)
        .args(["push", "-q", "origin", "refs/notes/*:refs/notes/*"])
        .ok();
    assert_eq!(
        scene.origin_ref("refs/notes/commits"),
        note_ref,
        "the notes ref did not arrive at the origin"
    );

    // The server must *advertise* it, named rather than inferred from the
    // fetch below succeeding: a server that hides the namespace fails the
    // fetch with "couldn't find remote ref", which is a true failure reported
    // as the wrong thing.
    let advertised = scene
        .client(ProtocolVersion::V2)
        .args(["ls-remote"])
        .arg(scene.url())
        .ok();
    assert!(
        advertised.contains("refs/notes/commits"),
        "the server does not advertise refs/notes/commits:\n{advertised}"
    );

    // And back out again, into a fresh repository, with the note readable.
    let readback = scene.root.join("readback");
    scene
        .client(ProtocolVersion::V2)
        .args(["clone", "-q"])
        .arg(scene.url())
        .arg(readback.to_str().unwrap())
        .ok();
    scene
        .client(ProtocolVersion::V2)
        .cwd(&readback)
        .args(["fetch", "-q", "origin", "refs/notes/*:refs/notes/*"])
        .ok();
    let text = git_in(&readback).args(["notes", "show", "HEAD"]).ok();
    assert_eq!(
        text.trim(),
        "a note on the tip",
        "the note did not survive the round trip"
    );
}

/// Legal reference names that no default clone produces: deep nesting, a
/// namespace outside `refs/heads` and `refs/tags`, and the `refs/pull/N/head`
/// shape forges use.
fn odd_namespaces() {
    const ODD: &[&str] = &[
        "refs/heads/feature/deep/nested/branch",
        "refs/pull/42/head",
        "refs/custom/thing",
        "refs/heads/dash-and.dots_and+plus",
    ];

    let scene = Scene::new(Kind::Sha1);
    let target = clone_and_verify(&scene, ProtocolVersion::V2, "work");
    let tip = git_in(&target).args(["rev-parse", "HEAD"]).ok();

    scene.clear_log();
    let mut push = scene
        .client(ProtocolVersion::V0)
        .cwd(&target)
        .args(["push", "-q", "origin"]);
    for name in ODD {
        push = push.arg(format!("HEAD:{name}"));
    }
    push.ok();

    for name in ODD {
        assert_eq!(
            &scene.origin_ref(name),
            &tip,
            "the origin does not hold {name} at the pushed tip: {:#?}",
            ref_names(&scene.origin)
        );
    }

    // A mirror clone must bring every one of them back.
    let back = scene.root.join("odd-mirror.git");
    scene
        .client(ProtocolVersion::V2)
        .args(["clone", "-q", "--mirror"])
        .arg(scene.url())
        .arg(back.to_str().unwrap())
        .ok();
    for name in ODD {
        assert!(
            has_ref(&back, name),
            "a mirror clone lost {name}: {:#?}",
            ref_names(&back)
        );
    }
    assert_same_graph("odd namespaces mirror", &scene.origin, &back, 12, 7);
}

/// `--refs` is the filter that hides both `HEAD` and the peeled `^{}` lines.
/// The interop suite asserts the peeled line is *present* without it; this
/// asserts it is *absent* with it, which is the half that catches an
/// advertisement the server builds the same way every time.
fn ls_remote_refs() {
    let scene = Scene::new(Kind::Sha1);

    scene.clear_log();
    let full = scene
        .client(ProtocolVersion::V2)
        .args(["ls-remote", "--symref"])
        .arg(scene.url())
        .ok();
    assert!(
        full.contains("refs/tags/v1^{}"),
        "the unfiltered ls-remote has no peeled tag, so --refs hiding one proves nothing:\n{full}"
    );
    assert!(
        full.lines().any(|line| line.ends_with("\tHEAD")),
        "the unfiltered ls-remote has no HEAD line:\n{full}"
    );

    let refs_only = scene
        .client(ProtocolVersion::V2)
        .args(["ls-remote", "--refs"])
        .arg(scene.url())
        .ok();
    assert!(
        !refs_only.contains("^{}"),
        "ls-remote --refs still reported a peeled tag:\n{refs_only}"
    );
    assert!(
        !refs_only.lines().any(|line| line.ends_with("\tHEAD")),
        "ls-remote --refs still reported HEAD:\n{refs_only}"
    );
    assert!(
        refs_only.contains("refs/tags/v1"),
        "ls-remote --refs dropped the tag itself, not just its peeled line:\n{refs_only}"
    );
}

// ── object filters ──────────────────────────────────────────────────────────

/// A fixture with **nested trees and two blob sizes**, which is what the tree
/// and size filters need and [`Scene::new`] is not.
///
/// `Scene::new` puts one small file per commit at the root: against it,
/// `tree:0`, `tree:1` and `blob:limit=` all produce the same answer as
/// `blob:none`, and a server that implemented one and aliased the rest would
/// pass every case. This shape makes the three disagree.
///
/// ```text
///   small.txt          16 bytes      at depth 1 (the root tree)
///   large.bin          40960 bytes   at depth 1
///   deep/inner.txt     16 bytes      at depth 2
///   deep/sub/leaf.txt  16 bytes      at depth 3
/// ```
fn layered(scene_root: &Path) -> (PathBuf, PathBuf) {
    let work = scene_root.join("layered-seed");
    Git::new()
        .cwd(scene_root)
        .args(["init", "-q", "-b", "main"])
        .arg(work.to_str().unwrap())
        .ok();

    std::fs::create_dir_all(work.join("deep/sub")).unwrap();
    std::fs::write(work.join("small.txt"), "small contents\n").unwrap();
    // Incompressible-ish and comfortably over any limit the case picks, so
    // "large" is large by the size git reports and not by luck.
    let large: String = (0..LARGE_LINES)
        .map(|i| format!("large blob line {i:06} padding padding padding\n"))
        .collect();
    std::fs::write(work.join("large.bin"), &large).unwrap();
    std::fs::write(work.join("deep/inner.txt"), "inner contents\n").unwrap();
    std::fs::write(work.join("deep/sub/leaf.txt"), "leaf contents\n").unwrap();
    git_in(&work).args(["add", "-A"]).ok();
    git_in(&work)
        .args(["commit", "-q", "-m", "the layered tree"])
        .ok();

    let origin = scene_root.join("layered.git");
    Git::new()
        .cwd(scene_root)
        .args(["clone", "-q", "--bare"])
        .arg(work.to_str().unwrap())
        .arg(origin.to_str().unwrap())
        .ok();
    (work, origin)
}

/// Lines in `large.bin`. ~2 KiB per 40 lines; 800 lines is ~40 KiB, far above
/// the 1 KiB limit the size case uses and far below anything that would make
/// the fixture expensive.
const LARGE_LINES: usize = 800;

/// A [`Scene`] rebuilt around the layered fixture, so [`Scene::url`] serves it.
fn layered_scene() -> Scene {
    let mut scene = Scene::new(Kind::Sha1);
    let (_work, origin) = layered(&scene.root);
    scene.origin = origin;
    scene
}

fn filter_tree_0() {
    let scene = layered_scene();
    let target = scene.root.join("tree0");
    scene.clear_log();
    scene
        .client(ProtocolVersion::V2)
        .args(["clone", "-q", "--no-checkout", "--filter=tree:0"])
        .arg(scene.url())
        .arg(target.to_str().unwrap())
        .ok();

    // The fixture must actually contain what the filter is supposed to remove,
    // or "no trees arrived" is true of a repository that never had any.
    assert!(
        objects_of_kind(&scene.origin, "tree").len() >= 3,
        "the layered fixture has fewer than three trees, so tree:0 proves nothing"
    );

    let trees = objects_of_kind(&target, "tree");
    let blobs = objects_of_kind(&target, "blob");
    assert!(
        trees.is_empty(),
        "a tree:0 clone received {} trees, so the filter did nothing: {trees:#?}",
        trees.len()
    );
    assert!(
        blobs.is_empty(),
        "a tree:0 clone received {} blobs; no blob is reachable without a tree",
        blobs.len()
    );
    assert!(
        !objects_of_kind(&target, "commit").is_empty(),
        "a tree:0 clone received no commits either, so it received nothing at all"
    );
}

/// `tree:1` is the case that separates a real depth filter from `tree:0`
/// wearing a different name: the root tree arrives, the two below it do not.
fn filter_tree_1() {
    let scene = layered_scene();
    let target = scene.root.join("tree1");
    scene.clear_log();
    scene
        .client(ProtocolVersion::V2)
        .args(["clone", "-q", "--no-checkout", "--filter=tree:1"])
        .arg(scene.url())
        .arg(target.to_str().unwrap())
        .ok();

    let origin_trees = objects_of_kind(&scene.origin, "tree").len();
    assert!(
        origin_trees >= 3,
        "the layered fixture has {origin_trees} trees; tree:1 needs at least three to be \
         distinguishable from tree:0"
    );

    let trees = objects_of_kind(&target, "tree");
    assert_eq!(
        trees.len(),
        1,
        "a tree:1 clone should hold exactly the root tree, and holds {}: {trees:#?}",
        trees.len()
    );
}

fn filter_blob_limit() {
    let scene = layered_scene();
    let target = scene.root.join("bloblimit");
    scene.clear_log();
    scene
        .client(ProtocolVersion::V2)
        .args(["clone", "-q", "--no-checkout", "--filter=blob:limit=1k"])
        .arg(scene.url())
        .arg(target.to_str().unwrap())
        .ok();

    // The fixture straddles the limit, asserted rather than assumed.
    let sizes = blob_sizes(&scene.origin);
    assert!(
        sizes.iter().any(|size| *size <= 1024),
        "the fixture has no blob at or under 1 KiB, so the limit would drop everything"
    );
    assert!(
        sizes.iter().any(|size| *size > 1024),
        "the fixture has no blob over 1 KiB, so the limit would drop nothing"
    );

    let kept = blob_sizes(&target);
    assert!(
        !kept.is_empty(),
        "a blob:limit=1k clone received no blobs at all; the small ones should have come"
    );
    let oversize: Vec<_> = kept.iter().filter(|size| **size > 1024).collect();
    assert!(
        oversize.is_empty(),
        "a blob:limit=1k clone received blobs of {oversize:?} bytes, over the 1024 limit"
    );
    // Trees are not blobs: the limit must not have taken them too.
    assert!(
        !objects_of_kind(&target, "tree").is_empty(),
        "a blob:limit clone received no trees, which no blob filter should affect"
    );
}

/// Every blob's size, from git's own `--batch-check`.
fn blob_sizes(dir: &Path) -> Vec<u64> {
    git_in(dir)
        .args(["cat-file", "--batch-check", "--batch-all-objects"])
        .ok()
        .lines()
        .filter_map(|line| {
            let mut parts = line.split_whitespace();
            let _id = parts.next()?;
            if parts.next()? != "blob" {
                return None;
            }
            parts.next()?.parse().ok()
        })
        .collect()
}

/// A filter gunnar does not implement must be **refused**, not accepted and
/// ignored — the failure `filter.rs` names in its own module docs.
///
/// Discriminating twice over: a supported filter is run against the same server
/// first and asserted to succeed, so this is not a server that refuses every
/// filter, and the refused clone is asserted to have left no repository behind.
fn red_unsupported_filter() {
    let scene = layered_scene();

    // The acceptance half, first.
    let good = scene.root.join("supported");
    scene
        .client(ProtocolVersion::V2)
        .args(["clone", "-q", "--no-checkout", "--filter=blob:none"])
        .arg(scene.url())
        .arg(good.to_str().unwrap())
        .ok();
    assert!(
        !objects_of_kind(&good, "commit").is_empty(),
        "the supported filter produced an empty clone, so the refusal below proves nothing"
    );

    // `sparse:oid` is a real git filter gunnar does not implement.
    let bad = scene.root.join("unsupported");
    scene.clear_log();
    let (ok, _, stderr) = scene
        .client(ProtocolVersion::V2)
        .args([
            "clone",
            "-q",
            "--no-checkout",
            "--filter=sparse:oid=HEAD:sparse",
        ])
        .arg(scene.url())
        .arg(bad.to_str().unwrap())
        .run();
    assert!(
        !ok,
        "a clone with an unimplemented filter SUCCEEDED — the filter was accepted and \
         ignored, which is the exact failure filter.rs warns about:\n{stderr}"
    );
}

// ── bundles ─────────────────────────────────────────────────────────────────

/// The graph gunnar served has to be **self-contained**, and a bundle is the
/// sharpest way to say so: `git bundle create --all` walks every ref, `verify`
/// checks the prerequisites, and a clone from the bundle must rebuild the
/// origin's object graph without the server in the loop at all.
fn bundle() {
    let scene = Scene::new(Kind::Sha1);
    let source = scene.root.join("bundle-source.git");
    scene
        .client(ProtocolVersion::V2)
        .args(["clone", "-q", "--mirror"])
        .arg(scene.url())
        .arg(source.to_str().unwrap())
        .ok();

    let file = scene.root.join("all.bundle");
    git_in(&source)
        .args(["bundle", "create", "-q"])
        .arg(file.to_str().unwrap())
        .arg("--all")
        .ok();
    assert!(
        std::fs::metadata(&file).map(|m| m.len()).unwrap_or(0) > 0,
        "git bundle create produced an empty file"
    );

    let verify = git_in(&source)
        .args(["bundle", "verify"])
        .arg(file.to_str().unwrap())
        .ok();
    assert!(
        verify.contains("is okay") || verify.contains("The bundle"),
        "git bundle verify did not report the bundle as usable:\n{verify}"
    );

    let rebuilt = scene.root.join("from-bundle.git");
    Git::new()
        .cwd(&scene.root)
        .args(["clone", "-q", "--mirror"])
        .arg(file.to_str().unwrap())
        .arg(rebuilt.to_str().unwrap())
        .ok();

    assert_same_graph("bundle round trip", &scene.origin, &rebuilt, 12, 3);
}

// ── fixture helpers ─────────────────────────────────────────────────────────

/// Put a reference outside `refs/heads` and `refs/tags` on the origin.
fn seed_extra_ref(scene: &Scene, name: &str) {
    let main = scene.origin_ref("refs/heads/main");
    scene.origin_git().args(["update-ref", name, &main]).ok();
}

// ── empty repositories ──────────────────────────────────────────────────────

/// A [`Scene`] whose origin has **no references and no objects**.
///
/// The state every repository starts in, and the one most likely to divide by
/// zero: the advertisement has nothing to advertise and the first push has no
/// old value to compare against.
fn empty_scene() -> Scene {
    let mut scene = Scene::new(Kind::Sha1);
    let origin = scene.root.join("empty.git");
    Git::new()
        .cwd(&scene.root)
        .args(["init", "-q", "--bare", "-b", "main"])
        .arg(origin.to_str().unwrap())
        .ok();
    scene.origin = origin;
    scene
}

fn clone_of_an_empty_repository_succeeds_and_is_empty() {
    let scene = empty_scene();
    assert!(
        ref_names(&scene.origin).is_empty(),
        "the fixture origin is not empty: {:#?}",
        ref_names(&scene.origin)
    );

    let target = scene.root.join("empty-clone");
    scene.clear_log();
    let (ok, _, stderr) = scene
        .client(ProtocolVersion::V2)
        .args(["clone", "-q"])
        .arg(scene.url())
        .arg(target.to_str().unwrap())
        .run();
    assert!(ok, "cloning an empty repository failed:\n{stderr}");

    assert!(
        all_objects(&target).is_empty(),
        "an empty repository produced a clone holding {:?}",
        all_objects(&target)
    );
    assert!(ref_names(&target).is_empty());
    // It must be a usable repository, not a directory that happens to exist.
    git_in(&target).args(["fsck", "--no-progress"]).ok();
}

/// The first push a repository ever receives: every command has a null old
/// value, and there is no history to make the pack thin against.
fn first_push_into_an_empty_repository() {
    let scene = empty_scene();
    let work = scene.root.join("first-work");
    Git::new()
        .cwd(&scene.root)
        .args(["init", "-q", "-b", "main"])
        .arg(work.to_str().unwrap())
        .ok();
    commit(&work, "first.txt", "hello", "the very first commit");
    let tip = git_in(&work).args(["rev-parse", "HEAD"]).ok();

    scene.clear_log();
    scene
        .client(ProtocolVersion::V0)
        .cwd(&work)
        .args(["push", "-q"])
        .arg(scene.url())
        .arg("refs/heads/main:refs/heads/main")
        .ok();

    assert_eq!(
        scene.origin_ref("refs/heads/main"),
        tip,
        "the first push did not land"
    );
    scene.origin_git().args(["fsck", "--no-progress"]).ok();

    // And it is servable back out, which is the half that catches a push
    // that wrote refs without writing objects.
    // --mirror, not a plain clone: a working clone also holds
    // refs/remotes/origin/*, which the origin does not, and comparing the two
    // ref sets would fail on the difference between a clone and its origin
    // rather than on anything the server did.
    let back = scene.root.join("first-back.git");
    scene
        .client(ProtocolVersion::V2)
        .args(["clone", "-q", "--mirror"])
        .arg(scene.url())
        .arg(back.to_str().unwrap())
        .ok();
    assert_same_graph("first push", &scene.origin, &back, 3, 1);
}

// ── object content the server must not touch ────────────────────────────────

/// **A signed tag's bytes must survive verbatim.**
///
/// A signature is only worth anything if the object it covers comes back
/// byte-for-byte, and a tag object is the one place a server is tempted to
/// re-serialise: it parses tags to peel them for the advertisement. This pushes
/// a tag carrying a PGP signature block through gunnar and asserts the object
/// id is unchanged on the far side — which, the id being the hash of the
/// content, is the strongest available statement that not one byte moved.
///
/// The signature is crafted rather than made with `git tag -s`, so the case
/// needs no key on the machine. gunnar cannot tell the difference: nothing in
/// the server verifies signatures, and that is exactly the property under test.
fn a_signed_tag_survives_verbatim() {
    let scene = Scene::new(Kind::Sha1);
    let work = scene.root.join("seed");
    let target = scene.origin_ref("refs/heads/main");

    let tag_object = format!(
        "object {target}\n\
         type commit\n\
         tag signed-release\n\
         tagger Gunnar Test <gunnar@example.invalid> 1700000100 +0000\n\
         \n\
         a release that carries a signature\n\
         -----BEGIN PGP SIGNATURE-----\n\
         \n\
         iQEzBAABCAAdFiEE0000000000000000000000000000BQJhAAAACgkQ0000000000\n\
         AAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n\
         =AAAA\n\
         -----END PGP SIGNATURE-----\n"
    );
    let tag_path = scene.root.join("tag-object.txt");
    std::fs::write(&tag_path, &tag_object).unwrap();

    let tag_id = git_in(&work)
        .args(["hash-object", "-t", "tag", "-w"])
        .arg(tag_path.to_str().unwrap())
        .ok();
    let tag_id = tag_id.trim().to_owned();
    assert_eq!(
        tag_id.len(),
        40,
        "the tag object was not written: {tag_id:?}"
    );
    git_in(&work)
        .args(["update-ref", "refs/tags/signed-release", &tag_id])
        .ok();

    scene.clear_log();
    scene
        .client(ProtocolVersion::V0)
        .cwd(&work)
        .args(["push", "-q"])
        .arg(scene.url())
        .arg("refs/tags/signed-release:refs/tags/signed-release")
        .ok();

    // Same id on the server: the content is identical, signature included.
    assert_eq!(
        scene.origin_ref("refs/tags/signed-release"),
        tag_id,
        "the tag object's id changed in transit, so its bytes did not survive"
    );

    // And the signature is still readable after a round trip back out.
    let back = scene.root.join("signed-back");
    scene
        .client(ProtocolVersion::V2)
        .args(["clone", "-q", "--mirror"])
        .arg(scene.url())
        .arg(back.to_str().unwrap())
        .ok();
    let body = git_in(&back)
        .args(["cat-file", "tag", "refs/tags/signed-release"])
        .ok();
    assert!(
        body.contains("-----BEGIN PGP SIGNATURE-----")
            && body.contains("-----END PGP SIGNATURE-----"),
        "the signature block did not survive the round trip:\n{body}"
    );
    assert_eq!(
        git_in(&back)
            .args(["rev-parse", "refs/tags/signed-release"])
            .ok(),
        tag_id,
        "the cloned tag has a different id"
    );
}

// ── what gunnar does not implement, said out loud ───────────────────────────

/// `git archive --remote` asks for the v2 `git-upload-archive` service, which
/// gunnar does not implement.
///
/// Recorded as a case rather than left unstated: the requirement is that an
/// unimplemented service is **refused**, because the alternative — a server
/// that accepts the request and answers nothing — is a client that hangs. The
/// acceptance half proves the same server is reachable and serving.
fn archive_over_the_protocol_is_refused_not_hung() {
    let scene = Scene::new(Kind::Sha1);

    // Reachable and serving, first.
    scene
        .client(ProtocolVersion::V2)
        .args(["ls-remote"])
        .arg(scene.url())
        .ok();

    let (ok, stdout, stderr) = scene
        .client(ProtocolVersion::V2)
        .args(["archive", "--format=tar", "--remote"])
        .arg(scene.url())
        .arg("HEAD")
        .run();
    assert!(
        !ok,
        "git archive --remote SUCCEEDED against a server that implements no \
         upload-archive service:\nstdout {} bytes\n{stderr}",
        stdout.len()
    );
}

/// `combine:` is a real git filter gunnar refuses by name, and the refusal has
/// to reach the client rather than be ignored.
fn combined_filters_are_refused() {
    let scene = Scene::new(Kind::Sha1);

    // A single filter works against this server.
    let good = scene.root.join("single-filter");
    scene
        .client(ProtocolVersion::V2)
        .args(["clone", "-q", "--no-checkout", "--filter=blob:none"])
        .arg(scene.url())
        .arg(good.to_str().unwrap())
        .ok();
    assert!(!all_objects(&good).is_empty(), "the control clone is empty");

    let bad = scene.root.join("combined-filter");
    let (ok, _, stderr) = scene
        .client(ProtocolVersion::V2)
        .args([
            "clone",
            "-q",
            "--no-checkout",
            "--filter=blob:none",
            "--filter=tree:0",
        ])
        .arg(scene.url())
        .arg(bad.to_str().unwrap())
        .run();
    assert!(
        !ok,
        "a combined filter was accepted by a server that implements one at a \
         time, so the client believes it made a partial clone it did not:\n{stderr}"
    );
}