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 five backend trait signatures (ADR-012 §1, ADR-018 §4; the
//! backend.md §Pinned-signatures mirror).
//!
//! Zero drift: the signatures are copied from the ADR text, not
//! restated — this is the surface every later wave and every
//! third-party backend implements against, and it enters OQ-03's
//! freeze inventory at the core's first publish. `#[async_trait]` on
//! all five: bare `async fn` in a trait is not dyn-compatible (E0038,
//! review 001 A-2) — the desugared `Pin<Box<dyn Future + Send>>`
//! boxed form is the published vtable shape.

use std::io;

use async_trait::async_trait;
use gix_hash::ObjectId;

use crate::error::{RegistryError, StorageError};
use crate::limits::Limits;
use crate::registry::RepoRecord;
use crate::seam::{PreparedPush, PushOptions, RefLine, RefOutcome, RefUpdate};

/// The registry read trait (ADR-012 §1 as amended by review 001 A-2) —
/// repo id → [`RepoRecord`], the authoritative mapping (ADR-008).
///
/// `Send + Sync` supertraits: every impl is shared across the wire
/// layer's concurrent sessions. Resolution failure is indistinguishable
/// from authorization failure ([`RegistryError::NotFound`] and
/// [`RegistryError::Forbidden`] collapse to the same generic wire
/// denial — ADR-007, the no-existence-oracle rule).
#[async_trait]
pub trait GitRegistry: Send + Sync {
    /// Resolve a repo id to its record (registry family, `RegistryError`).
    ///
    /// The wire layer resolves once (ADR-007 step 2 / ADR-018 §2) and
    /// holds the record — object-storage methods take it as `&RepoRecord`
    /// from there, so this is the only registry lookup per session.
    async fn resolve(&self, repo_id: &str) -> Result<RepoRecord, RegistryError>;
}

/// The registry write supertrait (ADR-012 §1; alknet ADR-035's
/// read/write split shape — read on the wire hot path, write on the
/// management path).
///
/// Grants ride [`RepoRecord`] (ADR-011), so grant mutation is
/// [`GitRegistryStore::update_repo`] — convenience wrappers (grant /
/// revoke as named methods) are additive later, two-way. The
/// `git/repo/*` ops are thin wrappers over `Arc<dyn GitRegistryStore>`
/// (ADR-012 §3).
#[async_trait]
pub trait GitRegistryStore: GitRegistry {
    /// Create a repo record (the id is minted by the store; ADR-012 §3).
    async fn put_repo(&self, record: &RepoRecord) -> Result<(), RegistryError>;

    /// Replace a repo record — the grant-mutation path (ADR-011).
    async fn update_repo(&self, record: &RepoRecord) -> Result<(), RegistryError>;

    /// Remove a repo record.
    async fn remove_repo(&self, repo_id: &str) -> Result<(), RegistryError>;
}

/// Ref listing + ref transactions (ADR-018 §4; object-storage trait 3 —
/// `StorageError`).
///
/// The `repo` parameter is `&RepoRecord` on every method (ADR-018 §2):
/// the wire layer resolves once and the impl keys its storage on
/// `repo_id` — the record carries no path of any kind (ADR-019 §8) and
/// no impl opens anything from it.
#[async_trait]
pub trait GitRefs: Send + Sync {
    /// Ref listing — advertisement + ls-refs response data (the full
    /// set; ref-prefix filtering is wire-side, client-driven).
    async fn list_refs(&self, repo: &RepoRecord) -> Result<Vec<RefLine>, StorageError>;

    /// One CAS transaction per call — the atomic-correctness home
    /// (ADR-013 §7; the substrate row transaction — ADR-019 §2). Runs
    /// the per-ref CAS; per-ref denials are outcomes ([`RefOutcome`]),
    /// not errors (a stale ref is a business result of receive-pack —
    /// ADR-013 §7–8).
    ///
    /// The `atomic` flag is the call's, not the list's (ADR-018 §6):
    /// non-atomic applies each update and reports per-ref outcomes;
    /// atomic prepares the whole transaction and commits it only if
    /// every update succeeds, mapping the abort to
    /// `ng <ref> atomic push failure` (ADR-013 §10).
    async fn apply_updates(
        &self,
        repo: &RepoRecord,
        updates: Vec<RefUpdate>,
        atomic: bool,
    ) -> Result<Vec<RefOutcome>, StorageError>;
}

/// Pack generation + the negotiation ack seam (ADR-018 §4;
/// object-storage trait 4). Generation is negotiation-agnostic
/// (ADR-004's pipeline; backend.md authoritative).
///
/// Owned-value parameters (`Vec`s, `Limits`, the boxed `Send` streams)
/// are the spawn_blocking-friendly shape: impls move them into their
/// blocking tasks (backend.md's concurrency model — the permit stays
/// wire-side, the threads stay impl-side). Boxed `Send + 'static`
/// streams are the blocking-side ends the substrate bridges produce
/// (ADR-005).
#[async_trait]
pub trait GitPackGen: Send + Sync {
    /// closure(wants) − closure(boundary_haves) → pack streamed into
    /// sink (ADR-004's pipeline). Missing objects abort per ADR-004 —
    /// never a broken pack emitted downstream
    /// ([`StorageError::ObjectMissing`]).
    async fn generate(
        &self,
        repo: &RepoRecord,
        wants: Vec<ObjectId>,
        boundary_haves: Vec<ObjectId>,
        limits: Limits,
        sink: Box<dyn io::Write + Send>,
    ) -> Result<(), StorageError>;

    /// The negotiation ack seam (ADR-014 §5, §2's done-round
    /// amendment). A have is recognized iff it **exists in the object
    /// store and is a commit** (ADR-014 §5's predicate — the §1 ACK rule
    /// and the §2 boundary-set rule are this same predicate; ADR-010
    /// keeps the odb-free wire layer from applying any refinement of
    /// its own). The wire layer acks exactly the returned subset and
    /// subtracts exactly it on the done round.
    async fn common_haves(
        &self,
        repo: &RepoRecord,
        haves: Vec<ObjectId>,
    ) -> Result<Vec<ObjectId>, StorageError>;
}

/// Pack ingestion (ADR-018 §4; object-storage trait 5).
///
/// Prepare-only: the single CAS application home is
/// [`GitRefs::apply_updates`] (ADR-013 §6–7) — ingest validates, refs
/// commits. No apply method exists anywhere on this trait.
#[async_trait]
pub trait GitPackIngest: Send + Sync {
    /// Unpack + index + fsck; validates and prepares the ref updates.
    /// Never applies them. Pack-level validation failures (parse, index,
    /// fsck connectivity) land in [`PreparedPush::unpack`]'s ng leg, not
    /// as `Err` — the session continues to the report per ADR-013 §8.
    ///
    /// The `updates` are the command-derived ref updates the wire layer
    /// parsed (`expected` = old, `new` = new). The impl validates them —
    /// the connectivity walk from the `new` tips (ADR-020 §2) — and
    /// returns them verbatim as [`PreparedPush::updates`]; no impl-side
    /// filtering in v1 (ref-name validation and CAS stay wire-side and
    /// backend-CAS-side; ADR-013 §9).
    ///
    /// `push_options` is `None` until the config gate opens (ADR-013
    /// §11); opening the gate later is value-additive.
    async fn prepare(
        &self,
        repo: &RepoRecord,
        updates: Vec<RefUpdate>,
        pack: Box<dyn io::Read + Send>,
        push_options: Option<PushOptions>,
        limits: Limits,
    ) -> Result<PreparedPush, StorageError>;
}

#[cfg(test)]
mod tests {
    use std::sync::Arc;

    use super::*;

    struct NoopRegistry;
    struct NoopRegistryStore;
    struct NoopRefs;
    struct NoopPackGen;
    struct NoopPackIngest;

    #[async_trait]
    impl GitRegistry for NoopRegistry {
        async fn resolve(&self, _repo_id: &str) -> Result<RepoRecord, RegistryError> {
            Err(RegistryError::NotFound)
        }
    }

    #[async_trait]
    impl GitRegistry for NoopRegistryStore {
        async fn resolve(&self, _repo_id: &str) -> Result<RepoRecord, RegistryError> {
            Err(RegistryError::NotFound)
        }
    }

    #[async_trait]
    impl GitRegistryStore for NoopRegistryStore {
        async fn put_repo(&self, _record: &RepoRecord) -> Result<(), RegistryError> {
            Ok(())
        }
        async fn update_repo(&self, _record: &RepoRecord) -> Result<(), RegistryError> {
            Ok(())
        }
        async fn remove_repo(&self, _repo_id: &str) -> Result<(), RegistryError> {
            Ok(())
        }
    }

    #[async_trait]
    impl GitRefs for NoopRefs {
        async fn list_refs(&self, _repo: &RepoRecord) -> Result<Vec<RefLine>, StorageError> {
            Ok(Vec::new())
        }
        async fn apply_updates(
            &self,
            _repo: &RepoRecord,
            _updates: Vec<RefUpdate>,
            _atomic: bool,
        ) -> Result<Vec<RefOutcome>, StorageError> {
            Ok(Vec::new())
        }
    }

    #[async_trait]
    impl GitPackGen for NoopPackGen {
        async fn generate(
            &self,
            _repo: &RepoRecord,
            _wants: Vec<ObjectId>,
            _boundary_haves: Vec<ObjectId>,
            _limits: Limits,
            _sink: Box<dyn io::Write + Send>,
        ) -> Result<(), StorageError> {
            Ok(())
        }
        async fn common_haves(
            &self,
            _repo: &RepoRecord,
            _haves: Vec<ObjectId>,
        ) -> Result<Vec<ObjectId>, StorageError> {
            Ok(Vec::new())
        }
    }

    #[async_trait]
    impl GitPackIngest for NoopPackIngest {
        async fn prepare(
            &self,
            _repo: &RepoRecord,
            _updates: Vec<RefUpdate>,
            _pack: Box<dyn io::Read + Send>,
            _push_options: Option<PushOptions>,
            _limits: Limits,
        ) -> Result<PreparedPush, StorageError> {
            Ok(PreparedPush {
                unpack: Ok(()),
                updates: Vec::new(),
            })
        }
    }

    #[tokio::test]
    async fn all_five_traits_are_dyn_compatible() {
        let registry: Arc<dyn GitRegistry> = Arc::new(NoopRegistry);
        let store: Arc<dyn GitRegistryStore> = Arc::new(NoopRegistryStore);
        let refs: Arc<dyn GitRefs> = Arc::new(NoopRefs);
        let gen: Arc<dyn GitPackGen> = Arc::new(NoopPackGen);
        let ingest: Arc<dyn GitPackIngest> = Arc::new(NoopPackIngest);

        let repo = RepoRecord {
            repo_id: "demo".to_string(),
            visibility: crate::registry::Visibility::Private,
            grants: std::collections::BTreeMap::new(),
        };

        assert_eq!(
            registry.resolve("demo").await.unwrap_err(),
            RegistryError::NotFound
        );
        assert!(store.put_repo(&repo).await.is_ok());
        assert!(store.update_repo(&repo).await.is_ok());
        assert!(store.remove_repo("demo").await.is_ok());
        assert!(refs.list_refs(&repo).await.unwrap().is_empty());
        let outcomes = refs.apply_updates(&repo, Vec::new(), true).await.unwrap();
        assert!(outcomes.is_empty());
        gen.generate(
            &repo,
            Vec::new(),
            Vec::new(),
            Limits::default(),
            Box::new(io::sink()),
        )
        .await
        .unwrap();
        assert!(gen
            .common_haves(&repo, Vec::new())
            .await
            .unwrap()
            .is_empty());
        let prepared = ingest
            .prepare(
                &repo,
                Vec::new(),
                Box::new(io::empty()),
                None,
                Limits::default(),
            )
            .await
            .unwrap();
        assert!(prepared.unpack.is_ok());
    }
}