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/src/lib.rs

//! **`GitOps` over `gix`** — the second implementation of the frozen
//! `git-storage-trait` contract, opposite znippy's Arrow-IPC one.
//!
//! | implementer | how it stores |
//! |---|---|
//! | `znippy-plugin-git` | Arrow-IPC in a znippy archive (OpenZL) |
//! | **`storage-git-gix`** (this crate) | `gix-odb` / `gix-ref` on a plain filesystem |
//!
//! The store is an ordinary **bare git repository**. That is the point of it:
//! whatever this crate writes, stock `git` can read, and `tests/` proves it by
//! running real `git` against the directory rather than by asserting against
//! this crate's own reader.
//!
//! # No Arrow, and it is structural
//!
//! `git-storage-trait` depends on `anyhow` and nothing else, deliberately, so
//! that a gix backend can implement it without linking Arrow and a znippy
//! backend without linking gix. This crate's graph honours that: `cargo tree`
//! contains no `arrow*` crate. Nothing here re-exports a znippy type, and there
//! is no `znippy-common` or `znippy-plugin-git` edge.
//!
//! # No rayon (LAW 3)
//!
//! *"Pure `std::thread::scope`; no global pool, no rayon."* Nothing in this
//! crate spawns a thread at all yet, so the law is satisfied trivially rather
//! than carefully — but the two places a fan-out will eventually go are already
//! decided and written down: [`gc`] (batch tier, fan-out wanted) and
//! [`graph`] (serving tier, serial **on purpose**, `P-004`).
//!
//! `gix-features/parallel` **is** enabled, and that is not a violation: it is
//! `crossbeam-channel` + `parking_lot`, not rayon, verified in gix-features
//! 0.49's manifest. Leaving it off is its own defect — `P-002`, *"the appliance
//! build silently uses the serial code path"*.
//!
//! # What this crate does NOT claim
//!
//! - **No delta encoder**, so [`gc`] sweeps whole dead packs and never rewrites
//!   a live one. `P-009`.
//! - **No writer lock.** Two processes writing one store root is not defended
//!   against here, exactly as `P-014` says of the other backend.
//!
//! Every one of those is a named, bounded absence rather than a stub that
//! returns a plausible wrong answer.
//!
//! # The reachability bitmap, which this crate now has
//!
//! The list above used to open with *"**No reachability bitmap.** gitoxide has
//! none; [`graph`] walks."* The first half is still true and the second is no
//! longer the whole story. gitoxide has no bitmap support at all — no `ewah`, no
//! reader in `gix-pack`, `gix-odb` or `gix-features` — so edda's `gunnar-bitmap`
//! is the format, [`bitmap`] is the half that needs an object database, [`gc`]
//! writes the file and [`graph`] is the fallback for every request a bitmap
//! cannot answer exactly.
//!
//! What the bitmap replaces is not a micro-cost. A clone off a bitmapped pack is
//! **one `OR` per want and no object read**, against a walk that decodes every
//! commit and every tree in the repository.

pub mod bitmap;
pub mod delta_cache;
pub mod fanout;
pub mod gc;
pub mod graph;
pub mod midx;
pub mod odb;
pub mod packs;
pub mod refs;
pub mod serve;

use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::RwLock;

use anyhow::{anyhow, Context, Result};
use git_storage_trait::{
    Extent, GcReport, GitOps, ObjType, Oid, RefCas, RefRow, RefUpdate, Stored, TxId,
};
use gix_hash::ObjectId;

use crate::packs::PackSet;

/// A bare git repository, serving the eleven.
pub struct GixStore {
    git_dir: PathBuf,
    hash_kind: gix_hash::Kind,
    /// The verbatim-read path: `.idx` + `.pack` + the sorted-offset table.
    /// Rebuilt after every `put_pack`, which is why it is behind a lock.
    packs: RwLock<PackSet>,
    /// The decode path. Used for `size` (a delta's resolved size needs the
    /// chain walked) and for [`graph`]'s object reads. Never used to answer
    /// `get`, because it cannot — see [`packs`]'s module doc on `P-025`.
    ///
    /// A **pool**, not a handle, and that is `P-012`'s answer lifted out of the
    /// quarantine wholesale: every pooled handle is `refresh_never()`, so a miss
    /// is a binary search rather than a re-scan of `objects/pack` behind the
    /// store's global write lock, and the re-scan is put back exactly where it
    /// is needed by [`odb::OdbPool::refresh_if_stale`]'s one `statx`. See
    /// [`odb`]'s module doc for the numbers, all of which are **oden's**.
    odb: odb::OdbPool,
    refs: gix_ref::file::Store,
    /// Monotonic within one process. The contract calls `push_seq` "the ref
    /// log's ordering authority"; this backend keeps no ref log, so the honest
    /// thing it can offer is an in-process sequence, and [`GixStore::put_refs`]
    /// documents that it does not survive a restart.
    seq: AtomicU64,
    /// **One writer over `objects/pack`.** Held by [`GixStore::gc`], which
    /// unlinks packs, and by [`midx::MULTI_PACK_INDEX`]'s writer, which names
    /// them. Those two interleaved produce an index pointing at a pack that is
    /// not there, which is a lookup failure the reader cannot distinguish from a
    /// genuine absence — so this is a lock and not a comment.
    ///
    /// It is *not* a store lock. Two processes over one directory are still
    /// undefended (`P-014`, and the module header says so).
    maint_lock: std::sync::Mutex<()>,
    /// The `.bitmap` beside the one pack that has one, re-`stat`ed per request
    /// and reloaded when it moves. See [`bitmap::BitmapCache`] for why a stale
    /// one is a **wrong** answer rather than a slow one.
    bitmaps: bitmap::BitmapCache,
}

