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

nordisk/znippy

plugins/native/znippy-plugin-git/src/thin_storm.rs

//! **What a repository pushed one commit at a time does to a verbatim
//! archive** — the shape that refused 21 of 140 ordinary pushes.
//!
//! # 🔴 The defect these tests exist for
//!
//! MEASURED on oden, 2026-08-30 (lane AI, reproducing lane AD's two-appliance
//! drill on a locally built server): 140 sequential single-commit pushes of one
//! file into one repository. Push #120 came back
//!
//! ```text
//!   ! [remote rejected] master -> master (the push cannot be checked: this pack
//!   is thin: it deltas against 509be6a75f6f… which is not in it, and no base
//!   source could produce that object's content … so this repository genuinely
//!   does not have that object)
//! ```
//!
//! and so did every one of the 20 pushes after it, forever. The object was a
//! blob this store held, in a pack it had already absorbed, one index lookup
//! away. Three separate faults stacked into that one sentence, and each has its
//! own guard below:
//!
//! 1. **The chain ceiling.** `git index-pack --fix-thin` completes a pushed
//!    thin pack by appending the resolved base into it, so a pack stock git
//!    stores has self-contained delta chains. §14 stores the client's bytes
//!    **verbatim**, which is what makes a clone a byte-range copy — so a
//!    `REF_DELTA` goes on naming a base in an *earlier pack*, and one commit per
//!    push grows the chain one link per push, without bound.
//!    [`crate::resolve::MAX_DELTA_LINKS`] refused the 101st link as "a cycle".
//! 2. **The swallowed error.** `impl BaseSource for Absorber` ended in
//!    `.ok().flatten()`, so *"I hold it and could not read it"* and *"I have
//!    never had it"* arrived as the same `None`, and the resolver blamed the
//!    pushed pack for the store's own failure.
//! 3. **The loop that could not move on.** Every absorb entry point ran
//!    `for job in &jobs { self.absorb_gated(job)?; }` in ordinal order, so one
//!    pack that could not be indexed was hit first, aborted the loop, and left
//!    every pack behind it un-indexed — which is why the failure was not one
//!    push but all of them from there on.
//!
//! # Why the assertion is the push's own result
//!
//! Every count below comes from [`GitStore::put_pack`] and
//! [`GitStore::index_now`] — the two calls `receive_pack::apply` makes before it
//! acks — and not from a log line. A log-line oracle would have been green for
//! the first 100 pushes of a run that was already broken.

#![cfg(test)]

use crate::git_ops::GitStore;
use crate::object::{canonical, GitHashKind, GitObjectKind};
use crate::pack_fixture::thin_ref_delta_pack;
use crate::store::tests::{one_blob_pack, tmpdir};
use git_storage_trait::GitOps;

/// The blob push number `i` writes. Content that deflate cannot collapse into
/// nothing, so every push really does store an entry.
fn body_of(i: usize) -> Vec<u8> {
    format!("lane AI storm commit {i}\n").repeat(3).into_bytes()
}

/// One push: a thin pack whose single entry is a `REF_DELTA` against the object
/// the push before it introduced. **This is the shape `git push` sends** when a
/// client commits one change on top of the ref the server just advertised, and
/// it is the shape that grows the chain.
fn storm(store: &GitStore, pushes: usize) -> (usize, Option<String>) {
    let seed = b"lane AI seed\n";
    let (p0, oid0) = one_blob_pack(seed);
    store.put_pack(&p0).expect("the seed pack");
    store.wait_indexed();

    let mut base_oid = oid0;
    let mut base_len = seed.len() as u64;
    let mut fails = 0usize;
    let mut first: Option<String> = None;
    for i in 0..pushes {
        let body = body_of(i);
        let pack = thin_ref_delta_pack(&base_oid, base_len, &body);
        // Exactly what the ack path does: store the pack, then make every
        // object it introduced addressable before the connectivity walk.
        let r = store
            .put_pack(&pack)
            .and_then(|_| store.index_now().map(|_| ()));
        if let Err(e) = r {
            fails += 1;
            if first.is_none() {
                first = Some(format!("push {i}: {e:#}"));
            }
        }
        base_oid = GitHashKind::Sha1.oid_of(&canonical(GitObjectKind::Blob, &body));
        base_len = body.len() as u64;
    }
    (fails, first)
}

/// ★ **150 single-commit pushes to one repository are all accepted.**
///
/// Seen RED at [`crate::resolve::MAX_DELTA_LINKS`] exactly: **50 of 150
/// refused**, the first at push 100, *"absorbing pack 101 at (9144, 92) … this
/// pack is thin: it deltas against 1e1a1751… which is not in it, and no base
/// source could produce that object's content"*. 150 rather than 101 because a
/// run that stops at the first failure cannot tell a one-push hiccup from the
/// permanent stop this was: every push after the first failure failed too.
#[test]
fn a_hundred_and_fifty_single_commit_pushes_are_all_accepted() {
    let dir = tmpdir("laneAI-storm-ack");
    let store = GitStore::open(&dir, "alice").unwrap();
    let (fails, first) = storm(&store, 150);
    assert_eq!(
        fails,
        0,
        "{} of 150 single-commit pushes were refused; the first was {}",
        fails,
        first.unwrap_or_default()
    );
    assert!(
        store.unindexable_packs().is_empty(),
        "the store gave up on {:?}",
        store.unindexable_packs()
    );
}

