alkgit 0.1.0

Git smart protocol for the alk family: pkt-line substrates, protocol V2 serving (upload-pack/receive-pack), producer/consumer halves on alkcall channels, and the backend seam traits — the shipped alkstore+alkblobs composition is the alkgit-substrate crate (ADR-010/025)
Documentation
//! The backend-family error model (backend.md §Registry types and
//! schemas; ADR-018 §7).
//!
//! Two error types, one per backend family, with no catch-all variant in
//! either: the variant set of each type is closed over the failures its
//! family can produce, which is what keeps op handlers able to map every
//! failure to a precise wire error instead of a generic fallback. These
//! shapes enter OQ-03's freeze inventory at the core's first publish.
//!
//! What is deliberately *not* here:
//!
//! - The wire-mapping table (which variant collapses to which wire code,
//!   the `unauthorized` denial) is transport-layer business — it lands
//!   with the op family (wave 4, backend.md §`git/repo/*` op JSON
//!   schemas), not on the error types.
//! - Serde impls: errors cross the wire only as op-level error payloads
//!   built by the transport layer; the trait surface is
//!   `Result<_, RegistryError>`/`Result<_, StorageError>` in-process.
//! - `From<std::io::Error>`: a blanket conversion would funnel
//!   unclassified failures into the error types and break the
//!   no-catch-all claim — call sites classify failures explicitly.
//! - CAS-stale ref outcomes and fsck/pack-level validation reports are
//!   *outcomes, not errors*: a stale ref is a business result of
//!   receive-pack (ADR-013 §7–8, `RefOutcome`) and pack validation
//!   failures the session survives land in `PreparedPush.unpack`'s ng
//!   leg (ADR-013 §8). They are deliberately not variants — that is
//!   what keeps the no-catch-all claim true for both types (backend.md
//!   §`RegistryError`, closing paragraph).

use gix_hash::ObjectId;

/// The error type of the registry family (traits 1–2:
/// `GitRegistry`/`GitRegistryStore`) — every failure the registry
/// family can produce, no catch-all (backend.md §`RegistryError`,
/// review 002 R-2 re-scope).
///
/// Not the error type of the whole backend family: the object-storage
/// traits (3–5) carry [`StorageError`] (ADR-018 §7). `Io` is the only
/// stringly-typed variant — everything else is structural so the op
/// layer can report precise per-variant JSON errors while the trait
/// surface stays `Result<_, RegistryError>` (ADR-012 §1).
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum RegistryError {
    /// The repo id is unresolved in the registry.
    ///
    /// Collapses with [`RegistryError::Forbidden`] to the generic wire
    /// denial at the transport boundary (unknown ≡ unauthorized,
    /// ADR-008) — the variant itself leaks nothing.
    #[error("repo not found")]
    NotFound,
    /// The gate denied the operation (op-layer gate or the
    /// `authorize` policy).
    ///
    /// Same wire shape as [`RegistryError::NotFound`] — never
    /// distinguishable on the wire, so no existence oracle.
    #[error("access denied")]
    Forbidden,
    /// A create targeted an id that is already taken.
    ///
    /// Create-side only — `create` responses may carry it (the caller
    /// already knows the id; no oracle) and `resolve` never returns
    /// it.
    #[error("repo already exists")]
    AlreadyExists,
    /// A malformed record or field was rejected by store-side
    /// validation (e.g. an empty repo id, an unknown action string).
    #[error("invalid registry record or field")]
    Invalid,
    /// The backing store failed. Message-only by pin: not
    /// `std::io::Error`, which is not a stable surface for a type in
    /// OQ-03's freeze inventory.
    #[error("registry backing-store failure: {0}")]
    Io(String),
}

