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

nordisk/gunnar

crates/gunnar-client/tests/real_git_shallow.rs

//! Shallow, against the real `git upload-pack` binary — **and against what
//! stock git's own client produced for the same request**.
//!
//! # The gap this closes
//!
//! `gunnar-wire` has served all five shallow wire arguments since it first
//! advertised the `shallow` capability. The client had none of them:
//! `gunnar clone --depth 1` was an unknown-argument error. gunnar could serve a
//! shallow clone it could not make, and `--depth 1` is what `actions/checkout`
//! passes by default, so that hole is also the CI case.
//!
//! # Why stock git is the judge, twice over
//!
//! Every arm here runs the same request through **git 2.53.0's own client**
//! against the same `git upload-pack`, and compares two things that a working
//! clone does not reveal on its own:
//!
//! * the **commit count** — how much history landed;
//! * the contents of **`.git/shallow`** — where the boundary ended up.
//!
//! A shallow fetch whose `deepen` line never reached the wire still succeeds,
//! still writes every ref, and still passes `git fsck`. It differs from a working
//! one in exactly those two places, which is why nothing here asserts on a
//! `Result` being `Ok`.
//!
//! # Both protocol versions, both hashes
//!
//! Every arm runs at v2 **and** at v0, and on sha1 **and** sha256. The v0/v2
//! split produced four separate wrong answers in gunnar in a single day, each one
//! invisible to a v2-only test, because the two versions carry the same arguments
//! in different frames behind differently-named capabilities. `is_really_v0`
//! below reads the client's own bytes to prove the v0 arms are v0 rather than
//! trusting the flag that asked for it.

mod common;

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

use common::*;
use gix::hash::Kind;
use gunnar_client::fetch::{fetch_with_io_opts, open_or_init_bare, FetchOptions};
use gunnar_client::{DepthArgs, Shallow};

/// Fetch every branch into `refs/heads/*` of a bare repository, which is what
/// `git clone --bare` produces and therefore what the reference clone can be
/// compared against ref for ref.
const BARE_REFS: &[&str] = &["+refs/heads/*:refs/heads/*"];

/// Both protocol versions. `true` is v2.
const BOTH_PROTOCOLS: [bool; 2] = [true, false];

fn label(kind: Kind, v2: bool) -> String {
    format!(
        "{:?}/{}",
        kind,
        if v2 { "protocol-v2" } else { "protocol-v0" }
    )
}

/// A source repository: `c1 → c2 → c3` on `main`, plus `side` off `c1`, with
/// commit times ten seconds apart so `deepen-since` has something to cut on.
struct Scene {
    dir: tempfile::TempDir,
    kind: Kind,
    /// `c1`, `c2`, `c3` on `main`, oldest first.
    main: Vec<String>,
    /// The tip of `side`, a child of `c1`.
    side: String,
}

/// The commit times used, so a cutoff in a test reads against something.
const T1: i64 = 1_700_001_000;
const T2: i64 = 1_700_002_000;
const T3: i64 = 1_700_003_000;

impl Scene {
    fn new(kind: Kind) -> Self {
        let dir = tempfile::tempdir().unwrap();
        let src = dir.path().join("src");
        init_worktree(&src, kind);
        let c1 = commit_file_at(&src, "a.txt", "one\n", "c1", T1);
        let c2 = commit_file_at(&src, "b.txt", "two\n", "c2", T2);
        let c3 = commit_file_at(&src, "c.txt", "three\n", "c3", T3);
        // `side` branches off `c1`, so `--shallow-exclude=side` has an ancestry
        // to remove that is not all of `main`.
        git(&src, &["checkout", "-q", "-b", "side", &c1]);
        let side = commit_file_at(&src, "s.txt", "side\n", "s1", T2);
        git(&src, &["checkout", "-q", "main"]);
        Scene {
            dir,
            kind,
            main: vec![c1, c2, c3],
            side,
        }
    }

    fn src(&self) -> PathBuf {
        self.dir.path().join("src")
    }

    /// `file://` so git uses the smart transport (`--depth` needs it) rather
    /// than a local hardlink clone.
    fn file_url(&self) -> String {
        format!("file://{}", self.src().display())
    }

    /// Clone with **stock git**, at the given protocol version, and return the
    /// bare clone's path. The reference every gunnar arm is judged against.
    ///
    /// `--no-single-branch` is not decoration. Every shallow option in git
    /// (`--depth`, `--shallow-since`, `--shallow-exclude`) **implies
    /// `--single-branch`**, so the default reference clone fetches only `main`
    /// while gunnar's `+refs/heads/*:refs/heads/*` fetches `main` and `side`.
    /// Comparing those two compares two different requests: MEASURED here on
    /// 2026-08-05, git wrote one cut point and gunnar wrote two, and the
    /// difference was the refspec, not the depth.
    fn git_clone(&self, name: &str, v2: bool, extra: &[&str]) -> PathBuf {
        let dest = self.dir.path().join(name);
        let version = if v2 {
            "protocol.version=2"
        } else {
            "protocol.version=0"
        };
        let url = self.file_url();
        let mut args: Vec<&str> =
            vec!["-c", version, "clone", "-q", "--bare", "--no-single-branch"];
        args.extend_from_slice(extra);
        let dest_s = dest.to_string_lossy().to_string();
        args.push(&url);
        args.push(&dest_s);
        git(self.dir.path(), &args);
        dest
    }

