Same treatment for the prose the compiler carries: doc and line comments across src/ and tests/, test assertion and error-message strings (prose surfaces: the tunnel-map registration/establish errors, the key-source decode payload, assertion messages), scripts/matrix.sh comment lines, the fuzz sidecar's comments and README (corpus seeds untouched), and the Cargo.toml comment headers plus the crates.io description. The AGENTS.md no-em-dash rule (convention 14) now holds for the whole tracked tree. Verification: repo-wide git grep is clean (single exempt hit: the verification command's own search pattern), cargo test (18 suites, zero failures, default and per-feature builds via scripts/matrix.sh), clippy --all-targets -D warnings and clippy all-features, fmt --check, doc --no-deps, and the fuzz corpus gate (fuzz/shared, 11 tests) all green.
341 lines
10 KiB
Rust
341 lines
10 KiB
Rust
//! 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<String, Vec<AccessAction>>,
|
|
pub refs: Vec<RefLine>,
|
|
}
|
|
|
|
impl RepoSetup {
|
|
pub fn private_granted(repo_id: &str, identity_id: &str, refs: Vec<RefLine>) -> 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<AccessAction>,
|
|
refs: Vec<RefLine>,
|
|
) -> 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<RefLine>) -> 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<RefUpdate>,
|
|
pub atomic: bool,
|
|
}
|
|
|
|
/// One recorded pack ingestion (`GitPackIngest::prepare`).
|
|
#[derive(Clone, Debug)]
|
|
pub struct PreparedPack {
|
|
pub repo_id: String,
|
|
pub updates: Vec<RefUpdate>,
|
|
pub push_options: Option<PushOptions>,
|
|
}
|
|
|
|
/// One recorded pack generation (`GitPackGen::generate`).
|
|
#[derive(Clone, Debug)]
|
|
pub struct GeneratedPack {
|
|
pub repo_id: String,
|
|
pub wants: Vec<ObjectId>,
|
|
pub boundary_haves: Vec<ObjectId>,
|
|
}
|
|
|
|
/// 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<RepoRegistry>,
|
|
refs: RwLock<HashMap<String, Vec<RefLine>>>,
|
|
resolves: Mutex<Vec<String>>,
|
|
applied: Mutex<Vec<AppliedUpdates>>,
|
|
prepared: Mutex<Vec<PreparedPack>>,
|
|
generated: Mutex<Vec<GeneratedPack>>,
|
|
gates: Mutex<HashMap<String, Arc<MemGate>>>,
|
|
}
|
|
|
|
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<String> {
|
|
self.resolves.lock().clone()
|
|
}
|
|
|
|
/// Every CAS application, in call order.
|
|
pub fn applied(&self) -> Vec<AppliedUpdates> {
|
|
self.applied.lock().clone()
|
|
}
|
|
|
|
/// Every pack preparation, in call order.
|
|
pub fn prepared(&self) -> Vec<PreparedPack> {
|
|
self.prepared.lock().clone()
|
|
}
|
|
|
|
/// Every pack generation, in call order.
|
|
pub fn generated(&self) -> Vec<GeneratedPack> {
|
|
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<MemGate> {
|
|
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<RepoRecord, RegistryError> {
|
|
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<String, (Visibility, BTreeMap<String, Vec<AccessAction>>)>;
|
|
|
|
#[async_trait]
|
|
impl GitRegistry for MemGit {
|
|
async fn resolve(&self, repo_id: &str) -> Result<RepoRecord, RegistryError> {
|
|
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<Vec<RefLine>, 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<RefUpdate>,
|
|
atomic: bool,
|
|
) -> Result<Vec<RefOutcome>, 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<ObjectId>,
|
|
boundary_haves: Vec<ObjectId>,
|
|
_limits: alkgit::Limits,
|
|
_sink: Box<dyn std::io::Write + Send>,
|
|
) -> 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<ObjectId>,
|
|
) -> Result<Vec<ObjectId>, StorageError> {
|
|
Ok(Vec::new())
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl GitPackIngest for MemGit {
|
|
async fn prepare(
|
|
&self,
|
|
repo: &RepoRecord,
|
|
updates: Vec<RefUpdate>,
|
|
_pack: Box<dyn std::io::Read + Send>,
|
|
push_options: Option<PushOptions>,
|
|
_limits: alkgit::Limits,
|
|
) -> Result<PreparedPush, StorageError> {
|
|
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<MemGit>`, the
|
|
/// default alkgit budget, the push-options gate off (the v1 default).
|
|
pub fn git_seam(mem: &Arc<MemGit>) -> 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)),
|
|
}
|
|
}
|