- dispatch: the full exec grammar replaces the wave-2 skeleton probe (quoted/unquoted paths, the .git normalization decision, unknown- service / service-only decline-with-reason class; the probe's unit rows migrate with their pinned verdicts preserved) - git_map: the git row dispatches serve_duplex_resolved over the channel duplex (repo id validated, never resolved; the exit-status mapping 0/1 decided per POC-3 §A.5; status before close from the spawned task), with the resolved identity riding the seam - handler: the store-and-take channel handoff (stored at the open, taken in the spawned serving task, released at close) and the max_exec_command_len pre-parse cap - config course correction: GitSeam carries the assembly-supplied alkgit Limits (the ADR-009 budget alkgit's contract assigns the assembler) - tests: tests/git_mapping.rs (byte-level process-mode advertisement, ls-refs round + exit 0, the collapsed-denial byte-order + exit 1, the identity-ride authorize-gate witness, the packless CAS push, the grammar/cap decline rows, the sequential-serve store row, and the real-tool git 2.43 clone smoke over a TCP mount with a skip-with-reason posture); dispatch_core/observe rows updated to the new decision set; tests/common/git_doubles.rs (the hand-rolled recording alkgit quartet) Verification: cargo test (default), --features session, --features git, --all-features; clippy -D warnings ×2; fmt --check; cargo doc --no-deps; taskgraph validate
285 lines
8.5 KiB
Rust
285 lines
8.5 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 — 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;
|
|
|
|
/// 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>,
|
|
}
|
|
|
|
/// 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>>,
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
|
|
/// 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> {
|
|
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) — 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)),
|
|
}
|
|
}
|