    /// Fetch with **gunnar** into a fresh bare repository, returning it and the
    /// bytes the client wrote.
    fn gunnar_fetch(
        &self,
        name: &str,
        v2: bool,
        options: &FetchOptions,
    ) -> (PathBuf, gunnar_client::FetchOutcome, String) {
        let dest = self.dir.path().join(name);
        let repo = open_or_init_bare(&dest, self.kind).unwrap();
        self.gunnar_fetch_into(&repo, &dest, v2, options)
    }

    /// The same fetch, into a repository that already exists — how a deepening
    /// or an unshallowing is done.
    fn gunnar_fetch_into(
        &self,
        repo: &gix::Repository,
        dest: &Path,
        v2: bool,
        options: &FetchOptions,
    ) -> (PathBuf, gunnar_client::FetchOutcome, String) {
        let child = if v2 {
            GitChild::upload_pack(&self.src())
        } else {
            GitChild::upload_pack_v0(&self.src())
        };
        let (stdout, stdin, mut proc) = child.into_halves();
        let (tee, seen) = Tee::new(stdin);
        let url = self.src().to_string_lossy().to_string();
        let outcome = fetch_with_io_opts(repo, stdout, tee, &url, &url, BARE_REFS, options);
        let _ = proc.wait();
        let wire = String::from_utf8_lossy(&seen.lock().unwrap()).into_owned();
        // `GUNNAR_DUMP_WIRE=1 cargo test … -- --nocapture` prints exactly what
        // the client wrote. Kept because every defect in this file was found by
        // reading it, and the v0 one is invisible any other way.
        if std::env::var_os("GUNNAR_DUMP_WIRE").is_some() {
            eprintln!("--- {} ---\n{}", label(self.kind, v2), readable(&wire));
        }
        match outcome {
            Ok(outcome) => (dest.to_path_buf(), outcome, wire),
            Err(e) => panic!(
                "gunnar's shallow fetch failed at {}: {e}\nwhat the client wrote:\n{}",
                label(self.kind, v2),
                readable(&wire)
            ),
        }
    }
}

/// The client's writes, split back into pkt-lines.
///
/// # Why a substring search is not good enough here
///
/// v0 bakes the negotiated capabilities onto the first `want` line, and three of
/// them are literally named `deepen-since`, `deepen-not` and `deepen-relative`.
/// So `wire.contains("deepen")` is **true of every v0 fetch ever sent**,
/// including one that asked for no shallowness at all — MEASURED here on
/// 2026-08-05, where it turned the file's own negative control green for the
/// wrong reason. An argument is a whole pkt-line; that is what is compared.
///
/// Flush (`0000`), delim (`0001`) and response-end (`0002`) carry no payload and
/// are skipped.
fn pkt_lines(wire: &str) -> Vec<String> {
    let bytes = wire.as_bytes();
    let mut out = Vec::new();
    let mut at = 0usize;
    while at + 4 <= bytes.len() {
        let Ok(header) = std::str::from_utf8(&bytes[at..at + 4]) else {
            break;
        };
        let Ok(len) = usize::from_str_radix(header, 16) else {
            break;
        };
        if len < 4 {
            at += 4;
            continue;
        }
        if at + len > bytes.len() {
            break;
        }
        out.push(
            String::from_utf8_lossy(&bytes[at + 4..at + len])
                .trim_end_matches('\n')
                .to_string(),
        );
        at += len;
    }
    out
}

/// Did the client send exactly this argument as its own pkt-line?
fn sent(wire: &str, argument: &str) -> bool {
    pkt_lines(wire).iter().any(|line| line == argument)
}

/// Did the client send any argument beginning with `prefix`? Used for the
/// negative controls, where the point is that no `deepen…` **argument** went out
/// even though the v0 capability list names three of them.
fn sent_any(wire: &str, prefix: &str) -> bool {
    pkt_lines(wire)
        .iter()
        .any(|line| line.starts_with(prefix) && !line.starts_with("want "))
}

/// The pkt-lines, one per output line, for a failure message.
fn readable(wire: &str) -> String {
    pkt_lines(wire)
        .iter()
        .map(|l| format!("  | {l}"))
        .collect::<Vec<_>>()
        .join("\n")
}

/// Does this byte stream prove the conversation was protocol **v0**?
///
/// Read off the client's own writes, not off the flag that asked for v0. v0 puts
/// the capabilities on the first `want` line and has no `command=` verb at all;
/// v2 sends `command=fetch` and never puts capabilities on a want. Either
/// property alone can be satisfied by accident — a v2 stream contains the word
/// `want` too — so both are required.
fn is_really_v0(wire: &str) -> bool {
    let lines = pkt_lines(wire);
    !lines.iter().any(|l| l.starts_with("command="))
        && lines
            .iter()
            .any(|l| l.starts_with("want ") && l.contains("multi_ack_detailed"))
}

/// And the converse, so the v2 arms cannot silently be v0 either.
fn is_really_v2(wire: &str) -> bool {
    pkt_lines(wire).iter().any(|l| l == "command=fetch")
}

fn assert_version(wire: &str, v2: bool, kind: Kind) {
    if v2 {
        assert!(
            is_really_v2(wire),
            "{}: asked for protocol v2 and the client never sent `command=fetch`. \
             What it wrote:\n{}",
            label(kind, v2),
            readable(wire)
        );
    } else {
        assert!(
            is_really_v0(wire),
            "{}: asked for protocol v0 and the client's bytes do not look like v0 \
             (expected capabilities on the want line and no `command=`). What it wrote:\n{}",
            label(kind, v2),
            readable(wire)
        );
    }
}