impl GixStore {
    /// Open an existing bare repository, or create one.
    ///
    /// The hash kind is read from the repository's own `config`
    /// (`extensions.objectFormat`), which is where git puts it, so a sha256
    /// repository is opened as sha256 without the caller having to know.
    ///
    /// # The `P-008` consequence, stated at the door
    ///
    /// `gix-hash` builds its sha1 hasher as
    /// `sha1_checked::Builder::default().safe_hash(false).build()` — collision
    /// **detection** stays on, so it runs `sha1-checked`'s pure-Rust
    /// `compress::compress` and never reaches `sha1`'s SHA-NI path. A **sha1**
    /// repository therefore hashes several times slower than a **sha256** one
    /// on this hardware. That matches git's own `sha1dc` default, so it is not
    /// a gitoxide defect and this crate does not work around it. It is the
    /// reason a benchmark must state which hash kind its fixture used.
    pub fn open_or_init(git_dir: impl AsRef<Path>, hash_kind: gix_hash::Kind) -> Result<Self> {
        let git_dir = git_dir.as_ref().to_path_buf();

        if git_dir.join("HEAD").exists() {
            let stored = stored_hash_kind(&git_dir)?;
            if stored != hash_kind {
                return Err(anyhow!(
                    "repository at {} is {stored:?}, but was opened as {hash_kind:?}",
                    git_dir.display()
                ));
            }
        } else {
            init_bare(&git_dir, hash_kind)?;
        }

        let objects_dir = git_dir.join("objects");
        // gix-odb 0.85 (gix 0.88) takes the hash kind as an argument instead of an
        // `Options` field.
        let store = gix_odb::Store::at_opts(
            objects_dir.clone(),
            hash_kind,
            &mut std::iter::empty(),
            gix_odb::store::init::Options::default(),
        )
        .with_context(|| format!("open odb at {}", objects_dir.display()))?;
        // `OdbPool::new` scans once, so the first lookup is neither wrong nor
        // slow, and sizes the slot table from what is on disk.
        let odb = odb::OdbPool::new(std::sync::Arc::new(store), &git_dir, hash_kind);

        let refs = gix_ref::file::Store::at_opts(
            git_dir.clone(),
            hash_kind,
            gix_ref::store::init::Options {
                // A git server has no use for a per-ref reflog file tree, and
                // `GitOps` exposes none. Stating it here rather than reading
                // `core.logallrefupdates` back is part of what keeps
                // `gix-config` out of this crate's build.
                write_reflog: gix_ref::store::WriteReflog::Disable,
                ..Default::default()
            },
        );

        let packs = PackSet::load(&objects_dir.join("pack"), hash_kind)?;

        let store = GixStore {
            git_dir,
            hash_kind,
            packs: RwLock::new(packs),
            odb,
            refs,
            seq: AtomicU64::new(0),
            maint_lock: std::sync::Mutex::new(()),
            bitmaps: bitmap::BitmapCache::default(),
        };
        // Where git puts it too: `git multi-pack-index write` is maintenance,
        // and the one moment a server can afford a pass over every pack is
        // before it starts serving. Above [`midx::MULTI_PACK_INDEX_THRESHOLD`]
        // packs only, never on the read path, and a failure is logged rather
        // than propagated — see the method.
        store.write_multi_pack_index_if_worthwhile();
        Ok(store)
    }

    pub fn git_dir(&self) -> &Path {
        &self.git_dir
    }

    pub fn hash_kind(&self) -> gix_hash::Kind {
        self.hash_kind
    }

    /// The underlying `gix-odb` store, for the one caller that needs a handle of
    /// its own rather than a lease: `P-001`'s pack-copy source.
    ///
    /// A lease will not do there. `gunnar_pack::PackedObjects` clones its handle
    /// once per worker and `entry_by_location` hands out slices pointing into a
    /// mapped pack, so that handle needs `prevent_pack_unload()` — which a
    /// pooled handle deliberately does **not** have, because it would pin every
    /// pack a gc replaced for as long as the handle sat idle in the pool.
    pub fn odb_store(&self) -> std::sync::Arc<gix_odb::Store> {
        self.odb.store()
    }

    /// `(staleness probes, of those, full consolidations)` — how many times a
    /// miss has asked the filesystem whether a pack appeared, and how often the
    /// answer was yes.
    ///
    /// **A count, not a clock**, which is what `locked_on_target.md` §6 asks a
    /// bench arm to prefer: it does not move with load, and it is the only thing
    /// that can tell a `refresh_never()` pool from the default one. Both answer
    /// identically; only this says what the answer cost.
    pub fn rescan_counts(&self) -> (u64, u64) {
        self.odb.rescan_counts()
    }

    /// `(slots, times grown)` in the odb's index slot table.
    ///
    /// Observable because the failure it guards against is not: a full table
    /// makes `gix-odb` report objects in later packs absent, silently, for the
    /// life of the process. See [`odb::OdbPool::grow_slots_if_full`].
    pub fn slot_table(&self) -> (u64, u64) {
        self.odb.slot_table()
    }

    /// How many `.keep` markers this process failed to remove.
    ///
    /// **A count, not a log line** — see [`drop_keep`] for why the failure
    /// matters here specifically (a stale `.keep` is a pack [`gc`] will never
    /// reclaim). Process-wide rather than per-store: the counter exists to be
    /// asserted and to be read off a bench row, and no caller has two stores.
    pub fn keep_removal_failures() -> u64 {
        KEEP_REMOVAL_FAILURES.load(Ordering::Relaxed)
    }

    /// How many separately-searched pack indices a miss walks — the `M` in
    /// `P-012`'s `O(M)`. One `.idx` each, or **exactly one** when a
    /// `multi-pack-index` folds them. See [`odb::OdbPool::index_slots`] and
    /// [`midx`].
    pub fn index_slots(&self) -> usize {
        self.odb.index_slots()
    }

    /// `(selections answered from a bitmap, selections that fell back to the
    /// walk)`, process-wide.
    ///
    /// **A count, not a clock**, and it is not optional instrumentation: the two
    /// paths are *defined* to return the same object set, so no wire byte, no
    /// `fsck` and no exit code can tell them apart. `P-027` is that failure
    /// already having happened once — *"five instruments measured the walk path
    /// and published it as the bitmap path"* — and a benchmark row quoting a
    /// bitmap win it never took is worse than no row.
    ///
    /// Process-wide rather than per-store for the same reason
    /// [`GixStore::keep_removal_failures`] is: the counter exists to be asserted
    /// off a test and read off a bench row, and no caller has two stores.
    pub fn bitmap_selection_counts() -> (u64, u64) {
        (
            BITMAP_SELECTIONS.load(Ordering::Relaxed),
            WALK_SELECTIONS.load(Ordering::Relaxed),
        )
    }

    /// Commits opened by the bitmap path's bounded walk, process-wide.
    ///
    /// The one guard that can see a bounded walk that stopped being bounded.
    /// ORing a commit's stored bitmap in and then walking its parents anyway
    /// changes **no answer** — a bitmap is closed under reachability, so
    /// everything the extra walk finds is already set — so every correctness
    /// test in the crate stays green while the cost comes back. Only a count
    /// fails on it.
    pub fn bitmap_commits_walked() -> u64 {
        BITMAP_COMMITS_WALKED.load(Ordering::Relaxed)
    }

