gunnar-store 1.6.0

The gunnar server's storage seam: RepoStore (which repository), SecretStore (what has no git equivalent), and the one conformance suite both GitOps backends run
Documentation
//! One repository's storage, as the server sees it.
//!
//! # Why this exists
//!
//! The server used to hold `FsStore` — a concrete type from the packfile
//! engine — and reach through it for objects and refs. That is what made
//! "amputate the engine" a change to six files in `gunnar-cli` and
//! forty-three call sites in the bench harness rather than a change to one
//! manifest: the engine's *name* was in the server's own signatures.
//!
//! So the shape is inverted. The server names this trait, an engine implements
//! it, and which engine is running is a fact about startup rather than about
//! the source.
//!
//! # This is the repo-resolution seam, and it did not have to be invented
//!
//! `gunnar-server`'s `StorageEngine` was *repository*-oriented (`open_repo`,
//! `ingest_push`); [`GitOps`](git_storage_trait::GitOps) is *object*-oriented (the
//! twelve). Deleting the
//! former left one question open — something must still own **"which
//! repository"** — and the answer is this trait, which already existed and had
//! already made the right promise. See the storage-trait disposition.
//!
//! # What it deliberately does not have
//!
//! `FsStore` also offered `slot_table()`, `rescan_probes()`,
//! `stored_hash_kind()`, `with_journal_gap_hook()` and `repack()`. Every one of
//! those is a packfile concept — a `gix-odb` slot table, a re-scan of
//! `objects/pack`, a journal-gap hook for a ref store made of files — and none
//! is on this trait. A storage engine that has no packfiles cannot answer them,
//! and a trait that asked would be the engine's shape wearing the server's name
//! again.
//!
//! `head()` and `set_head()` are not here either — but not for the reason this
//! header used to give. It said *"`HEAD` is a reference, and `GitOps::refs` /
//! `update_ref` already own it"*, and the `GitServe` HEAD ruling settled that
//! they do not: `HEAD` is a pseudo-ref and is deliberately absent from the row
//! stream. It has an accessor pair, [`GitServe::head`] and
//! [`GitServe::set_head`], and they are
//! reachable through [`RepoStore::git`] — which is a method on the store this
//! trait resolves, not a second method on this trait.
//!
//! # `secrets()` is not here either, since 2026-08-10
//!
//! It used to be, and the reason it was is written down in the version of this
//! header that carried it: *"the server keeps them in a repository — the
//! control repository — and an engine that can hold objects and refs for one
//! can hold its secrets too."* The premise was the thing that changed. The
//! server does not keep its secrets in a repository any more; they are rows in
//! `control.redb` beside the catalog and the identity table
//! ([`gunnar_core::secrets`]), which is where a compare-and-swap-free key/value
//! pair always belonged.
//!
//! What that method cost while it was here was **not** an extra trait method.
//! It was that every implementor had to produce a secret store at
//! `open_repo` time, so opening *any* repository created an Arrow secret log
//! beside it — a `create_dir_all`, a lock file and an `secrets.arrow` per
//! repository, for a store nothing ever read. And it was that reading the
//! server's own SSH host key went through a repository, which meant through a
//! storage engine, which is what made `gunnar-server` unable to boot without
//! one. Removing it is why `ServerStores` no longer has a lazily-opened control
//! repository at all.
//!
//! **Verified before removing it** (2026-08-10): the only non-test caller in
//! the tree was `ServerStores::secrets`, on the *control* repository. No
//! served repository's secret store was ever read. There is no per-repository
//! secret.
//!
//! # One `git()`, where there were two
//!
//! `RepoStore::objects` and `RepoStore::refs` used to be separate, over two
//! gunnar-owned traits. Both are now `GitOps`, the single backend-neutral
//! contract, so they collapse into [`RepoStore::git`]. A repository's objects and
//! its references are written by **one** durable transaction (`GitOps::put` —
//! "the pack's bytes durable first, then the refs that point into them"), and two
//! handles onto one transaction was always a seam that could not be honoured.
//!
//! # `Arc`, not a borrow
//!
//! [`RepoStore::git`] hands back an `Arc` because the push path clones the
//! handle into a `PackIngest` that outlives the borrow, and the serve path
//! shares one across workers. Returning `&dyn GitServe` reads more cheaply
//! and does not survive either call site.
//!
//! # It hands out `GitServe`, not `GitOps` — DECIDED 2026-08-10
//!
//! `GitServe: GitOps`, so this is still **one** method and **one** authority:
//! the eleven are reachable through the supertrait and nothing was added beside
//! them. What changed is which end of the family the seam is stated at, and the
//! reason it had to change is that the hand-off is one-way. There is no route
//! from `dyn GitOps` back to the reading half — `dyn` has no downcast — so a
//! server holding the narrow handle simply cannot serve a clone from it.
//!
//! And every consumer of this trait is a *serving* process: `UploadPack` and
//! `ReceivePack` take `&dyn GitServe`, `gunnar-policy`'s branch-protection
//! ancestry walk and its signature gate take `&dyn GitServe`, and `HEAD` is
//! reachable **only** through [`GitServe::head`] / [`GitServe::set_head`],
//! because it is deliberately not a row in `GitOps::refs` (the `GitServe` HEAD ruling). There is
//! no useful `RepoStore` that cannot be served, so the bound belongs on the
//! method.
//!
//! The alternative — a second accessor beside `git()` — was rejected: two
//! methods handing out two views of one store is two authorities on one handle,
//! which is the exact thing collapsing `objects()` + `refs()` removed.
//!
//! # `Arc<dyn GitServe>`, and the `Send + Sync` that is no longer spelled here
//!
//! [`RepoStore`] is `Send + Sync`, because the server holds one per repository
//! and serves many connections from it at once. A `Send + Sync` type cannot hold
//! an `Arc<dyn GitServe>` unless `GitOps` itself is `Send + Sync`, so for a while
//! this signature spelled the bounds out at the use site — the narrow fix, taken
//! because the contract is frozen and widening it is
//! a two-implementation decision.
//!
//! **That decision was taken.** `git-storage-trait` now declares
//! `pub trait GitOps: Send + Sync`, which both implementations already satisfied
//! (each is held behind an `Arc` and served from many connections at once), so
//! the signature is back to a bare `Arc<dyn _>` exactly as frozen. Do **not**
//! re-spell the bounds at a use site: the supertrait exists precisely so that
//! spelling is not viral, and a consumer who writes one form and not the other
//! gets a needless diff rather than an error.