/// ★ **And the objects are readable afterwards, at every depth.**
///
/// The push landing is not the whole claim: a chain that is accepted and then
/// cannot be read back is the same defect one layer down (it is where the
/// original red actually lived — the *base* read failed, not the push's own
/// parse). Every blob the storm wrote is read back and compared byte for byte,
/// including the deepest.
#[test]
fn every_object_of_a_long_chain_reads_back_byte_correct() {
    let dir = tmpdir("laneAI-storm-read");
    let store = GitStore::open(&dir, "alice").unwrap();
    const N: usize = 150;
    let (fails, first) = storm(&store, N);
    assert_eq!(fails, 0, "{}", first.unwrap_or_default());
    store.wait_indexed();

    let hash = store.hash_kind();
    let mut checked = 0usize;
    for i in 0..N {
        let body = body_of(i);
        let oid = hash.oid_of(&canonical(GitObjectKind::Blob, &body));
        let (kind, payload) = store
            .content(&oid)
            .unwrap_or_else(|e| panic!("reading the blob of push {i} (chain depth {i}): {e:#}"))
            .unwrap_or_else(|| panic!("the blob of push {i} is absent after its push was acked"));
        assert_eq!(kind, GitObjectKind::Blob);
        assert_eq!(payload, body, "the blob of push {i} came back wrong");
        checked += 1;
    }
    assert_eq!(checked, N);
}

/// ★ **A chain that returns to an offset it has already stood on is refused as
/// a cycle** — the job [`crate::resolve::MAX_DELTA_LINKS`] used to do with a
/// count, done with a proof.
///
/// Two arms, and the second is the one that makes the first mean anything:
///
/// * a **300-link** legitimate chain — three times the old ceiling — resolves;
/// * a genuine cycle is refused **by name**, not walked forever.
///
/// The cycle is built where a cycle can actually exist in this store: an entry
/// whose `REF_DELTA` base oid is its own resolved oid. `offset_of_oid` sends the
/// walk straight back to the entry it is standing on, which no acyclic chain can
/// do and which the old count could only notice 100 links later.
#[test]
fn a_deep_chain_resolves_and_a_real_cycle_is_refused_by_name() {
    use crate::resolve::{materialize_at, ArchiveBytes, NoBases};

    // A one-entry "archive" that answers every offset with the same ref-delta
    // entry, whose base oid resolves back to offset 0.
    struct SelfReferential(Vec<u8>);
    impl ArchiveBytes for SelfReferential {
        fn at(&self, at: u64, len: u64) -> anyhow::Result<std::borrow::Cow<'_, [u8]>> {
            let at = at as usize;
            if at >= self.0.len() {
                return Ok(std::borrow::Cow::Owned(Vec::new()));
            }
            let end = (at + len as usize).min(self.0.len());
            Ok(std::borrow::Cow::Borrowed(&self.0[at..end]))
        }
    }
    let oid = vec![7u8; 20];
    let entry = {
        let delta = crate::pack_fixture::insert_only_delta(4, b"abcd");
        let mut e = Vec::new();
        crate::pack_walk::encode_type_and_size(
            &mut e,
            crate::index_layout::ObjType::RefDelta,
            delta.len() as u64,
        );
        e.extend_from_slice(&oid);
        e.extend_from_slice(&crate::pack_fixture::deflate(&delta));
        e
    };
    let err = materialize_at(
        &SelfReferential(entry),
        0,
        20,
        // Every base oid is at offset 0, which is where the walk started.
        &|_| Some(0),
        &NoBases,
    )
    .expect_err("a self-referential ref-delta must not resolve");
    let text = format!("{err:#}");
    assert!(
        text.contains("form a cycle") && text.contains("returns to archive offset 0"),
        "a cycle must be refused by name and by offset, not by a link count: {text}"
    );

    // And the legitimate deep chain the old count refused: three times the
    // ceiling that used to be a refusal. Three and not thirty because the cost
    // of a push here is the repository's whole history re-folded, so the arm
    // that matters is "past 100", not "how far past".
    let dir = tmpdir("laneAI-deep-chain");
    let store = GitStore::open(&dir, "alice").unwrap();
    let (fails, first) = storm(&store, 300);
    assert_eq!(
        fails,
        0,
        "a 300-link chain is deep, not corrupt: {}",
        first.unwrap_or_default()
    );
}

/// ★ **A base source that FAILS is not a base source that is empty.**
///
/// The seam lane AI's red hid behind. `BaseSource::content` returned a bare
/// `Option`, so a store that held the delta base and could not reconstruct it
/// answered identically to one that had never seen it — and
/// [`crate::resolve::ResolveError::missing_base`] then told the pushing client
/// *"this repository genuinely does not have that object"* about an object the
/// repository did have. A client cannot act on a diagnosis that names the wrong
/// party.
///
/// Seen RED by restoring the swallow — `bases.content(oid).ok().flatten()` at
/// the two call sites — which turns the source's own sentence into
/// `missing_base`'s: *"this pack is thin … so this repository genuinely does not
/// have that object"*, with the real reason gone.
#[test]
fn a_base_source_that_errors_says_so_instead_of_saying_absent() {
    use crate::resolve::{resolve, BaseSource};

    struct Broken;
    impl BaseSource for Broken {
        fn content(&self, _oid: &[u8]) -> anyhow::Result<Option<(GitObjectKind, Vec<u8>)>> {
            anyhow::bail!("the store holds this base and could not read it back")
        }
    }
    let base_oid = vec![9u8; 20];
    let pack = crate::pack_fixture::thin_ref_delta_pack(&base_oid, 4, b"abcd");
    let err = resolve(&pack, GitHashKind::Sha1, 0, &Broken)
        .expect_err("a thin pack whose base source fails must not resolve");
    let text = format!("{err:#}");
    assert!(
        text.contains("could not read it back"),
        "the base source's own sentence must survive to the caller: {text}"
    );
    assert!(
        !text.contains("genuinely does not have that object"),
        "a source that FAILED was reported as a repository that does not HOLD the object: {text}"
    );
}