    /// Write a reachability bitmap beside the pack best placed to carry one.
    ///
    /// This is git's `repack -b`, minus the repack (`P-009`: gitoxide has no
    /// delta encoder, so this backend never consolidates). Called from
    /// [`GitOps::gc`], and public so a maintenance command or a benchmark can
    /// ask for it without also sweeping.
    ///
    /// # Which pack, and why "the biggest" is the honest rule
    ///
    /// A `.bitmap`'s positions are **one pack's** positions, so a store with
    /// many packs can only have a bitmap over one of them — everything else is
    /// reached through the extended index and the bounded walk. The biggest pack
    /// is the one that leaves the least for that walk to do.
    ///
    /// [`bitmap::write_for_pack`] then refuses on its own terms if that pack
    /// cannot carry an exact answer — a commit whose parent is elsewhere, a tree
    /// reaching an object that is not there — and `Ok(None)` is the result, not
    /// an error. A store that gets no bitmap is served exactly as it was before.
    ///
    /// # Every other `.bitmap` is unlinked first
    ///
    /// [`bitmap::BitmapCache`] declines a directory holding more than one,
    /// because two bitmaps address different position spaces and neither covers
    /// the other's objects. Leaving a stale one behind would therefore turn the
    /// whole feature **off** rather than produce a wrong answer — quietly, and
    /// in exactly the shape a benchmark cannot see.
    pub fn write_reachability_bitmap(&self) -> Result<Option<PathBuf>> {
        let _one_writer = self
            .maint_lock
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);
        self.write_reachability_bitmap_locked()
    }

    /// [`GixStore::write_reachability_bitmap`], for a caller already holding
    /// [`GixStore::maint_lock`] — `std::sync::Mutex` is not reentrant.
    fn write_reachability_bitmap_locked(&self) -> Result<Option<PathBuf>> {
        let target = {
            let packs = self
                .packs
                .read()
                .map_err(|_| anyhow!("pack set lock poisoned"))?;
            packs
                .packs
                .iter()
                .max_by_key(|p| p.num_objects())
                .map(|p| (p.idx_path.clone(), p.num_objects()))
        };
        let Some((idx_path, objects)) = target else {
            return Ok(None);
        };
        if objects == 0 {
            return Ok(None);
        }

        // Stale bitmaps go before the new one lands, so the directory never
        // holds two — see the doc above for why that is a correctness matter
        // and not tidiness.
        let keep = bitmap::path_for(&idx_path);
        for entry in std::fs::read_dir(self.pack_dir())?.flatten() {
            let path = entry.path();
            if path.extension().is_some_and(|e| e == "bitmap") && path != keep {
                let _ = std::fs::remove_file(&path);
            }
        }

        let written = self
            .odb
            .with_handle(|odb| bitmap::write_for_pack(&idx_path, self.hash_kind, odb))?;
        if written.is_none() {
            // Nothing was written, so anything already sitting at `keep` now
            // describes a pack that may have moved under it. `PackBitmap::at`
            // would catch that by checksum, but leaving it costs a parse per
            // reload for a file that can never be used.
            let _ = std::fs::remove_file(&keep);
        }
        // The cache keys on `(path, mtime, len)` and cannot see a *removal*, so
        // it is told rather than left to notice.
        self.bitmaps.clear();
        Ok(written)
    }

    /// Packs carrying a 16-bit fanout, and the bytes they cost. See
    /// [`fanout`] — off unless `GIX_FANOUT16=on`.
    pub fn fanout16_footprint(&self) -> Result<(usize, usize)> {
        Ok(self
            .packs
            .read()
            .map_err(|_| anyhow!("pack set lock poisoned"))?
            .fanout16_footprint())
    }

    fn pack_dir(&self) -> PathBuf {
        self.git_dir.join("objects").join("pack")
    }

    /// Re-scan `objects/pack`. Called after a pack lands.
    ///
    /// Refreshes the odb pool too, and that is not belt-and-braces: every
    /// pooled handle is `refresh_never()`, so nothing else would ever tell them
    /// a pack had appeared inside the racy one-second window where the mtime
    /// watch cannot. This is git's `reprepare_packed_git()`, called from the
    /// writer, which is where git calls it.
    fn reload_packs(&self) -> Result<()> {
        self.odb.refresh();
        let fresh = PackSet::load(&self.pack_dir(), self.hash_kind)?;
        *self
            .packs
            .write()
            .map_err(|_| anyhow!("pack set lock poisoned"))? = fresh;
        Ok(())
    }

    /// Turn the contract's borrowed raw oid into gix's.
    ///
    /// The contract's `Oid<'a>` is `&[u8]` — raw bytes, not hex — so a wrong
    /// width here is a caller passing a sha1 into a sha256 repository, and it
    /// must be an error rather than a silent truncation.
    fn oid(&self, raw: Oid<'_>) -> Result<ObjectId> {
        ObjectId::try_from(raw).map_err(|_| {
            anyhow!(
                "object id is {} bytes, but this repository is {:?} ({} bytes)",
                raw.len(),
                self.hash_kind,
                self.hash_kind.len_in_bytes()
            )
        })
    }

    /// One object's resolved header, through the pool's retry loop.
    ///
    /// `AskTheDisk`, not `Answer`: a header miss is asked by `size()` and by
    /// `get()`'s delta path, and both of those are about an object the caller
    /// has reason to believe is here. A stale `false` there is a refused fetch,
    /// which is the exact failure `gunnar.endurance` saw — so this one pays the
    /// `statx` when it misses, and the retry loop covers the interleavings the
    /// `statx` cannot see.
    ///
    /// An **error is not a hit**. `gix-odb` reports an unresolvable ref-delta
    /// base as `DeltaBaseMissing` rather than as `Ok(None)`, and a base sitting
    /// in a pack written after the snapshot is precisely what the retry is for.
    fn header(&self, id: &gix_hash::oid) -> Result<Option<gix_odb::find::Header>> {
        use gix_odb::Header as _;
        self.odb
            .lookup(
                |odb| odb.try_header(id),
                |r: &std::result::Result<_, _>| matches!(r, Ok(Some(_))),
            )
            .map_err(|e| anyhow!("header for {id}: {e}"))
    }

    /// The current ref tips, as object ids. Symbolic refs are followed.
    fn tip_ids(&self) -> Result<Vec<ObjectId>> {
        let mut out = Vec::new();
        for row in self.refs()? {
            if let Some(raw) = row.oid {
                if let Ok(id) = ObjectId::try_from(raw.as_slice()) {
                    out.push(id);
                }
            }
        }
        Ok(out)
    }

    /// Peel an annotated tag to what it points at, following a chain of tags.
    fn peel(&self, id: &gix_hash::oid) -> Option<ObjectId> {
        use gix_object::Find;
        // One lease for the whole chain, not one per hop: the chain is at most
        // sixteen reads and they are the reads most likely to share a delta
        // base with each other.
        self.odb.with_handle(|odb| {
            let mut buf = Vec::new();
            let mut current = id.to_owned();
            // A tag pointing at a tag is legal; a *cycle* of them is not, and
            // this is untrusted input. Bound the walk rather than trusting the
            // data.
            for _ in 0..16 {
                let data = odb.try_find(&current, &mut buf).ok()??;
                if data.kind != gix_object::Kind::Tag {
                    return (current != id).then_some(current);
                }
                current = gix_object::TagRefIter::from_bytes(data.data, self.hash_kind)
                    .target_id()
                    .ok()?;
            }
            None
        })
    }
}

/// How many `.keep` markers could not be removed, process-wide.
///
/// See [`drop_keep`].
static KEEP_REMOVAL_FAILURES: AtomicU64 = AtomicU64::new(0);

