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

nordisk/edda

crates/gunnar-pack/tests/ofs_delta_off.rs

//! **`P-001` for a client that cannot decode `OBJ_OFS_DELTA`.**
//!
//! `write_pack_copy` re-points a copied delta at its base. With the client's
//! `ofs-delta` capability that base is named by *distance backwards*
//! (`OBJ_OFS_DELTA`); without it, by *object id* (`OBJ_REF_DELTA`). The delta
//! bytes are identical either way, so the reuse survives in full — and until
//! 2026-08-03 the serve path did not copy for such a client at all, which
//! MEASURED **18.336× the bytes** on the churned head-to-head fixture.
//!
//! # Why this test exists, and why it uses stock `git`
//!
//! `gix-pack`'s own comment calls an `OBJ_REF_DELTA` whose base lies inside the
//! same pack *"a discontinued practice"*
//! (`gix-pack/src/data/output/entry/mod.rs`). Discontinued is not invalid — `git
//! index-pack` resolves ref-deltas against the pack's own objects before looking
//! outside it, which is precisely how it completes a thin pack — but "gunnar's
//! reader accepts it" would be worth nothing here. A reader written next to the
//! writer can be wrong in exactly the same way and still agree. So the verdict
//! comes from **`git index-pack --strict`** and **`git verify-pack -v`**, and the
//! pack is one gunnar produced.
//!
//! # The assertion that is not the obvious one
//!
//! "`git index-pack` accepted it" passes just as happily on a pack containing no
//! deltas at all — which is exactly the state this change exists to get out of,
//! and exactly what the fixture would produce if `git repack` found nothing to
//! delta. So every case here asserts on `git verify-pack -v`'s own **entry type
//! column**: the `ofs_delta: false` pack must contain `ref delta` entries and no
//! `ofs delta` ones, and the `ofs_delta: true` pack the reverse. That is applied
//! output, read out of stock git, and it is what makes the acceptance mean
//! something.
//!
//! **A missing `git` is a failure, not a skip** (`S-026`).

use std::path::Path;

use gix_hash::{Kind, ObjectId};
use gunnar_pack::{write_pack_copy, WriteOptions};

mod common;

// The git harness and the entry-form reader live in `common` so that this file
// and `tests/deltified.rs` cannot drift apart about what `verify-pack -v`
// prints (LAW 5). They were written here first.
use common::{churned_repo, entry_kinds, git, git_available, packed_objects};

/// Write `ids` with `write_pack_copy`, index it with stock `git index-pack
/// --strict`, and report what stock git says is inside.
fn serve_and_index(
    repo: &Path,
    out_dir: &Path,
    ids: &[ObjectId],
    ofs_delta: bool,
) -> (u64, usize, usize, usize) {
    let packed = packed_objects(repo);
    let options = WriteOptions::new(Kind::Sha1).ofs_delta(ofs_delta);
    let mut bytes = Vec::new();
    let outcome =
        write_pack_copy(ids, &packed, &mut bytes, &options).expect("writing the copied pack");
    assert_eq!(
        outcome.objects_recompressed, 0,
        "the fixture is packed, so every entry should have been copied; {} were recompressed",
        outcome.objects_recompressed
    );
    assert_eq!(
        outcome.objects_copied_from_pack,
        ids.len(),
        "only {} of {} entries were copied",
        outcome.objects_copied_from_pack,
        ids.len()
    );

    let pack = out_dir.join(format!("in-{ofs_delta}.pack"));
    std::fs::write(&pack, &bytes).expect("saving the pack");
    // `--strict` turns "the bytes parsed" into "every object in it is valid and
    // every delta resolved". This is the whole safety argument for emitting an
    // OBJ_REF_DELTA whose base is inside the same pack.
    git(
        &["index-pack", "--strict", "-v", &pack.to_string_lossy()],
        out_dir,
    );
    let idx = pack.with_extension("idx");
    assert!(
        idx.is_file(),
        "git index-pack wrote no .idx next to {pack:?}"
    );
    let (base, ofs, refs) = entry_kinds(&idx, &pack, out_dir);
    (bytes.len() as u64, base, ofs, refs)
}