/// The capabilities the client claimed on its v0 `want` line, or `None` on v2
/// (where want lines carry none).
fn want_line_caps(wire: &str) -> Option<Vec<String>> {
    pkt_lines(wire)
        .into_iter()
        .find(|l| l.starts_with("want ") && l.split(' ').count() > 2)
        .map(|l| l.split(' ').skip(2).map(str::to_string).collect())
}

/// `deepen-relative` lives in a different place in each protocol version, and
/// claiming it in the wrong one is a wrong answer in both directions.
///
/// * **v0**: it is a *capability* on the want line, and there is no such
///   argument line — stock git dies with *protocol error, expected to get object
///   ID, not 'deepen-relative'*.
/// * **v2**: it is an *argument*, and want lines carry no capabilities at all.
fn assert_relative_deepen(wire: &str, v2: bool, at: &str) {
    assert!(
        sent(wire, "deepen 1"),
        "{at}: --deepen must send `deepen 1`:\n{}",
        readable(wire)
    );
    if v2 {
        assert!(
            sent(wire, "deepen-relative"),
            "{at}: on v2 `deepen-relative` is an argument line:\n{}",
            readable(wire)
        );
    } else {
        assert!(
            !sent(wire, "deepen-relative"),
            "{at}: v0 has no `deepen-relative` argument line; stock git dies on \
             it:\n{}",
            readable(wire)
        );
        assert!(
            want_line_caps(wire)
                .unwrap_or_default()
                .iter()
                .any(|c| c == "deepen-relative"),
            "{at}: on v0 a relative deepening is requested by the want-line \
             capability, and it is missing:\n{}",
            readable(wire)
        );
    }
}

// ── --depth ─────────────────────────────────────────────────────────────────

/// `--depth 1`: the case `actions/checkout` runs by default.
#[test]
fn depth_one_lands_what_stock_git_lands() {
    for kind in BOTH_HASH_KINDS {
        for v2 in BOTH_PROTOCOLS {
            let scene = Scene::new(kind);
            let reference = scene.git_clone("ref.git", v2, &["--depth=1"]);
            let (dest, outcome, wire) = scene.gunnar_fetch(
                "gunnar.git",
                v2,
                &FetchOptions::new().with_shallow(Shallow::DepthAtRemote(
                    std::num::NonZeroU32::new(1).unwrap(),
                )),
            );
            let at = label(kind, v2);

            assert_version(&wire, v2, kind);
            // The argument reached the wire. Without this the rest of the test
            // can pass on a client that sent an ordinary fetch and happened to
            // be pointed at a repository small enough not to notice.
            assert!(
                sent(&wire, "deepen 1"),
                "{at}: the client never sent `deepen 1`. What it wrote:\n{}",
                readable(&wire)
            );
            // The v0 half of the fix, and the reason this test sees a shallow
            // clone at all. gix claims `deepen-relative` on every v0 want line,
            // and `upload-pack.c` then counts the depth from a boundary this
            // fresh clone does not have — which selects nothing and sends the
            // WHOLE history with no `shallow` line. MEASURED 2026-08-05:
            // 1170 bytes and three commits, against git's own 292 and one.
            if !v2 {
                assert!(
                    !want_line_caps(&wire)
                        .unwrap_or_default()
                        .iter()
                        .any(|c| c == "deepen-relative"),
                    "{at}: a NON-relative --depth must not claim `deepen-relative`, \
                     or stock git answers with the whole history:\n{}",
                    readable(&wire)
                );
            } else {
                assert!(
                    want_line_caps(&wire).is_none(),
                    "{at}: v2 want lines carry no capabilities:\n{}",
                    readable(&wire)
                );
            }

            assert_eq!(
                commit_count(&dest, "refs/heads/main"),
                commit_count(&reference, "refs/heads/main"),
                "{at}: gunnar landed a different amount of history than git did"
            );
            assert_eq!(
                commit_count(&dest, "refs/heads/main"),
                1,
                "{at}: --depth 1 must land exactly the tip"
            );
            assert_eq!(
                shallow_file(&dest),
                shallow_file(&reference),
                "{at}: the boundary in .git/shallow differs from git's"
            );
            // Both tips, because both branches were asked for: `main`'s c3 and
            // `side`'s only commit. Named rather than counted, so a boundary at
            // the wrong commits cannot pass.
            let mut expected = vec![scene.main[2].clone(), scene.side.clone()];
            expected.sort();
            assert_eq!(
                shallow_file(&dest),
                expected,
                "{at}: the cut points must be the two tips themselves"
            );
            assert_eq!(
                outcome
                    .shallow_boundary
                    .iter()
                    .map(ToString::to_string)
                    .collect::<Vec<_>>(),
                shallow_file(&dest),
                "{at}: FetchOutcome::shallow_boundary disagrees with the file on disk"
            );
            assert!(
                git_try(&dest, &["fsck", "--strict", "--no-progress"]).is_ok(),
                "{at}: fsck rejected the shallow clone"
            );
        }
    }
}