/// Selections answered from a `.bitmap`. See
/// [`GixStore::bitmap_selection_counts`].
pub(crate) static BITMAP_SELECTIONS: AtomicU64 = AtomicU64::new(0);
/// Selections that fell back to [`graph`]'s walk.
pub(crate) static WALK_SELECTIONS: AtomicU64 = AtomicU64::new(0);
/// Commits the bitmap path's bounded walk had to open. See
/// [`GixStore::bitmap_commits_walked`].
pub(crate) static BITMAP_COMMITS_WALKED: AtomicU64 = AtomicU64::new(0);

/// Remove a `.keep` marker, and **count it** if it will not go.
///
/// `S-030` in the charred record, and the quarantine's
/// `tests/swallowed_errors.rs` is a whole file about this one shape. Both call
/// sites here were `let _ = std::fs::remove_file(keep)`, which is the swallow
/// that finding is named after — and in *this* crate it has a specific
/// consequence rather than a general untidiness:
///
/// > [`gc::apply`] **skips a pack that has a `.keep` beside it, even when the
/// > pack looks entirely dead**, because that marker is the receive path's
/// > claim on a push whose refs have not landed. A `.keep` that could not be
/// > removed is therefore a pack no garbage collection will ever reclaim, for
/// > the life of the repository, silently.
///
/// # Why it does not return a `Result`
///
/// The pack the marker names is complete and correct; failing a finished push
/// over a leftover marker would turn a cosmetic fault into an operator-visible
/// error. What was wrong before is that nothing was *told*.
///
/// # Why a counter and not only a log line
///
/// A log line can only be asserted with a subscriber, and §6's rule in this
/// crate is to **prefer a count**: [`GixStore::keep_removal_failures`] is
/// readable from a test and from a bench row without a `tracing-subscriber`
/// dependency, and it cannot be satisfied by a message that merely looks right.
/// Both are emitted — the count so it can be asserted, the line so an operator
/// learns *which* file.
pub fn drop_keep(path: &Path) {
    match std::fs::remove_file(path) {
        Ok(()) => {}
        // Already gone is the outcome this function wants, not a failure.
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
        Err(err) => {
            KEEP_REMOVAL_FAILURES.fetch_add(1, Ordering::Relaxed);
            tracing::error!(
                event = "store.keep.not_removed",
                keep = %path.display(),
                %err,
                "the .keep marker could not be removed; the pack it names is complete, but \
                 this marker is now stale and `gc` will never reclaim that pack"
            );
        }
    }
}

/// Read `extensions.objectFormat` out of the repository config.
///
/// A hand-rolled read of one key, rather than a `gix-config` dependency. The
/// grammar accepted is the one git itself writes on `git init`; anything more
/// exotic falls back to sha1, which is git's default and therefore the right
/// thing to be wrong towards.
fn stored_hash_kind(git_dir: &Path) -> Result<gix_hash::Kind> {
    let path = git_dir.join("config");
    let Ok(text) = std::fs::read_to_string(&path) else {
        return Ok(gix_hash::Kind::Sha1);
    };
    for line in text.lines() {
        let line = line.trim();
        let Some((k, v)) = line.split_once('=') else {
            continue;
        };
        if k.trim().eq_ignore_ascii_case("objectformat") {
            return match v.trim() {
                "sha256" => Ok(gix_hash::Kind::Sha256),
                "sha1" => Ok(gix_hash::Kind::Sha1),
                other => Err(anyhow!("unknown extensions.objectFormat {other:?}")),
            };
        }
    }
    Ok(gix_hash::Kind::Sha1)
}

/// Create a bare repository stock `git` will accept.
///
/// Written by hand rather than by shelling out, so the crate has no runtime
/// dependency on a `git` binary. `tests/` then runs real `git` against the
/// result, which is what makes this honest: the layout is asserted by the
/// implementation that has to read it, not by the one that wrote it.
fn init_bare(git_dir: &Path, hash_kind: gix_hash::Kind) -> Result<()> {
    std::fs::create_dir_all(git_dir.join("objects").join("pack"))?;
    std::fs::create_dir_all(git_dir.join("objects").join("info"))?;
    std::fs::create_dir_all(git_dir.join("refs").join("heads"))?;
    std::fs::create_dir_all(git_dir.join("refs").join("tags"))?;
    std::fs::write(git_dir.join("HEAD"), b"ref: refs/heads/main\n")?;

    // `repositoryformatversion` MUST be 1 for sha256: version 0 has no
    // `extensions` section, and a git that reads a sha256 repository as
    // version 0 misreads every object id in it.
    let config = match hash_kind {
        gix_hash::Kind::Sha256 => {
            "[core]\n\trepositoryformatversion = 1\n\tfilemode = true\n\tbare = true\n\
             \tlogallrefupdates = false\n[extensions]\n\tobjectFormat = sha256\n"
        }
        // `gix_hash::Kind` is `#[non_exhaustive]`. A hash kind this crate has
        // never seen must be a hard error and not a silent fallback to sha1:
        // writing a sha1 config over a repository whose objects are some third
        // width would misread every object id in it.
        gix_hash::Kind::Sha1 => {
            "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = true\n\
             \tlogallrefupdates = false\n"
        }
        other => {
            return Err(anyhow!(
                "hash kind {other:?} has no known repository config layout"
            ))
        }
    };
    std::fs::write(git_dir.join("config"), config)?;
    Ok(())
}

impl GitOps for GixStore {
    // ── STORE ───────────────────────────────────────────────────────────────

    /// One push: pack bytes durable **first**, then the refs that point into
    /// them.
    ///
    /// The order is the contract's and the reason is stated there: a crash
    /// between the two leaves objects nobody points at, which a gc reclaims,
    /// whereas the reverse leaves a ref pointing at objects that are not there,
    /// which no later pass repairs.
    ///
    /// The `.keep` file `put_pack` leaves behind is removed **after** the ref
    /// transaction commits, which is precisely what it is for: between the two
    /// steps the pack's objects are unreferenced, and a concurrent `gc` without
    /// the `.keep` would be correct to delete the push.
    fn put(&self, pack: &[u8], refs: &[RefUpdate]) -> Result<TxId> {
        let (mut tx, keep) = self.write_pack(pack)?;
        let ref_tx = self.put_refs(refs)?;
        if let Some(keep) = keep {
            drop_keep(&keep);
        }
        tx.push_seq = ref_tx.push_seq;
        Ok(tx)
    }

    /// The pack half, on its own. The `.keep` is dropped immediately, because
    /// no ref transaction is coming to justify holding it.
    fn put_pack(&self, bytes: &[u8]) -> Result<TxId> {
        let (tx, keep) = self.write_pack(bytes)?;
        if let Some(keep) = keep {
            drop_keep(&keep);
        }
        Ok(tx)
    }

