//! alkssh's git-seam doubles (`w3-git-mapping`): the hand-rolled //! in-memory alkgit backend quartet, shaped per //! `alkgit/src/backend.rs:164-254`'s noop impls (the shapes that file //! itself pins for dyn compatibility), with recording accessors the //! rows assert through. alkgit's own `Mem*` constructions are //! test-private to that crate, so this module hand-rolls the minimal //! surface the git rows need, never a sibling-checkout path, never a //! copy of alkgit's doubles' *code*. //! //! State/accessor split: the doubles are configurable per row (repo //! records with visibility + grants; advertised ref lines) and //! record every seam call the serving path makes (resolutions, //! applied CAS updates, prepared pushes' updates, generated packs); //! the dispatch fidelity (which id was resolved, which updates were //! applied) is observable without alkgit's own test surface. //! //! Feature gating: this module is `git`-gated at the include site //! (`tests/common/mod.rs`); a non-git build compiles none of it, and //! alkgit types never appear outside. use std::collections::{BTreeMap, HashMap}; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; /// The seam's ref-line shape re-exported for the rows (`RefLine` is /// the doubles' `list_refs` payload). pub use alkgit::RefLine; use alkgit::{ AccessAction, GitPackGen, GitPackIngest, GitRefs, GitRegistry, PreparedPush, PushOptions, RefOutcome, RefUpdate, RegistryError, RepoRecord, StorageError, Visibility, }; use async_trait::async_trait; use gix_hash::ObjectId; use parking_lot::{Mutex, RwLock}; /// A test oid, deterministic bytes (the row-assertable hex shape). pub fn oid(byte: u8) -> ObjectId { ObjectId::from([byte; 20]) } /// One repo the registry double serves: grants keyed by the resolved /// identity id (alkgit's `authorize` input, the identity-ride /// witness), the advertised ref set the refs double lists. pub struct RepoSetup { pub repo_id: String, pub visibility: Visibility, pub grants: BTreeMap>, pub refs: Vec, } impl RepoSetup { pub fn private_granted(repo_id: &str, identity_id: &str, refs: Vec) -> Self { let mut grants = BTreeMap::new(); grants.insert( identity_id.to_string(), vec![AccessAction::Read, AccessAction::Write], ); Self { repo_id: repo_id.to_string(), visibility: Visibility::Private, grants, refs, } } pub fn granted( repo_id: &str, identity_id: &str, actions: Vec, refs: Vec, ) -> Self { let mut grants = BTreeMap::new(); grants.insert(identity_id.to_string(), actions); Self { repo_id: repo_id.to_string(), visibility: Visibility::Private, grants, refs, } } pub fn private_ungranted(repo_id: &str, refs: Vec) -> Self { Self { repo_id: repo_id.to_string(), visibility: Visibility::Private, grants: BTreeMap::new(), refs, } } } /// One recorded CAS application (the wire layer's single ingestion /// home, `GitRefs::apply_updates`). #[derive(Clone, Debug)] pub struct AppliedUpdates { pub repo_id: String, pub updates: Vec, pub atomic: bool, } /// One recorded pack ingestion (`GitPackIngest::prepare`). #[derive(Clone, Debug)] pub struct PreparedPack { pub repo_id: String, pub updates: Vec, pub push_options: Option, } /// One recorded pack generation (`GitPackGen::generate`). #[derive(Clone, Debug)] pub struct GeneratedPack { pub repo_id: String, pub wants: Vec, pub boundary_haves: Vec, } /// One per-repo serve gate (the `w4-git-exit-close` row's /// serve-timing control): the double's `GitRefs::list_refs` for the /// gated repo id pends on it until [`MemGate::release`]; the serving /// task stalls at that call with its channel's reader parked, so the /// row schedules the serve's later stages (the answer flood, the /// serve tail) against a prepared wire state. Race-safe by /// construction (the double-check pattern over the released flag). pub struct MemGate { released: AtomicBool, opened: tokio::sync::Notify, } impl MemGate { fn new() -> Self { Self { released: AtomicBool::new(false), opened: tokio::sync::Notify::new(), } } /// Releases the gate's waiting serve. pub fn release(&self) { self.released.store(true, Ordering::Release); self.opened.notify_waiters(); } async fn wait(&self) { if self.released.load(Ordering::Acquire) { return; } let notified = self.opened.notified(); if self.released.load(Ordering::Acquire) { return; } notified.await; } } /// The in-memory quartet: one struct, four trait impls; the /// [`GitSeam`](alkssh::GitSeam) takes a separate `Arc` per backend /// trait and the rows share this state. #[derive(Default)] pub struct MemGit { repos: RwLock, refs: RwLock>>, resolves: Mutex>, applied: Mutex>, prepared: Mutex>, generated: Mutex>, gates: Mutex>>, } impl MemGit { pub fn new() -> Self { Self::default() } /// Installs one repo record + advertised ref set. pub fn install(&self, setup: RepoSetup) { self.repos .write() .insert(setup.repo_id.clone(), (setup.visibility, setup.grants)); self.refs.write().insert(setup.repo_id.clone(), setup.refs); } /// Every `GitRegistry::resolve` id, in call order. pub fn resolved_ids(&self) -> Vec { self.resolves.lock().clone() } /// Every CAS application, in call order. pub fn applied(&self) -> Vec { self.applied.lock().clone() } /// Every pack preparation, in call order. pub fn prepared(&self) -> Vec { self.prepared.lock().clone() } /// Every pack generation, in call order. pub fn generated(&self) -> Vec { self.generated.lock().clone() } /// Installs a serve gate for one repo id, so [`GitRefs::list_refs`] /// calls for it pend until the returned gate's `release`. The /// rows use the gate to prepare wire state (saturated queues, /// blocked clients) for the serve's later stages. pub fn install_gate(&self, repo_id: &str) -> Arc { let gate = Arc::new(MemGate::new()); self.gates .lock() .insert(repo_id.to_string(), Arc::clone(&gate)); gate } } /// The record for a resolved id: the RepoRecord the seam hands the /// backend traits. fn record(id: &str, repos: &RepoRegistry) -> Result { match repos.get(id) { Some((visibility, grants)) => Ok(RepoRecord { repo_id: id.to_string(), visibility: *visibility, grants: grants.clone(), }), None => Err(RegistryError::NotFound), } } /// The registry's per-repo record: visibility + the grant map keyed /// by the resolved identity id. type RepoRegistry = HashMap>)>; #[async_trait] impl GitRegistry for MemGit { async fn resolve(&self, repo_id: &str) -> Result { self.resolves.lock().push(repo_id.to_string()); record(repo_id, &self.repos.read()) } } #[async_trait] impl GitRefs for MemGit { async fn list_refs(&self, repo: &RepoRecord) -> Result, StorageError> { let gate = self.gates.lock().get(&repo.repo_id).map(Arc::clone); if let Some(gate) = gate { gate.wait().await; } Ok(self .refs .read() .get(&repo.repo_id) .cloned() .unwrap_or_default()) } async fn apply_updates( &self, repo: &RepoRecord, updates: Vec, atomic: bool, ) -> Result, StorageError> { let applied = AppliedUpdates { repo_id: repo.repo_id.clone(), updates: updates.clone(), atomic, }; let outcomes = updates .iter() .map(|update| RefOutcome { name: update.name.clone(), result: Ok(()), }) .collect(); self.applied.lock().push(applied); Ok(outcomes) } } #[async_trait] impl GitPackGen for MemGit { async fn generate( &self, repo: &RepoRecord, wants: Vec, boundary_haves: Vec, _limits: alkgit::Limits, _sink: Box, ) -> Result<(), StorageError> { self.generated.lock().push(GeneratedPack { repo_id: repo.repo_id.clone(), wants, boundary_haves, }); Ok(()) } async fn common_haves( &self, _repo: &RepoRecord, _haves: Vec, ) -> Result, StorageError> { Ok(Vec::new()) } } #[async_trait] impl GitPackIngest for MemGit { async fn prepare( &self, repo: &RepoRecord, updates: Vec, _pack: Box, push_options: Option, _limits: alkgit::Limits, ) -> Result { self.prepared.lock().push(PreparedPack { repo_id: repo.repo_id.clone(), updates: updates.clone(), push_options, }); Ok(PreparedPush { unpack: Ok(()), updates, }) } } /// Assembles the recording quartet into a /// [`GitSeam`](alkssh::GitSeam) taking one shared `Arc`, the /// default alkgit budget, the push-options gate off (the v1 default). pub fn git_seam(mem: &Arc) -> alkssh::GitSeam { alkssh::GitSeam { limits: alkgit::Limits::default(), registry: Arc::clone(mem) as _, refs: Arc::clone(mem) as _, packgen: Arc::clone(mem) as _, ingest: Arc::clone(mem) as _, push_options_negotiated: false, pipeline: Arc::new(tokio::sync::Semaphore::new(2)), } }