/// `--depth 2` on the same history, so a passing `--depth 1` cannot be a client
/// that sends a constant.
#[test]
fn depth_two_cuts_one_commit_deeper() {
    for kind in BOTH_HASH_KINDS {
        for v2 in BOTH_PROTOCOLS {
            let scene = Scene::new(kind);
            let reference = scene.git_clone("ref.git", v2, &["--depth=2"]);
            let (dest, _, wire) = scene.gunnar_fetch(
                "gunnar.git",
                v2,
                &FetchOptions::new().with_shallow(Shallow::DepthAtRemote(
                    std::num::NonZeroU32::new(2).unwrap(),
                )),
            );
            let at = label(kind, v2);
            assert_version(&wire, v2, kind);
            assert!(
                sent(&wire, "deepen 2"),
                "{at}: the client never sent `deepen 2`:\n{}",
                readable(&wire)
            );
            assert_eq!(commit_count(&dest, "refs/heads/main"), 2, "{at}");
            assert_eq!(
                commit_count(&dest, "refs/heads/main"),
                commit_count(&reference, "refs/heads/main"),
                "{at}"
            );
            assert_eq!(shallow_file(&dest), shallow_file(&reference), "{at}");
            // c2 on `main`, and c1 reached at depth 2 down `side`. c1 IS a root,
            // and stock git marks it shallow anyway — `get_shallow_commits`
            // cuts at the depth without asking whether the commit has parents.
            // Asserted because it is surprising, and because gunnar's own
            // upload-pack deliberately does the opposite (`gunnar_wire::shallow`
            // filters roots out of `newly_shallow`), which is a server-side
            // divergence from git this client test is the first thing to see.
            let mut expected = vec![scene.main[1].clone(), scene.main[0].clone()];
            expected.sort();
            assert_eq!(
                shallow_file(&dest),
                expected,
                "{at}: the cut points must be c2 (on main) and c1 (at depth 2 down side)"
            );
        }
    }
}

// ── --unshallow and --deepen ────────────────────────────────────────────────

/// `--unshallow` on a depth-1 clone: the whole history arrives and the boundary
/// file goes away entirely.
#[test]
fn unshallow_completes_the_history_and_removes_the_boundary() {
    for kind in BOTH_HASH_KINDS {
        for v2 in BOTH_PROTOCOLS {
            let scene = Scene::new(kind);
            let at = label(kind, v2);

            // git's own reference: clone --depth=1 then fetch --unshallow.
            let reference = scene.git_clone("ref.git", v2, &["--depth=1"]);
            assert_eq!(
                shallow_file(&reference).len(),
                2,
                "{at}: fixture — a depth-1 clone of two branches has two cut points"
            );
            git(
                &reference,
                &[
                    "fetch",
                    "--unshallow",
                    "origin",
                    "+refs/heads/*:refs/heads/*",
                ],
            );

            let depth1 = FetchOptions::new().with_shallow(Shallow::DepthAtRemote(
                std::num::NonZeroU32::new(1).unwrap(),
            ));
            let dest = scene.dir.path().join("gunnar.git");
            let repo = open_or_init_bare(&dest, kind).unwrap();
            let (_, first, _) = scene.gunnar_fetch_into(&repo, &dest, v2, &depth1);
            assert_eq!(
                first.shallow_boundary.len(),
                2,
                "{at}: the depth-1 half of the fixture did not go shallow"
            );

            // `--unshallow` is `deepen 2147483647` on the wire, not an argument
            // of its own.
            let (_, outcome, wire) = scene.gunnar_fetch_into(
                &repo,
                &dest,
                v2,
                &FetchOptions::new().with_shallow(Shallow::undo()),
            );
            assert_version(&wire, v2, kind);
            assert!(
                sent(&wire, "deepen 2147483647"),
                "{at}: --unshallow must go out as deepen 2147483647:\n{}",
                readable(&wire)
            );
            assert!(
                sent(&wire, &format!("shallow {}", scene.main[2])),
                "{at}: the client must declare its existing boundary, or the server \
                 cannot know what to unshallow:\n{}",
                readable(&wire)
            );
            assert_eq!(
                commit_count(&dest, "refs/heads/main"),
                3,
                "{at}: --unshallow must land every commit"
            );
            assert_eq!(
                commit_count(&dest, "refs/heads/main"),
                commit_count(&reference, "refs/heads/main"),
                "{at}"
            );
            assert!(
                shallow_file(&dest).is_empty(),
                "{at}: .git/shallow still holds {:?} after --unshallow",
                shallow_file(&dest)
            );
            assert_eq!(
                shallow_file(&dest),
                shallow_file(&reference),
                "{at}: git removed the boundary and gunnar did not, or the reverse"
            );
            assert!(
                outcome.shallow_boundary.is_empty(),
                "{at}: the outcome still reports a boundary"
            );
            assert!(
                git_try(&dest, &["fsck", "--strict", "--no-progress"]).is_ok(),
                "{at}: fsck rejected the unshallowed clone"
            );
        }
    }
}

