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

nordisk/gunnar

crates/gunnar-cli/tests/shallow_delta_blob.rs

//! **A blob pushed as a delta is served on the shallow and the by-id paths.**
//!
//! # The field report (git.gunnar.rs, 2026-10-28, FORGE-SHALLOW)
//!
//! A push to `nordisk/edda` (fcb2292) changed one line of a small manifest.
//! `git push` sends a *thin* pack, so the new blob (25e2859) arrived as a
//! `REF_DELTA` against the version the server already held: a 12-byte delta,
//! 41 bytes deflated, the LAST entry of the pack and so the last bytes of the
//! store's archive. From that push until the next one:
//!
//! * every **shallow** clone or fetch (`--depth 1`) died with `early EOF` /
//!   `the repository is unavailable; try again`;
//! * `git fetch --filter=blob:none origin 25e2859` failed;
//! * a **full** clone worked.
//!
//! Replayed against a copy of edda, the server's own log said why:
//! `resolving the type of 25e2859…: short read at (16843881, 64)`. The engine
//! (`znippy-plugin-git` 0.2.0, `serve.rs:175` `resolved_type_at`) probes a
//! ref-delta's header with a fixed 64-byte read, and an entry shorter than that
//! at the end of the archive has fewer than 64 bytes after it. A full clone is
//! answered from the bitmaps and never asks a blob its size; a shallow clone
//! walks the tree and asks every blob (`Limits::object_bytes`), and so does a
//! want naming the blob. The next push appended bytes, which is why edda
//! 40c95a6 made shallow clones work again.
//!
//! # A second, gunnar-side defect on the same path
//!
//! A `blob:none` partial clone that fetches a blob of its own tip BY ID sends
//! that tip as a `have`. gunnar closed over the have's tree, counted every blob
//! under it as already held (which a `blob:none` client by construction does
//! not) and answered an EMPTY pack: `did not send all necessary objects`.
//! Stock `git upload-pack` sends the blob.
//! [`a_partial_clone_fetches_a_blob_of_its_own_tip_by_id`] is that case.

#![cfg(feature = "http")]

mod common;

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

use common::{Harness, HarnessOptions};
use gunnar_core::{HashKind, Visibility};

fn harness() -> Harness {
    Harness::start_with_options(
        "team",
        "project",
        HashKind::Sha1,
        HarnessOptions {
            http: true,
            visibility: Visibility::Public,
            ..HarnessOptions::default()
        },
    )
}

/// A manifest whose next version differs in one line, so git stores that
/// version as a delta of a few bytes (edda's `Cargo.toml` had a 12-byte one).
fn manifest(version: usize) -> String {
    let mut text = String::new();
    for line in 0..200 {
        if line == 3 {
            text.push_str(&format!("version = \"0.1.{version}\"\n"));
        } else {
            text.push_str(&format!(
                "dependency-{line:04} = {{ path = \"../crate-{line:04}\" }}\n"
            ));
        }
    }
    text
}

fn rev_parse(harness: &Harness, repo: &Path, rev: &str) -> String {
    harness
        .git(repo, &["rev-parse", rev])
        .expect_ok("git rev-parse")
        .stdout
        .trim()
        .to_owned()
}

/// Two pushes. The second changes ONE line of `Cargo.toml` and nothing else,
/// so its thin pack is a commit, a tree and the manifest as a small
/// `REF_DELTA`: the last entry, and therefore the last bytes the store holds.
/// Returns the working tree and the new manifest blob's id.
fn pushed_twice(harness: &Harness) -> (PathBuf, String) {
    let work = harness.dir.path().join("work");
    std::fs::create_dir_all(&work).expect("the working tree");
    harness
        .git(&work, &["init", "-q", "-b", "main"])
        .expect_ok("git init");
    let token = harness.mint_token("tester");
    let push_url = harness.http_url_with_token("team", "project", &token);

    std::fs::write(work.join("Cargo.toml"), manifest(1)).expect("write the manifest");
    std::fs::write(work.join("README.md"), "a readme\n").expect("write the readme");
    harness.git(&work, &["add", "-A"]).expect_ok("git add");
    harness
        .git(&work, &["commit", "-q", "-m", "version 1"])
        .expect_ok("git commit");
    harness
        .git(&work, &["push", "-q", &push_url, "main"])
        .expect_ok("the first push");

    std::fs::write(work.join("Cargo.toml"), manifest(2)).expect("write the manifest");
    harness
        .git(&work, &["commit", "-q", "-a", "-m", "version 2"])
        .expect_ok("git commit");
    harness
        .git(&work, &["push", "-q", &push_url, "main"])
        .expect_ok("the second push");
    let blob = rev_parse(harness, &work, "main:Cargo.toml");
    (work, blob)
}