use std::path::Path;
use std::sync::Arc;

use git_storage_trait::GitServe;

/// One repository, opened.
///
/// Implementations must be cheap to share: the server holds one per repository
/// and serves many connections from it at once.
pub trait RepoStore: Send + Sync {
    /// Where this repository lives.
    ///
    /// Still a path, because a server addresses repositories by one and a
    /// catalog records one. It is **not** a promise that anything git-shaped is
    /// inside it: an engine is free to keep a single file there, and the
    /// znippy-git engine does.
    fn git_dir(&self) -> &Path;

    /// This repository's hash algorithm.
    ///
    /// A repository served under the wrong hash is not a degraded repository,
    /// it is a different one — so this is asked rather than assumed, and it
    /// comes from the repository rather than from a default.
    fn hash_kind(&self) -> gix_hash::Kind;

    /// **The repository, under the whole contract family.**
    ///
    /// The twelve are here through the supertrait and the reading half — an
    /// inflated `read`, `header`/`sizes`, the `head`/`set_head` pair,
    /// `emit_pack` and `select` — is here directly. One method, one authority;
    /// see the module header for why it is stated at this end.
    ///
    /// `GitOps: Send + Sync` is a supertrait on the contract itself, so this is
    /// plain `Arc<dyn GitServe>`.
    fn git(&self) -> Arc<dyn GitServe>;
}

impl<T: RepoStore + ?Sized> RepoStore for Arc<T> {
    fn git_dir(&self) -> &Path {
        (**self).git_dir()
    }
    fn hash_kind(&self) -> gix_hash::Kind {
        (**self).hash_kind()
    }
    fn git(&self) -> Arc<dyn GitServe> {
        (**self).git()
    }
}