/// `--deepen 1` on a depth-1 clone. It counts from **this** repository's
/// boundary, which is what `deepen-relative` means, and is the argument
/// `--depth` must not be confused with: `--depth 1` on a depth-1 clone changes
/// nothing at all.
#[test]
fn deepen_is_relative_to_our_own_boundary_and_depth_is_not() {
    for kind in BOTH_HASH_KINDS {
        for v2 in BOTH_PROTOCOLS {
            let scene = Scene::new(kind);
            let at = label(kind, v2);
            let depth1 = FetchOptions::new().with_shallow(Shallow::DepthAtRemote(
                std::num::NonZeroU32::new(1).unwrap(),
            ));

            // git's reference for `--deepen=1`.
            let reference = scene.git_clone("ref.git", v2, &["--depth=1"]);
            git(
                &reference,
                &[
                    "fetch",
                    "--deepen=1",
                    "origin",
                    "+refs/heads/*:refs/heads/*",
                ],
            );

            let dest = scene.dir.path().join("gunnar.git");
            let repo = open_or_init_bare(&dest, kind).unwrap();
            scene.gunnar_fetch_into(&repo, &dest, v2, &depth1);

            let (_, deepened, wire) = scene.gunnar_fetch_into(
                &repo,
                &dest,
                v2,
                &FetchOptions::new().with_shallow(Shallow::Deepen(1)),
            );
            assert_version(&wire, v2, kind);
            assert_relative_deepen(&wire, v2, &at);
            assert_eq!(
                commit_count(&dest, "refs/heads/main"),
                2,
                "{at}: one more commit than the depth-1 clone had"
            );
            assert_eq!(
                commit_count(&dest, "refs/heads/main"),
                commit_count(&reference, "refs/heads/main"),
                "{at}"
            );
            assert_eq!(shallow_file(&dest), shallow_file(&reference), "{at}");
            // Still shallow, one commit further back on each branch, and still
            // agreeing with git — which the comparison above already asserted.
            assert_eq!(
                deepened
                    .shallow_boundary
                    .iter()
                    .map(ToString::to_string)
                    .collect::<Vec<_>>(),
                shallow_file(&reference),
                "{at}: the deepened boundary is not the one git arrived at"
            );

            // The negative control that gives the assertion above its meaning.
            // `--depth` counts at the REMOTE, so running it on the deepened
            // clone moves the boundary back to one commit — it *shrinks*, where
            // `--deepen` only ever grows. If the two were the same argument this
            // step would leave two commits.
            //
            // git's own answer is fetched first, so the number is git's and not
            // a guess: MEASURED 2.53.0, `fetch --depth=1` on a two-commit shallow
            // clone leaves one.
            git(
                &reference,
                &["fetch", "--depth=1", "origin", "+refs/heads/*:refs/heads/*"],
            );
            let (_, again, _) = scene.gunnar_fetch_into(&repo, &dest, v2, &depth1);
            assert_eq!(
                commit_count(&dest, "refs/heads/main"),
                commit_count(&reference, "refs/heads/main"),
                "{at}: --depth on an already-deepened clone did not do what git does"
            );
            assert_eq!(
                commit_count(&dest, "refs/heads/main"),
                1,
                "{at}: --depth counts at the remote, so it shrinks the boundary back"
            );
            assert_eq!(
                again
                    .shallow_boundary
                    .iter()
                    .map(ToString::to_string)
                    .collect::<Vec<_>>(),
                shallow_file(&reference),
                "{at}"
            );
        }
    }
}

// ── --shallow-since ─────────────────────────────────────────────────────────

/// `--shallow-since`, cut between c1 and c2.
#[test]
fn shallow_since_cuts_where_git_cuts() {
    for kind in BOTH_HASH_KINDS {
        for v2 in BOTH_PROTOCOLS {
            let scene = Scene::new(kind);
            let at = label(kind, v2);
            // Between c1 and c2, so c1 is excluded and c2/c3 are kept.
            let cutoff = T1 + 500;
            let since = format!("--shallow-since=@{cutoff}");
            let reference = scene.git_clone("ref.git", v2, &[&since]);

            let shallow = DepthArgs {
                since: Some(format!("@{cutoff}")),
                ..DepthArgs::default()
            }
            .to_shallow()
            .expect("@<unix seconds> is a date git understands");
            let (dest, _, wire) =
                scene.gunnar_fetch("gunnar.git", v2, &FetchOptions::new().with_shallow(shallow));

            assert_version(&wire, v2, kind);
            assert!(
                sent(&wire, &format!("deepen-since {cutoff}")),
                "{at}: the client never sent `deepen-since {cutoff}`:\n{}",
                readable(&wire)
            );
            assert_eq!(
                commit_count(&dest, "refs/heads/main"),
                commit_count(&reference, "refs/heads/main"),
                "{at}: gunnar kept a different number of commits than git"
            );
            assert_eq!(
                commit_count(&dest, "refs/heads/main"),
                2,
                "{at}: c2 and c3 are newer than the cutoff, c1 is not"
            );
            assert_eq!(shallow_file(&dest), shallow_file(&reference), "{at}");
            // c2, because its parent c1 predates the cutoff — and s1 on `side`
            // for the same reason, since c1 is its parent too.
            let mut expected = vec![scene.main[1].clone(), scene.side.clone()];
            expected.sort();
            assert_eq!(
                shallow_file(&dest),
                expected,
                "{at}: the boundary is every commit whose parent is older than the cutoff"
            );
        }
    }
}

// ── --shallow-exclude ───────────────────────────────────────────────────────