/// `git clone --depth 1` and `git fetch --depth 1` after the push: the walk
/// sizes every blob, the manifest included.
#[test]
fn a_depth_one_clone_serves_a_blob_that_was_pushed_as_a_delta() {
    let harness = harness();
    let (work, blob) = pushed_twice(&harness);
    let url = harness.http_url("team", "project");

    for version in ["0", "2"] {
        let target = harness.dir.path().join(format!("shallow-v{version}"));
        harness
            .git_http(
                harness.dir.path(),
                &[
                    "-c",
                    &format!("protocol.version={version}"),
                    "clone",
                    "-q",
                    "--depth",
                    "1",
                    &url,
                    target.to_str().expect("utf-8 path"),
                ],
            )
            .expect_ok("a depth-1 clone after a delta push");
        assert_eq!(
            rev_parse(&harness, &target, "HEAD:Cargo.toml"),
            blob,
            "the shallow clone holds the pushed manifest"
        );
        let content = std::fs::read_to_string(target.join("Cargo.toml")).expect("checked out");
        assert_eq!(
            content,
            manifest(2),
            "the delta resolved to the pushed bytes"
        );
        harness
            .git(&target, &["fsck", "--strict"])
            .expect_ok("fsck of the shallow clone");
    }

    // A shallow FETCH into a shallow clone of the first commit, which is what
    // a CI checkout that already exists does.
    let target = harness.dir.path().join("shallow-fetch");
    let first = rev_parse(&harness, &work, "main~1");
    harness
        .git_http(
            harness.dir.path(),
            &["init", "-q", target.to_str().expect("utf-8 path")],
        )
        .expect_ok("git init");
    harness
        .git_http(&target, &["fetch", "-q", "--depth", "1", &url, &first])
        .expect_ok("a depth-1 fetch of the first commit");
    harness
        .git_http(&target, &["fetch", "-q", "--depth", "1", &url, "main"])
        .expect_ok("a depth-1 fetch of main after a delta push");
    harness
        .git(&target, &["--no-lazy-fetch", "cat-file", "-e", &blob])
        .expect_ok("the fetched manifest is present");
}

/// `git fetch --filter=blob:none <url> <blob>` into an empty repository: one
/// want, naming the delta-pushed blob, nothing in common.
#[test]
fn a_blob_pushed_as_a_delta_is_fetched_by_id() {
    let harness = harness();
    let (_work, blob) = pushed_twice(&harness);
    let url = harness.http_url("team", "project");

    let target = harness.dir.path().join("by-id");
    harness
        .git_http(
            harness.dir.path(),
            &["init", "-q", "--bare", target.to_str().expect("utf-8 path")],
        )
        .expect_ok("git init");
    harness
        .git_http(&target, &["fetch", "-q", "--filter=blob:none", &url, &blob])
        .expect_ok("fetch of the delta-pushed blob by id");
    let shown = harness
        .git(&target, &["--no-lazy-fetch", "cat-file", "-p", &blob])
        .expect_ok("the blob is readable")
        .stdout
        .clone();
    assert_eq!(
        shown,
        manifest(2),
        "the blob fetched by id is the pushed bytes"
    );
}

/// A `blob:none` clone fetching a blob of the very tip it holds, by id. Its
/// `have` is that tip, and a filtered client's `have` does not vouch for the
/// blobs under it. Stock `git upload-pack` sends the blob; gunnar answered an
/// empty pack. A WHOLE blob (the readme), so this is gunnar-wire's selection
/// and not the engine's header probe.
#[test]
fn a_partial_clone_fetches_a_blob_of_its_own_tip_by_id() {
    let harness = harness();
    let (work, _manifest) = pushed_twice(&harness);
    let readme = rev_parse(&harness, &work, "main:README.md");
    let url = harness.http_url("team", "project");

    let target = harness.dir.path().join("partial");
    harness
        .git_http(
            harness.dir.path(),
            &[
                "clone",
                "-q",
                "--filter=blob:none",
                "--no-checkout",
                &url,
                target.to_str().expect("utf-8 path"),
            ],
        )
        .expect_ok("a blob:none clone");
    harness
        .git(&target, &["--no-lazy-fetch", "cat-file", "-e", &readme])
        .expect_failure("a blob:none clone holds no blob yet");
    harness
        .git_http(
            &target,
            &["fetch", "-q", "--filter=blob:none", "origin", &readme],
        )
        .expect_ok("fetch of the tip's README.md by id");
    let shown = harness
        .git(&target, &["--no-lazy-fetch", "cat-file", "-p", &readme])
        .expect_ok("the readme arrived")
        .stdout
        .clone();
    assert_eq!(
        shown, "a readme\n",
        "the blob fetched by id is the pushed bytes"
    );
}