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

nordisk/gunnar

crates/gunnar-policy/src/error.rs

//! What can go wrong *inside* a gate.
//!
//! # An error is not a rejection
//!
//! This distinction is the one thing to get right in this module. A
//! [`Verdict::Reject`](crate::Verdict::Reject) is a **policy** answer: the push
//! was well-formed and gunnar has decided not to take it. An [`Error`] is a
//! **gunnar** answer: the gate could not reach a decision at all, because an
//! object was missing, a pattern was malformed, or a walk ran out of budget.
//!
//! Conflating them is how a broken gate turns into a silent allow. A gate that
//! swallows "I could not read the commit" and returns `Allow` is exactly the
//! always-allow failure mode a measured assertion exists to catch. So every
//! gate in this crate propagates the error, and
//! [`PolicyEngine::evaluate`](crate::PolicyEngine::evaluate) treats an
//! erroring gate as a **rejection with the error as its reason**, never as an
//! allow.

use gix_hash::ObjectId;

/// The result type every gate returns.
pub type Result<T, E = Error> = std::result::Result<T, E>;

/// A gate could not reach a decision.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
    /// The storage layer failed.
    #[error("storage: {0}")]
    Store(#[from] gunnar_store::Error),

    /// A git object could not be decoded.
    #[error("object {oid} could not be decoded: {reason}")]
    Decode {
        /// The object that would not decode.
        oid: ObjectId,
        /// Why.
        reason: String,
    },

    /// An object the gate needs is not in the store.
    ///
    /// At receive-pack time this should be impossible, since connectivity is
    /// verified before the gate runs (the receive-pack sequence, step 4). So it means either a thin
    /// pack whose bases did not resolve, or a caller that ran the gate too early.
    /// Either way it is *inconclusive*, and inconclusive must never allow.
    #[error("object {oid} is not in the store, so {what} cannot be decided")]
    MissingObject {
        /// The missing object.
        oid: ObjectId,
        /// What the gate was trying to decide.
        what: &'static str,
    },

    /// An ancestry walk gave up before proving or disproving descent.
    ///
    /// Rejecting here is deliberate: "I ran out of budget" is not "this is a
    /// fast-forward".
    #[error(
        "walked {walked} commits from {from} without reaching {looking_for} \
         and hit the {budget}-commit budget; ancestry is undecided"
    )]
    AncestryBudget {
        /// Where the walk started: the pushed tip.
        from: ObjectId,
        /// The old tip it was looking for.
        looking_for: ObjectId,
        /// How many commits were visited.
        walked: usize,
        /// The budget that was hit.
        budget: usize,
    },

    /// A configured reference pattern is not usable.
    #[error("reference pattern {pattern:?} is invalid: {reason}")]
    BadPattern {
        /// The offending pattern.
        pattern: String,
        /// Why it was refused.
        reason: &'static str,
    },

    /// Configuration could not be parsed.
    #[error("policy configuration: {0}")]
    Config(String),
}

impl Error {
    /// A decode failure with a formatted reason.
    pub(crate) fn decode(oid: impl Into<ObjectId>, reason: impl std::fmt::Display) -> Self {
        Error::Decode {
            oid: oid.into(),
            reason: reason.to_string(),
        }
    }

    /// **The one conversion from a storage fault into a gate error.**
    ///
    /// `GitServe` returns `anyhow::Result` — the contract crate depends on
    /// `anyhow` and nothing else, deliberately, so that neither backend has to
    /// resolve the other's dependencies. Everything this crate can say about
    /// such a fault is therefore "the storage layer failed, and here is what it
    /// said", which is [`gunnar_store::Error::Backend`].
    ///
    /// One function rather than a `map_err` at each of the six call sites, for
    /// the reason one implementation gives: the day there is a typed rejection
    /// to tell apart (`RefCas` / `RefLocked`, `locked_on_target.md`'s typed-CAS question)
    /// there is exactly one place that has to learn about it. **Nothing
    /// anywhere may tell two storage faults apart by matching on this
    /// string.**
    ///
    /// `{:#}` and not `{}`: `anyhow`'s alternate form prints the whole cause
    /// chain, and the outermost message of a decode failure four layers down is
    /// usually the least informative line in it.
    pub(crate) fn backend(err: impl std::fmt::Display) -> Self {
        Error::Store(gunnar_store::Error::backend(format!("{err:#}")))
    }
}