#[test]
fn a_client_without_ofs_delta_still_gets_the_copied_deltas() -> anyhow::Result<()> {
    assert!(
        git_available(),
        "this test requires `git` on PATH; a pack format claim cannot be proven without it"
    );
    let repo = tempfile::tempdir()?;
    let work = tempfile::tempdir()?;
    let ids = churned_repo(repo.path());

    let (ofs_bytes, ofs_base, ofs_ofs, ofs_ref) =
        serve_and_index(repo.path(), work.path(), &ids, true);
    let (raw_bytes, raw_base, raw_ofs, raw_ref) =
        serve_and_index(repo.path(), work.path(), &ids, false);

    // ── the fixture is load-bearing, so it is asserted, not assumed ─────────
    assert!(
        ofs_ofs > 0,
        "the ofs-delta pack holds {ofs_ofs} ofs-deltas over {ofs_base} bases — the fixture \
         deltas to nothing, so neither case below proves anything"
    );

    // ── the applied output: which entry form stock git found ───────────────
    assert_eq!(
        ofs_ref, 0,
        "the ofs-delta pack should name every base by distance, but git found {ofs_ref} \
         ref-deltas in it"
    );
    assert_eq!(
        raw_ofs, 0,
        "a client without the capability was sent {raw_ofs} OBJ_OFS_DELTA entries it cannot \
         decode"
    );
    assert_eq!(
        raw_ref, ofs_ofs,
        "the two packs should carry the same deltas under different names: {ofs_ofs} \
         ofs-deltas against {raw_ref} ref-deltas"
    );
    assert_eq!(
        raw_base, ofs_base,
        "the two packs should hold the same base objects: {ofs_base} against {raw_base}"
    );

    // ── and the cost of the rename, which is the only thing that may differ ─
    //
    // One full object id per delta entry in place of a varint distance. At sha1
    // that is 20 bytes against 1-3, so the ceiling is derived rather than picked:
    // anything materially above it means something other than the header grew.
    let ceiling = ofs_bytes + 20 * raw_ref as u64;
    assert!(
        raw_bytes > ofs_bytes && raw_bytes <= ceiling,
        "the ref-delta pack is {raw_bytes} bytes against {ofs_bytes}; one base id per delta \
         accounts for at most {ceiling}"
    );
    Ok(())
}

/// **What this one cannot catch, stated rather than left to be found.** With the
/// flag ignored — the pre-fix behaviour — both packs come out identical and this
/// comparison passes trivially. It went green under exactly that edit while the
/// test above went red. It is not hollow: it exists to catch the two forms
/// *decoding differently*, which is a distinct failure from emitting the wrong
/// form, and the form is what the other test asserts.
#[test]
fn the_two_forms_carry_byte_identical_delta_data() -> anyhow::Result<()> {
    assert!(git_available(), "this test requires `git` on PATH");
    let repo = tempfile::tempdir()?;
    let ids = churned_repo(repo.path());
    let packed = packed_objects(repo.path());
    // Same fixture guard as the other test: on a corpus that deltas to nothing
    // the two forms are trivially identical and this proves nothing.
    {
        let work = tempfile::tempdir()?;
        let (_, _, ofs, _) = serve_and_index(repo.path(), work.path(), &ids, true);
        assert!(
            ofs > 0,
            "the fixture holds no deltas, so this comparison is vacuous"
        );
    }

    // The claim this whole change rests on: only the *header* differs. Read it
    // back out of both packs with gix's own decoder and compare the objects —
    // not the headers, which are supposed to differ, and not gunnar's own tally,
    // which is what produced them.
    let mut both = Vec::new();
    for ofs_delta in [true, false] {
        let options = WriteOptions::new(Kind::Sha1).ofs_delta(ofs_delta);
        let mut bytes = Vec::new();
        write_pack_copy(&ids, &packed, &mut bytes, &options)?;
        let dir = tempfile::tempdir()?;
        let pack = dir.path().join("p.pack");
        std::fs::write(&pack, &bytes)?;
        git(
            &["index-pack", "--strict", &pack.to_string_lossy()],
            dir.path(),
        );
        // `cat-file --batch` through stock git, so the comparison is of what a
        // real client would end up holding.
        let odb = dir.path().join("odb");
        std::fs::create_dir_all(&odb)?;
        git(&["init", "-q", "--bare", "--object-format=sha1", "."], &odb);
        std::fs::copy(&pack, odb.join("objects/pack/p.pack"))?;
        std::fs::copy(pack.with_extension("idx"), odb.join("objects/pack/p.idx"))?;
        let mut digest: Vec<String> = Vec::with_capacity(ids.len());
        for id in &ids {
            let hex = id.to_string();
            let kind = git(&["cat-file", "-t", &hex], &odb).trim().to_string();
            let size = git(&["cat-file", "-s", &hex], &odb).trim().to_string();
            digest.push(format!("{hex} {kind} {size}"));
        }
        both.push(digest);
        // `dir` must outlive the reads above.
        drop(dir);
    }
    assert_eq!(
        both[0], both[1],
        "the two packs decode to different objects, so the delta bytes were not the same"
    );
    Ok(())
}