/// `--shallow-exclude=side` — with the **short** ref name, which is the ordinary
/// spelling and the one that broke the server side of this on 2026-08-05:
/// `FullName::new("side")` failed, the exclusion was dropped, and the client got
/// the whole history with an empty `.git/shallow` and no error on either end.
///
/// The client half of the same trap is [`gunnar_client::DepthArgs`] parsing the
/// name: a `FullName` there would refuse `side` outright, which at least fails
/// loudly, or worse silently drop it.
#[test]
fn shallow_exclude_takes_a_short_ref_name_and_cuts_where_git_cuts() {
    for kind in BOTH_HASH_KINDS {
        for v2 in BOTH_PROTOCOLS {
            let scene = Scene::new(kind);
            let at = label(kind, v2);
            let reference = scene.git_clone("ref.git", v2, &["--shallow-exclude=side"]);

            let shallow = DepthArgs {
                exclude: vec!["side".into()],
                ..DepthArgs::default()
            }
            .to_shallow()
            .expect("a short ref name is what --shallow-exclude takes");
            let (dest, _, wire) =
                scene.gunnar_fetch("gunnar.git", v2, &FetchOptions::new().with_shallow(shallow));

            assert_version(&wire, v2, kind);
            assert!(
                sent(&wire, "deepen-not side"),
                "{at}: the client never sent `deepen-not side`, so nothing was \
                 excluded:\n{}",
                readable(&wire)
            );
            assert_eq!(
                commit_count(&dest, "refs/heads/main"),
                commit_count(&reference, "refs/heads/main"),
                "{at}: gunnar and git disagree on how much --shallow-exclude=side removes"
            );
            assert_eq!(
                commit_count(&dest, "refs/heads/main"),
                2,
                "{at}: c1 is reachable from side, so main keeps c2 and c3"
            );
            assert_eq!(shallow_file(&dest), shallow_file(&reference), "{at}");
            assert!(
                !shallow_file(&dest).is_empty(),
                "{at}: an empty .git/shallow is exactly what the dropped-exclusion \
                 bug produced"
            );
            // `side`'s own tip is a want too, and it was excluded, so it must not
            // have arrived complete.
            assert!(
                !objects_via_git(&dest, "refs/heads/main").contains(&scene.side),
                "{at}: the excluded branch's commit came across anyway"
            );
        }
    }
}

// ── the command line, judged against git's own refusals ─────────────────────

/// Every combination `DepthArgs` refuses, paired with stock git refusing the
/// same one.
///
/// Both halves are asserted. A local refusal that git accepts would be gunnar
/// inventing a rule; a combination gunnar accepts and git refuses is a request
/// that can only ever come back as an error.
#[test]
fn the_flag_combinations_gunnar_refuses_are_the_ones_git_refuses() {
    let scene = Scene::new(Kind::Sha1);
    let shallow_clone = scene.git_clone("ref.git", true, &["--depth=1"]);

    // 1. depth 0.
    let err = DepthArgs {
        depth: Some(0),
        ..DepthArgs::default()
    }
    .to_shallow()
    .expect_err("depth 0 is not a positive number");
    assert!(
        err.to_string().contains("not a positive number"),
        "gunnar's wording drifted: {err}"
    );
    let git_said = git_try(
        scene.dir.path(),
        &[
            "clone",
            "--bare",
            "--depth=0",
            &scene.file_url(),
            "zero.git",
        ],
    )
    .expect_err("git accepts --depth=0?!");
    assert!(
        git_said.contains("depth 0 is not a positive number"),
        "git 2.53.0 said something else about --depth=0: {git_said}"
    );

    // 2. --depth with --unshallow.
    let err = DepthArgs {
        depth: Some(1),
        unshallow: true,
        ..DepthArgs::default()
    }
    .to_shallow()
    .expect_err("--depth and --unshallow are exclusive");
    assert!(
        err.to_string()
            .contains("'--depth' and '--unshallow' cannot be used together"),
        "gunnar's wording drifted: {err}"
    );
    let git_said = git_try(
        &shallow_clone,
        &["fetch", "--depth=1", "--unshallow", "origin"],
    )
    .expect_err("git accepts --depth with --unshallow?!");
    assert!(
        git_said.contains("'--depth' and '--unshallow' cannot be used together"),
        "git 2.53.0 said something else: {git_said}"
    );

    // 3. --deepen with --unshallow. git folds it onto the same message.
    let err = DepthArgs {
        deepen: Some(1),
        unshallow: true,
        ..DepthArgs::default()
    }
    .to_shallow()
    .expect_err("--deepen and --unshallow are exclusive");
    assert!(
        err.to_string()
            .contains("'--depth' and '--unshallow' cannot be used together"),
        "gunnar's wording drifted: {err}"
    );
    let git_said = git_try(
        &shallow_clone,
        &["fetch", "--deepen=1", "--unshallow", "origin"],
    )
    .expect_err("git accepts --deepen with --unshallow?!");
    assert!(
        git_said.contains("'--depth' and '--unshallow' cannot be used together"),
        "git 2.53.0 said something else: {git_said}"
    );

    // 4. --depth with --shallow-since. git's CLIENT sends both and lets
    //    upload-pack die on them; gunnar refuses before sending, in git's own
    //    words, because `Shallow` has one variant per mode.
    let err = DepthArgs {
        depth: Some(1),
        since: Some("@1700000000".into()),
        ..DepthArgs::default()
    }
    .to_shallow()
    .expect_err("deepen and deepen-since are exclusive");
    assert!(
        err.to_string()
            .contains("deepen and deepen-since (or deepen-not) cannot be used together"),
        "gunnar's wording drifted: {err}"
    );
    let git_said = git_try(
        &shallow_clone,
        &[
            "fetch",
            "--depth=1",
            "--shallow-since=@1700000000",
            "origin",
        ],
    )
    .expect_err("git accepts --depth with --shallow-since?!");
    assert!(
        git_said.contains("deepen and deepen-since (or deepen-not) cannot be used together"),
        "git 2.53.0 said something else: {git_said}"
    );

    // 5. --depth with --shallow-exclude: the same refusal.
    let err = DepthArgs {
        depth: Some(1),
        exclude: vec!["side".into()],
        ..DepthArgs::default()
    }
    .to_shallow()
    .expect_err("deepen and deepen-not are exclusive");
    assert!(
        err.to_string()
            .contains("deepen and deepen-since (or deepen-not) cannot be used together"),
        "gunnar's wording drifted: {err}"
    );

    // 6. --shallow-since WITH --shallow-exclude is legal, and both go out. This
    //    is the row that stops the two above from being a blanket refusal.
    let both = DepthArgs {
        since: Some("@1700000000".into()),
        exclude: vec!["side".into()],
        ..DepthArgs::default()
    }
    .to_shallow()
    .expect("git accepts --shallow-since with --shallow-exclude");
    match both {
        Shallow::Exclude {
            remote_refs,
            since_cutoff,
        } => {
            assert_eq!(remote_refs.len(), 1);
            assert_eq!(
                since_cutoff.map(|t| t.seconds),
                Some(1_700_000_000),
                "the cutoff must survive alongside the exclusion"
            );
        }
        other => panic!("expected Shallow::Exclude, got {other:?}"),
    }

    // 7. Nothing asked for is `NoChange`, not a depth of zero.
    assert_eq!(
        DepthArgs::default().to_shallow().unwrap(),
        Shallow::NoChange
    );
    assert!(!DepthArgs::default().is_active());
    assert!(DepthArgs {
        depth: Some(1),
        ..DepthArgs::default()
    }
    .is_active());
}