    /// The ref half, on its own — one all-or-nothing gix transaction.
    ///
    /// `push_seq` is an **in-process** counter. This backend keeps no ref log,
    /// so there is no durable ordering authority to report and inventing one
    /// that resets to 0 on restart while looking persistent would be worse than
    /// saying so. See the note on [`GixStore::seq`].
    fn put_refs(&self, updates: &[RefUpdate]) -> Result<TxId> {
        if updates.is_empty() {
            return Ok(TxId::default());
        }
        let edits = updates
            .iter()
            .map(|u| refs::to_edit(u, self.hash_kind))
            .collect::<Result<Vec<_>>>()?;

        let existing: Vec<String> = self.refs()?.into_iter().map(|r| r.name).collect();
        refs::precheck_name_conflict(&edits, &existing)?;
        // `S-023`: evaluate the expectations before gix can short-circuit them.
        refs::precheck_cas(&edits, |name| {
            Ok(self
                .refs
                .try_find(name.as_ref())
                .with_context(|| "read ref")?
                .map(|r| r.target))
        })?;

        // gix prepares every edit — taking a lock and checking the expectation
        // for each — before committing any, and a preparation failure rolls the
        // whole batch back. That is the all-or-nothing this method promises,
        // implemented by gix rather than re-implemented here.
        //
        // Both locks fail immediately rather than waiting: a server that blocks
        // on a contended reference turns a lost race into a stalled connection.
        self.refs
            .transaction()
            .prepare(
                edits,
                gix_lock::acquire::Fail::Immediately,
                gix_lock::acquire::Fail::Immediately,
            )
            .map_err(|e| anyhow!("ref transaction refused: {e}"))?
            .commit(None)
            .map_err(|e| anyhow!("ref transaction failed to commit: {e}"))?;

        Ok(TxId {
            pack_id: None,
            extent: None,
            push_seq: Some(self.seq.fetch_add(1, Ordering::SeqCst)),
        })
    }

    // ── READ ────────────────────────────────────────────────────────────────

    /// The stored bytes of one object: the pack entry, header and all.
    ///
    /// Sliced out of the mmap by offset arithmetic — nothing is inflated to
    /// find where the entry ends (`P-025`). The single copy is the `to_vec`
    /// the contract forces by making `Stored::bytes` an owned `Vec<u8>`.
    ///
    /// Returns `None` for a **loose** object. That is a real limitation and it
    /// is honest rather than papered over: a loose object has no pack entry, so
    /// there are no verbatim pack bytes to return and no `ObjType` in
    /// `{OfsDelta, RefDelta}` that could describe it. Nothing this crate writes
    /// is ever loose — `put_pack` writes packs — so this is reachable only for
    /// a repository that was populated by something else.
    fn get(&self, oid: Oid<'_>) -> Result<Option<Stored>> {
        let id = self.oid(oid)?;
        let packs = self
            .packs
            .read()
            .map_err(|_| anyhow!("pack set lock poisoned"))?;
        let Some(pack) = packs.find(&id) else {
            return Ok(None);
        };
        let Some(loc) = pack.locate(&id) else {
            return Ok(None);
        };
        let Some((bytes, extent)) = pack.entry_bytes(&id) else {
            return Ok(None);
        };

        // For a base type the entry header's size **is** the object size. For a
        // delta it is the size of the instructions, so the chain has to be
        // walked — and `try_header` walks headers only, never inflating a
        // payload it does not need.
        let uncompressed_size = if loc.is_delta {
            self.header(&id)?
                .map(|h| h.size())
                .unwrap_or(loc.header_size)
        } else {
            loc.header_size
        };

        Ok(Some(Stored {
            obj_type: loc.obj_type,
            uncompressed_size,
            extent,
            bytes: bytes.to_vec(),
        }))
    }

    /// Is this object here? The negotiation call, made a thousand at a time.
    ///
    /// Packs first, and only then the loose probe. That ordering is `P-028`
    /// respected as far as this trait allows: *"a `have` that misses costs a
    /// `statx`"*, because `gix-odb`'s `contains` probes `objects/xx/yyy…` on
    /// every miss and a negotiation is mostly misses. Answering out of the
    /// mmapped `.idx` fanout tables costs no syscall at all, so the `statx` is
    /// paid only by a genuine miss — which is still the common case, and is the
    /// finding, not the fix. The fix needs a "packed only" seam the frozen trait
    /// does not have; that is written up in the report rather than bolted on
    /// here.
    ///
    /// # And one syscall per miss that is not this crate's to remove
    ///
    /// MEASURED 2026-08-10 on t14s, `strace -f -c -e trace=sched_yield` over
    /// 4 000 misses against a 64-pack, 100 600-object store: **4 001
    /// `sched_yield`, i.e. exactly one per miss** — the same count with and
    /// without a multi-pack index, so [`midx`] does not touch it. That is
    /// GitoxideLabs/gitoxide#2878, filed 2026-08-04: `Handle::contains` yields
    /// on every miss even under `RefreshMode::Never`. ~260 ns on a path a
    /// negotiation takes thousands of times. Filed upstream, not patched here,
    /// and this crate is not blocked on it.
    fn has(&self, oid: Oid<'_>) -> Result<bool> {
        let id = self.oid(oid)?;
        {
            let packs = self
                .packs
                .read()
                .map_err(|_| anyhow!("pack set lock poisoned"))?;
            if packs.find(&id).is_some() {
                return Ok(true);
            }
        }
        // `lookup_without_rescan`, not `lookup`. A `have` line naming an object
        // this server does not hold is the client telling you about its own
        // commits, not evidence of a stale snapshot — so a miss is the answer
        // and must not buy a `statx`. The generation check stays, because it is
        // an atomic load and it catches a handle leased from before another
        // request's scan, which the mtime watch structurally cannot.
        use gix_object::Exists as _;
        Ok(self
            .odb
            .lookup_without_rescan(|odb| odb.exists(&id), |found: &bool| *found))
    }

    /// Post-resolution size — what the object inflates to once its chain is
    /// applied.
    fn size(&self, oid: Oid<'_>) -> Result<Option<u64>> {
        let id = self.oid(oid)?;
        Ok(self.header(&id)?.map(|h| h.size()))
    }