/// The error type of the object-storage family (traits 3–5:
/// `GitRefs`/`GitPackGen`/`GitPackIngest`) — the ADR-018 §7 sibling of
/// [`RegistryError`], no catch-all.
///
/// Same `Io` stability rule as [`RegistryError::Io`]: message-only,
/// never `std::io::Error`.
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum StorageError {
    /// A requested object does not exist. Generation aborts per
    /// ADR-004 — never a broken pack emitted downstream. Fsck-level
    /// missing reachability is *not* this variant: it is reported
    /// through `PreparedPush.unpack`'s ng leg (ADR-013 §8), as an
    /// outcome the session survives.
    #[error("object {oid} does not exist")]
    ObjectMissing {
        /// The oid the caller asked for (the client computed it, so
        /// including it in the reason is no oracle).
        oid: ObjectId,
    },
    /// Structurally invalid backend input (e.g. a malformed pack
    /// stream reaching the impl) or structurally invalid backend
    /// state. A session/op error, not an unpack report.
    #[error("invalid object-storage input or state: {0}")]
    Invalid(String),
    /// The backing store failed. Message-only by pin — same rule as
    /// [`RegistryError::Io`].
    #[error("object-storage backing-store failure: {0}")]
    Io(String),
}

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

    fn oid(b: u8) -> ObjectId {
        ObjectId::from([b; 20])
    }

    #[test]
    fn registry_error_has_exactly_the_five_pinned_variants() {
        let all = [
            RegistryError::NotFound,
            RegistryError::Forbidden,
            RegistryError::AlreadyExists,
            RegistryError::Invalid,
            RegistryError::Io("corrupted index".to_string()),
        ];
        for e in &all {
            let _dbg: &dyn std::fmt::Debug = e;
            let _err: &dyn std::error::Error = e;
        }
        assert_eq!(all.len(), 5);
    }

    #[test]
    fn registry_error_display_paths_render() {
        assert_eq!(RegistryError::NotFound.to_string(), "repo not found");
        assert_eq!(RegistryError::Forbidden.to_string(), "access denied");
        assert_eq!(
            RegistryError::AlreadyExists.to_string(),
            "repo already exists"
        );
        assert_eq!(
            RegistryError::Invalid.to_string(),
            "invalid registry record or field"
        );
        assert_eq!(
            RegistryError::Io("sqlite step failed".to_string()).to_string(),
            "registry backing-store failure: sqlite step failed"
        );
    }

    #[test]
    fn registry_error_is_comparable_and_clonable() {
        assert_eq!(RegistryError::NotFound, RegistryError::NotFound);
        assert_ne!(RegistryError::NotFound, RegistryError::Forbidden);
        let io = RegistryError::Io("disk".to_string());
        assert_eq!(io.clone(), io);
    }

    #[test]
    fn storage_error_has_exactly_the_three_pinned_variants() {
        let all = [
            StorageError::ObjectMissing { oid: oid(1) },
            StorageError::Invalid("malformed pack stream".to_string()),
            StorageError::Io("blob store unavailable".to_string()),
        ];
        for e in &all {
            let _dbg: &dyn std::fmt::Debug = e;
            let _err: &dyn std::error::Error = e;
        }
        assert_eq!(all.len(), 3);
    }

    #[test]
    fn storage_error_object_missing_carries_the_gix_oid() {
        let e = StorageError::ObjectMissing { oid: oid(7) };
        match e {
            StorageError::ObjectMissing { oid: carried } => assert_eq!(carried, oid(7)),
            other => panic!("wrong variant: {other:?}"),
        }
        assert_eq!(
            StorageError::ObjectMissing { oid: oid(7) }.to_string(),
            "object 0707070707070707070707070707070707070707 does not exist"
        );
    }

    #[test]
    fn storage_error_display_paths_render() {
        assert_eq!(
            StorageError::Invalid("index-write failed fsck".to_string()).to_string(),
            "invalid object-storage input or state: index-write failed fsck"
        );
        assert_eq!(
            StorageError::Io("rocksdb opened twice".to_string()).to_string(),
            "object-storage backing-store failure: rocksdb opened twice"
        );
    }

    #[test]
    fn storage_error_is_comparable_and_clonable() {
        assert_eq!(
            StorageError::ObjectMissing { oid: oid(2) },
            StorageError::ObjectMissing { oid: oid(2) }
        );
        let invalid = StorageError::Invalid("d".to_string());
        assert_eq!(invalid.clone(), invalid);
        assert_ne!(
            StorageError::Invalid("a".to_string()),
            StorageError::Io("a".to_string())
        );
    }
}