/// `--shallow-exclude` naming nothing: the server is what refuses, and the
/// client must surface that rather than clone the whole history quietly.
#[test]
fn an_unresolvable_shallow_exclude_fails_rather_than_excluding_nothing() {
    let scene = Scene::new(Kind::Sha1);

    // git first, so the expected shape is stock git's and not gunnar's.
    let git_said = git_try(
        scene.dir.path(),
        &[
            "clone",
            "--bare",
            "--shallow-exclude=nosuchref",
            &scene.file_url(),
            "gitref.git",
        ],
    )
    .expect_err("git accepts an unresolvable --shallow-exclude?!");
    // **Two spellings, one refusal.** git 2.53 says `deepen-not is not a ref`;
    // git 2.43 says `ambiguous deepen-not: deepen-not <name>`. Both are
    // `upload-pack` refusing a `deepen-not` it cannot resolve, which is the
    // behaviour this arm exists to copy — the wording is git's and has moved
    // once already. Pinning one version's sentence made this test report on
    // which git is installed.
    assert!(
        git_said.contains("deepen-not is not a ref") || git_said.contains("ambiguous deepen-not"),
        "git said something that is not a deepen-not refusal: {git_said}"
    );

    let shallow = DepthArgs {
        exclude: vec!["nosuchref".into()],
        ..DepthArgs::default()
    }
    .to_shallow()
    .expect("the client cannot know whether a name resolves; the server decides");

    let dest = scene.dir.path().join("gunnar.git");
    let repo = open_or_init_bare(&dest, Kind::Sha1).unwrap();
    let (stdout, stdin, mut proc) = GitChild::upload_pack(&scene.src()).into_halves();
    let url = scene.src().to_string_lossy().to_string();
    let result = fetch_with_io_opts(
        &repo,
        stdout,
        stdin,
        &url,
        &url,
        BARE_REFS,
        &FetchOptions::new().with_shallow(shallow),
    );
    let _ = proc.wait();

    let err = result.expect_err(
        "a deepen-not nobody can resolve must fail; excluding nothing instead is the \
         silent-wrong-answer shape",
    );
    let _ = err;
    // And the applied side: nothing was left behind that claims to be a clone.
    // Two INDEPENDENT assertions, not `a == 0 || b`: `dest` is a fresh empty
    // bare repo, so `commit_count(--all)` is 0 by construction and an `||` would
    // short-circuit before `shallow_file` was ever consulted — the boundary
    // check could never go red. Each property is required on its own.
    assert_eq!(
        commit_count(&dest, "--all"),
        0,
        "a failed shallow fetch landed commits — it excluded nothing instead of failing"
    );
    assert!(
        shallow_file(&dest).is_empty(),
        "a failed shallow fetch left a boundary behind: {:?}",
        shallow_file(&dest)
    );
}

/// A fetch with no shallow request must leave a complete repository complete,
/// and its `deepen` line absent.
///
/// The negative control for the whole file: it is what makes `wire.contains(
/// "deepen 1")` above mean something, because it proves the string is not simply
/// always there.
#[test]
fn a_plain_fetch_sends_no_deepen_and_stays_complete() {
    for kind in BOTH_HASH_KINDS {
        for v2 in BOTH_PROTOCOLS {
            let scene = Scene::new(kind);
            let at = label(kind, v2);
            let (dest, outcome, wire) = scene.gunnar_fetch("gunnar.git", v2, &FetchOptions::new());
            assert_version(&wire, v2, kind);
            assert!(
                !sent_any(&wire, "deepen"),
                "{at}: a plain fetch sent a deepen argument. The v0 capability list \
                 names `deepen-since`, `deepen-not` and `deepen-relative`, so this \
                 looks only at whole argument lines:\n{}",
                readable(&wire)
            );
            assert_eq!(commit_count(&dest, "refs/heads/main"), 3, "{at}");
            assert!(
                outcome.shallow_boundary.is_empty(),
                "{at}: a plain fetch made the repository shallow"
            );
            assert!(shallow_file(&dest).is_empty(), "{at}");
        }
    }
}