    /// Byte extents, in bulk — the wire path.
    ///
    /// The extent is **pack-local**: `(offset_into_its_packfile, entry_len)`.
    /// That is what the contract asks for in as many words — *"an offset into a
    /// packfile for a gix backend"* — but it carries a consequence a caller must
    /// know: with more than one pack in the store, an extent does not identify a
    /// byte range on its own, because it does not say which pack. The contract's
    /// `Extent` is a bare `(u64, u64)` with nowhere to put a pack id. Flagged in
    /// the report as the one place where this backend's honest answer is
    /// narrower than the type suggests; not worked around by inventing a
    /// synthetic global offset, which would be a number that indexes nothing.
    ///
    /// Serial, on one thread. `P-004`: this is the serving tier.
    fn extents(&self, oids: &[Oid<'_>]) -> Result<Vec<Option<Extent>>> {
        let packs = self
            .packs
            .read()
            .map_err(|_| anyhow!("pack set lock poisoned"))?;
        let mut out = Vec::with_capacity(oids.len());
        for raw in oids {
            let Ok(id) = ObjectId::try_from(*raw) else {
                out.push(None);
                continue;
            };
            out.push(
                packs
                    .find(&id)
                    .and_then(|p| p.locate(&id))
                    .map(|l| l.extent),
            );
        }
        Ok(out)
    }

    // ── REFS ────────────────────────────────────────────────────────────────

    /// The current ref namespace, sorted by name.
    ///
    /// `all()` walks `refs/` loose and packed and excludes pseudo-refs such as
    /// `HEAD`, which is the same set every other backend advertises.
    fn refs(&self) -> Result<Vec<RefRow>> {
        let platform = self.refs.iter().context("open ref iterator")?;
        let mut out = Vec::new();
        for r in platform.all().context("iterate refs")? {
            let r = r.context("read ref")?;
            let name = r.name.as_bstr().to_string();
            out.push(refs::row(name, &r.target, |id| self.peel(id)));
        }
        out.sort_by(|a, b| a.name.cmp(&b.name));
        Ok(out)
    }

    /// Compare-and-swap one ref.
    ///
    /// `old: None` means "must not exist" (a create); `new: None` means delete.
    /// The expectation is checked by [`refs::precheck_cas`] before gix sees it —
    /// `S-023`, without which a create that must fail silently succeeds.
    fn update_ref(&self, name: &str, old: Option<Oid<'_>>, new: Option<Oid<'_>>) -> Result<TxId> {
        use gix_ref::transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog};
        use gix_ref::{FullName, Target};

        let full = FullName::try_from(name).with_context(|| format!("invalid ref name {name:?}"))?;
        let expected = match old {
            Some(raw) => PreviousValue::MustExistAndMatch(Target::Object(self.oid(raw)?)),
            None => PreviousValue::MustNotExist,
        };
        let change = match new {
            Some(raw) => Change::Update {
                log: LogChange {
                    mode: RefLog::AndReference,
                    force_create_reflog: false,
                    message: Default::default(),
                },
                expected,
                new: Target::Object(self.oid(raw)?),
            },
            None => Change::Delete {
                expected,
                log: RefLog::AndReference,
            },
        };
        let edit = RefEdit {
            change,
            name: full,
            deref: false,
        };

        refs::precheck_cas(std::slice::from_ref(&edit), |n| {
            Ok(self
                .refs
                .try_find(n.as_ref())
                .context("read ref")?
                .map(|r| r.target))
        })?;

        self.refs
            .transaction()
            .prepare(
                std::iter::once(edit),
                gix_lock::acquire::Fail::Immediately,
                gix_lock::acquire::Fail::Immediately,
            )
            .map_err(|e| anyhow!("ref transaction refused: {e}"))?
            .commit(None)
            .map_err(|e| anyhow!("ref transaction failed to commit: {e}"))?;

        Ok(TxId {
            pack_id: None,
            extent: None,
            push_seq: Some(self.seq.fetch_add(1, Ordering::SeqCst)),
        })
    }

    /// Apply every edit or none — the `--atomic` push primitive.
    ///
    /// The third shape: [`GixStore::put_refs`] is a batch without
    /// compare-and-swap and [`GixStore::update_ref`] is compare-and-swap without
    /// a batch, and `--atomic` is defined as both at once. Neither composes into
    /// the other, which is why this exists rather than being spelled as a loop.
    ///
    /// # `S-023` — the expectation is checked before gix sees the batch
    ///
    /// gix's file ref store short-circuits an edit whose new value equals the
    /// value the reference already holds: it rewrites `expected` to
    /// `MustExistAndMatch(current)` and never evaluates the one the caller wrote.
    /// For `old: None` — *must not exist* — that turns a create that must fail
    /// into a **silent success**, so "exactly one creator wins", the property
    /// receive-pack arbitrates two racing pushes with, was not true. The
    /// **whole** batch is therefore pre-checked against one snapshot of the
    /// namespace before any of it reaches gix's writer; see [`refs::precheck_cas`].
    ///
    /// # The rejection is typed, and never a message
    ///
    /// A lost race raises [`git_storage_trait::RefRejection::Cas`] carrying which
    /// ref, what was expected and what was actually there; contention raises
    /// [`git_storage_trait::RefRejection::Locked`]. Recover either with
    /// `RefRejection::of(&err)`. **This arm is the one that can raise `Locked`
    /// at all**, and the reason is two lines below: both lock modes are
    /// `Fail::Immediately`, so a contended reference surfaces as a retryable
    /// rejection rather than as a stalled connection.
    ///
    /// # Empty is a no-op, not an error
    ///
    /// A deletions-free push with nothing to apply calls this, and refusing it
    /// would make every call site special-case the empty batch.
    fn put_refs_cas(&self, edits: &[RefCas<'_>]) -> Result<TxId> {
        use gix_ref::transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog};
        use gix_ref::{FullName, Target};

        if edits.is_empty() {
            return Ok(TxId::default());
        }

        let mut batch = Vec::with_capacity(edits.len());
        for e in edits {
            let full = FullName::try_from(e.name.as_str())
                .with_context(|| format!("invalid ref name {:?}", e.name))?;
            // `None` is *must not exist*, never *don't care* — the contract is
            // explicit that there is no third state here, and `put_refs` is the
            // batch that makes no claim.
            let expected = match e.old {
                Some(raw) => PreviousValue::MustExistAndMatch(Target::Object(self.oid(raw)?)),
                None => PreviousValue::MustNotExist,
            };
            let change = match e.new {
                Some(raw) => Change::Update {
                    log: LogChange {
                        mode: RefLog::AndReference,
                        force_create_reflog: false,
                        message: Default::default(),
                    },
                    expected,
                    new: Target::Object(self.oid(raw)?),
                },
                None => Change::Delete {
                    expected,
                    log: RefLog::AndReference,
                },
            };
            batch.push(RefEdit {
                change,
                name: full,
                // A push that names `refs/heads/main` means that reference, not
                // whatever it might symbolically point at.
                deref: false,
            });
        }

        let existing: Vec<String> = self.refs()?.into_iter().map(|r| r.name).collect();
        refs::precheck_name_conflict(&batch, &existing)?;
        // `S-023`: the WHOLE batch, against one snapshot, before gix sees any of
        // it. This is also where the typed `Cas` rejection for a
        // create-that-must-fail comes from — gix's own `prepare` never sees that
        // case, which is the whole point of the finding.
        refs::precheck_cas(&batch, |n| {
            Ok(self
                .refs
                .try_find(n.as_ref())
                .context("read ref")?
                .map(|r| r.target))
        })?;

        self.refs
            .transaction()
            .prepare(
                batch.clone(),
                gix_lock::acquire::Fail::Immediately,
                gix_lock::acquire::Fail::Immediately,
            )
            .map_err(|e| refs::map_prepare_error(e, &batch))?
            .commit(None)
            .map_err(|e| anyhow!("ref transaction failed to commit: {e}"))?;

        Ok(TxId {
            pack_id: None,
            extent: None,
            push_seq: Some(self.seq.fetch_add(1, Ordering::SeqCst)),
        })
    }

    // ── GRAPH ───────────────────────────────────────────────────────────────

    /// `want` minus `have`, the object closure a fetch must send.
    ///
    /// **Through [`GixStore::selection`], the same seam
    /// [`git_storage_trait::GitServe::select`] uses**, so this answers from the
    /// `.bitmap` when there is one and walks when there is not. It used to call
    /// [`graph::reachable`] directly, on the reasoning that gitoxide has no
    /// bitmap support — which is true of gitoxide and no longer true of this
    /// crate, and the direct call was then a second reachability path that would
    /// quietly never take the bitmap.
    ///
    /// That mattered concretely rather than tidily: the head-to-head server
    /// binary this crate ships, `examples/git-store-serve.rs`, serves every
    /// clone through *this* method and not through `select`. Leaving it here
    /// would have meant a benchmark arm measuring the walk and a test suite
    /// measuring the bitmap — `P-027` exactly.
    fn reachable(&self, want: &[Oid<'_>], have: &[Oid<'_>]) -> Result<Vec<Vec<u8>>> {
        let to_ids = |s: &[Oid<'_>]| -> Result<Vec<ObjectId>> {
            s.iter().map(|raw| self.oid(raw)).collect()
        };
        let want = to_ids(want)?;
        let have = to_ids(have)?;
        let sel = self.selection(&want, &have)?;
        Ok(sel
            .objects
            .into_iter()
            .map(|id| id.as_slice().to_vec())
            .collect())
    }

    // ── MAINT ───────────────────────────────────────────────────────────────

    /// Mark from every ref tip, then drop packs with no live object in them.
    ///
    /// Reports itself as `"gix-mark-sweep-packs"`, not `"gc"`, because that is
    /// what it is: no partially-dead pack is rewritten and no packs are
    /// consolidated. See [`gc`]'s module doc for why (`P-009`: gitoxide has no
    /// delta encoder, so a naive repack makes the repository larger).
    fn gc(&self) -> Result<GcReport> {
        // One writer over `objects/pack`; see [`GixStore::maint_lock`]. The
        // multi-pack index writer takes the same lock, so it cannot be naming
        // packs this is unlinking.
        let _one_writer = self
            .maint_lock
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner);

        let tips = self.tip_ids()?;
        let live = self
            .odb
            .with_handle(|odb| graph::live_set(odb, self.hash_kind, &tips))?;

        let (sweep, retired, index_removed) = {
            let packs = self
                .packs
                .read()
                .map_err(|_| anyhow!("pack set lock poisoned"))?;
            let sweep = gc::plan(&packs, &live);
            // **Before** the unlink, not after. A multi-pack index names its
            // `.idx` files; one deleted while the index still lists it turns
            // every lookup that routes through the index into a read of a file
            // that is not there. Removing it first degrades to `O(packs)` for
            // the length of the sweep, which is slow and correct — the other
            // order is fast and wrong.
            let index_removed = if sweep.dead_packs.is_empty() {
                false
            } else {
                midx::remove_multi_pack_index(&self.pack_dir())?
            };
            let (retired, _) = gc::apply(&packs, &sweep)?;
            (sweep, retired, index_removed)
        };

        // Re-read the pack set off the filesystem, so `bytes_after` is measured
        // rather than computed from what we intended to delete. That is what
        // lets `verified` be `true`.
        self.reload_packs()?;
        // …and put the index back over what survived, but only if the store is
        // still above the threshold that justified having one — otherwise this
        // would write, from the cleanup path, an index the open path would have
        // declined to write. `_locked`, because this function holds
        // `maint_lock` and `std::sync::Mutex` is not reentrant.
        if index_removed && odb::count_packs(&self.pack_dir()) >= midx::MULTI_PACK_INDEX_THRESHOLD {
            if let Err(err) = self.write_multi_pack_index_locked() {
                // A gc that reclaimed bytes has succeeded. A missing index costs
                // speed and nothing else — the same posture
                // `write_multi_pack_index_if_worthwhile` takes, for the same
                // reason.
                tracing::warn!(error = %err, "gc could not rewrite the multi-pack index");
            }
        }
        // The reachability bitmap, which is where git writes one: `repack -b`
        // emits `.bitmap` beside `.idx`, and this is the only pass over the
        // whole object graph this backend makes. It runs **after** the sweep and
        // the reload, so it bitmaps what survived rather than what was there.
        //
        // A failure is logged rather than propagated, the same posture the
        // multi-pack index takes two blocks up and for the same reason: a gc
        // that reclaimed bytes has succeeded, and a missing bitmap costs speed
        // and nothing else. `graph`'s walk is still the whole answer without it.
        match self.write_reachability_bitmap_locked() {
            Ok(Some(path)) => tracing::info!(
                event = "gc.bitmap.written",
                bitmap = %path.display(),
                "wrote a reachability bitmap"
            ),
            Ok(None) => tracing::info!(
                event = "gc.bitmap.declined",
                "no pack here can carry an exact reachability bitmap; serving by walking"
            ),
            Err(err) => tracing::warn!(
                event = "gc.bitmap.failed",
                error = %err,
                "gc could not write a reachability bitmap"
            ),
        }

        let after = self
            .packs
            .read()
            .map_err(|_| anyhow!("pack set lock poisoned"))?
            .disk_bytes();

        Ok(gc::report(
            self.git_dir.clone(),
            sweep.bytes_before,
            after,
            &sweep,
            retired,
        ))
    }
}