/// The one thing `Tee` cannot see: that a fetch which never sent `deepen` also
/// never *wrote* a boundary. Kept as a compile-time reminder that `Write` is the
/// half being observed.
fn _tee_is_a_writer<W: Write>(_: Tee<W>) {}

// ── the clone verb ──────────────────────────────────────────────────────────

/// `gunnar clone --depth 1`: the headline. A real working tree, checked out from
/// a shallow fetch, judged against `git clone --depth=1` of the same repository.
///
/// This is the path `crate::clone::clone_with_io` assembles, and the only test
/// that exercises `init_clone_dir` for a remote that **has** commits — every
/// other caller of it is the empty-remote case.
#[test]
fn clone_at_depth_one_produces_the_same_worktree_git_does() {
    for kind in BOTH_HASH_KINDS {
        for v2 in BOTH_PROTOCOLS {
            let scene = Scene::new(kind);
            let at = label(kind, v2);

            // git's reference, non-bare this time: a worktree, single-branch,
            // exactly what `git clone --depth=1` gives a person.
            let reference = scene.dir.path().join("ref");
            let version = if v2 {
                "protocol.version=2"
            } else {
                "protocol.version=0"
            };
            git(
                scene.dir.path(),
                &[
                    "-c",
                    version,
                    "clone",
                    "-q",
                    "--depth=1",
                    &scene.file_url(),
                    &reference.to_string_lossy(),
                ],
            );

            // gunnar. Two connections, as `clone_ssh_with` makes over SSH: one
            // to learn the object format and the symbolic HEAD, one to fetch.
            let url = scene.src().to_string_lossy().to_string();
            let identity = {
                let child = if v2 {
                    GitChild::upload_pack(&scene.src())
                } else {
                    GitChild::upload_pack_v0(&scene.src())
                };
                let (stdout, stdin, mut proc) = child.into_halves();
                let id = gunnar_client::fetch::remote_identity_with_io(stdout, stdin, &url)
                    .unwrap_or_else(|e| {
                        panic!("{at}: remote identity: {}", gunnar_client::chain(&e))
                    });
                let _ = proc.wait();
                id
            };
            assert_eq!(
                identity.hash_kind, kind,
                "{at}: the object format must be READ, never guessed"
            );
            assert_eq!(
                identity.head_target.as_deref(),
                Some("refs/heads/main"),
                "{at}: the symbolic HEAD must come off the advertisement"
            );

            let dest = scene.dir.path().join("gunnar");
            let child = if v2 {
                GitChild::upload_pack(&scene.src())
            } else {
                GitChild::upload_pack_v0(&scene.src())
            };
            let (stdout, stdin, mut proc) = child.into_halves();
            let report = gunnar_client::clone_with_io(
                &identity,
                stdout,
                stdin,
                &url,
                &url,
                &dest,
                &FetchOptions::new().with_shallow(Shallow::DepthAtRemote(
                    std::num::NonZeroU32::new(1).unwrap(),
                )),
            )
            .unwrap_or_else(|e| {
                panic!("{at}: gunnar clone --depth 1: {}", gunnar_client::chain(&e))
            });
            let _ = proc.wait();

            assert_eq!(report.branch, "refs/heads/main", "{at}");
            assert_eq!(
                report.head.map(|id| id.to_string()),
                Some(scene.main[2].clone()),
                "{at}"
            );
            assert_eq!(
                git(&dest, &["rev-parse", "HEAD"]),
                git(&reference, &["rev-parse", "HEAD"]),
                "{at}: gunnar checked out a different commit than git did"
            );
            assert_eq!(
                commit_count(&dest, "HEAD"),
                commit_count(&reference, "HEAD"),
                "{at}: different amount of history"
            );
            assert_eq!(commit_count(&dest, "HEAD"), 1, "{at}");
            assert!(
                !shallow_file(&dest).is_empty(),
                "{at}: the clone is not shallow at all — .git/shallow is empty"
            );
            assert_eq!(
                report
                    .shallow_boundary
                    .iter()
                    .map(ToString::to_string)
                    .collect::<Vec<_>>(),
                shallow_file(&dest),
                "{at}: the report disagrees with .git/shallow"
            );
            // The worktree is real, not just the object database.
            assert_eq!(
                std::fs::read_to_string(dest.join("c.txt")).unwrap_or_default(),
                "three\n",
                "{at}: the tip's file is not on disk, so no checkout happened"
            );
            // And `git` itself is content with what gunnar built.
            assert!(
                git_try(&dest, &["fsck", "--strict", "--no-progress"]).is_ok(),
                "{at}: fsck rejected the shallow clone"
            );
            assert!(
                git_try(&dest, &["status", "--porcelain"])
                    .map(|s| s.trim().is_empty())
                    .unwrap_or(false),
                "{at}: the checkout is dirty, so the worktree does not match HEAD"
            );
            // `remote.origin` and the branch's tracking config, which is what
            // makes `git pull` work in the clone.
            assert_eq!(git(&dest, &["config", "remote.origin.url"]), url, "{at}");
            assert_eq!(
                git(&dest, &["config", "branch.main.remote"]),
                "origin",
                "{at}: without this `git pull` says there is no tracking information"
            );
        }
    }
}