impl GixStore {
    /// Index an untrusted pack into `objects/pack`, returning its receipt and
    /// the `.keep` guarding it.
    ///
    /// # What this reuses, and what it costs
    ///
    /// `gunnar_pack::index_pack` is the whole body: it validates the stream,
    /// resolves the deltas and writes `.pack`/`.idx`/`.keep`, with the
    /// allocation ceiling and the CRC-verifying iteration mode a server
    /// receiving a push needs. Reimplementing it here is the duplication the
    /// brief forbids.
    ///
    /// It also re-acquires a cost the Arrow backend shed. `index_pack` reaches
    /// `gix-features`' parallel machinery, which is where `P-030` found *"every
    /// push paid 50 ms of sleeping"* — a watcher thread in
    /// `in_parallel_with_slice`, reached unconditionally from
    /// `write_data_iter_to_stream`. The gunnar-side triage files `P-030` as
    /// `DEAD — gix` **because the Arrow receive path stopped calling it**. This
    /// path calls it. So `P-030` is live again in this crate and it is inherited
    /// rather than introduced.
    ///
    /// ## MEASURED 2026-08-10 on t14s. It is no longer a hypothesis.
    ///
    /// It is a **count**, so the loaded box does not weaken it. Over
    /// `tests/packs_beyond_the_slot_table.rs`'s 33 sequential `put_pack` calls,
    /// `strace -f -T -e trace=clock_nanosleep`:
    ///
    /// ```text
    /// 33 x clock_nanosleep(CLOCK_MONOTONIC, {tv_nsec=50000000}) = 0 <0.050093>
    /// ```
    ///
    /// **Exactly one 50 ms sleep per `put_pack`, and it is real elapsed time**
    /// (`<0.0501>` is strace's own wall measurement of the call). Every push,
    /// every `prepare`, and every one of those test files' runtimes is mostly
    /// this. `gix-pack`'s delta traversal asks for a 50 ms `periodic()` and
    /// `in_parallel_with_slice`'s watcher sleeps it out **after** every worker
    /// has been joined, inside the same `std::thread::scope` — so the interval
    /// is appended to the call rather than overlapped with it.
    ///
    /// **The fix exists and this workspace does not have it.**
    /// GitoxideLabs/gitoxide#2877 is an open PR (park-with-deadline instead of
    /// sleep, 32 lines), and gunnar carries it vendored —
    /// `gunnar/vendor/gix-features` + `[patch.crates-io]` at
    /// `gunnar/Cargo.toml:358`. edda's root `Cargo.toml` has **no `[patch]`
    /// table at all** and `edda/vendor/` holds only `cozo-rayonfree`, so this
    /// crate pays the 50 ms and the gunnar-built arm does not.
    ///
    /// That is a **confound for any head-to-head push or `prepare` clock**, and
    /// it is worth more than everything else measured in this crate today: on
    /// the quarantine's own 8-object pack the fix moved the p50 from 50.32 ms to
    /// 10.98 ms with process CPU unchanged, because nothing was made faster — a
    /// wait was removed.
    fn write_pack(&self, bytes: &[u8]) -> Result<(TxId, Option<PathBuf>)> {
        let dir = self.pack_dir();
        std::fs::create_dir_all(&dir)?;

        let mut cursor = bytes;
        let opts = gunnar_pack::IndexOptions {
            hash_kind: self.hash_kind,
            index_version: gix_pack::index::Version::V2,
            // `Verify`, not `AsIs`: this is an untrusted client's pack and the
            // CRC is the cheapest place to catch a corrupt one.
            iteration_mode: gix_pack::data::input::Mode::Verify,
            thread_limit: None,
            alloc_limit_bytes: Some(gunnar_pack::DEFAULT_ALLOC_LIMIT_BYTES),
        };
        // `index_thin_pack`, never `index_pack`, and this is not a detail.
        //
        // MEASURED, not assumed: `index_pack` fails on a pack straight out of
        // `git pack-objects --revs --stdout` with *"Ref delta objects are not
        // supported as there is no way to look them up"*. `git pack-objects`
        // emits **`OBJ_REF_DELTA`** unless `--delta-base-offset` is passed, and
        // a pushing client sends a **thin** pack by default — a delta whose base
        // the receiver already has and the sender therefore omitted. Both shapes
        // need a base lookup, and `index_pack` has none.
        //
        // The bases are this store's own odb, so a delta may point at anything
        // already here. Bases pulled in this way are written *into* the new
        // pack, so what lands on disk is self-contained and every later read
        // path — including the verbatim offset arithmetic in `packs` — is
        // reading a complete pack.
        let indexed = self
            .odb
            .with_handle(|odb| {
                gunnar_pack::index_thin_pack(&mut cursor, Some(&dir), odb, &opts, None)
            })
            // `anyhow::Error::new` rather than `anyhow!("{e}")`: gunnar-pack's
            // top-level Display is "failed to index the pack" and every useful
            // word is in its `source()` chain. Flattening it to the Display
            // string threw away the only part worth reading.
            .map_err(anyhow::Error::new)
            .context("index pack")?;

        self.reload_packs()?;

        // The pack id is the dense id the reloaded set assigned this pack, found
        // by the path `index_pack` reported. Reading it back rather than
        // predicting it keeps `TxId` "applied output", which is what the
        // contract says every field of it must be.
        let pack_id = indexed.data_path.as_ref().and_then(|p| {
            self.packs
                .read()
                .ok()?
                .packs
                .iter()
                .find(|pk| pk.data_path == *p)
                .map(|pk| pk.id)
        });

        let len = indexed
            .data_path
            .as_ref()
            .and_then(|p| std::fs::metadata(p).ok())
            .map(|m| m.len())
            .unwrap_or(bytes.len() as u64);

        Ok((
            TxId {
                pack_id,
                // The whole pack file, which is the extent this transaction
                // claimed. Per-object extents come from `extents()`.
                extent: Some((0, len)),
                push_seq: None,
            },
            indexed.keep_path,
        ))
    }
}

/// Re-exported so a caller can name the contract's types without a second
/// dependency edge on `git-storage-trait`.
pub use git_storage_trait::{self as contract, GitOps as _GitOpsIsImplemented};

/// The six pack entry codes, re-exported for tests and generators.
pub use git_storage_trait::ObjType as PackObjType;

const _: () = {
    // A compile-time assertion that the eleven are actually implemented, so a
    // signature drifting from the frozen contract is a build failure here rather
    // than a surprise at a call site.
    fn assert_impl<T: GitOps>() {}
    fn check() {
        assert_impl::<GixStore>();
    }
    let _ = check;
};

#[allow(dead_code)]
fn _obj_type_is_used(t: ObjType) -> u8 {
    t.code()
}

#[cfg(test)]
mod tests {
    use super::*;

    /// `S-030`, as a count. One test rather than three, because the counter is
    /// process-wide and three parallel tests reading it would race each other.
    #[test]
    fn a_keep_that_cannot_be_removed_is_counted_and_one_that_can_is_not() {
        let tmp = tempfile::tempdir().unwrap();

        // 1. A marker that is there goes, and is not a failure.
        let real = tmp.path().join("pack-x.keep");
        std::fs::write(&real, b"gunnar").unwrap();
        let before = GixStore::keep_removal_failures();
        drop_keep(&real);
        assert!(!real.exists(), "an existing .keep must actually be removed");
        assert_eq!(
            GixStore::keep_removal_failures(),
            before,
            "removing a marker that was there is not a failure"
        );

        // 2. A marker that is already gone is the outcome we wanted, not a
        //    failure. Getting this wrong would make the counter fire on every
        //    ordinary `put_pack` and render it useless.
        drop_keep(&real);
        assert_eq!(GixStore::keep_removal_failures(), before);

        // 3. A marker that will not go IS counted. A directory stands in for
        //    the real fault (a read-only parent, a `.keep` held open on another
        //    platform): `remove_file` on one is `EISDIR`, which is neither
        //    success nor `NotFound`.
        let stuck = tmp.path().join("pack-y.keep");
        std::fs::create_dir(&stuck).unwrap();
        drop_keep(&stuck);
        assert_eq!(
            GixStore::keep_removal_failures(),
            before + 1,
            "a .keep that could not be removed must be counted — it is a pack `gc` will \
             never reclaim, and before this it was a bare `let _ = remove_file(..)`"
        );
    }
}