git-ents.gitmain
⌘K
foforge
commit 02a5c71
multi: add a RefStore write half to the in-memory ref-store fixture

MemRefStore previously implemented only RefStoreRead, since every prior consumer (ents-gate, ents-query) seeds state directly via set/remove and never exercises the write seam itself. ents-receive orchestrates real RefStore::transaction calls, and none of ents-testutil’s existing signed-commit seeding helpers are generic over the store type, so a LooseRefStore-backed fixture would mean re-deriving that plumbing rather than reusing it. Adding transaction() here (a straightforward in-memory CAS over the same map, single-Mutex, all-or-nothing per the trait’s own contract) lets ents-receive’s tests reuse the whole existing fixture vocabulary unchanged.

Joseph D. Carpinelli · 1 month ago

Reviews

No reviews of this commit yet — record a verdict below.

Start a review

verdict

crates/ents-testutil/src/refs.rs @@ -1,19 +1,31 @@ -//! An in-memory [`RefStoreRead`] implementation for fixtures. +//! An in-memory [`RefStoreRead`] (and [`RefStore`]) implementation for +//! fixtures. use std::collections::BTreeMap; use std::sync::Mutex; use gix::refs::{FullName, FullNameRef}; use gix_hash::ObjectId; -use gix_ref_store::{RefIter, RefStoreRead, Result}; +use gix_ref_store::{Expected, RefEdit, RefIter, RefStore, RefStoreRead, Result, TxOutcome}; /// An in-memory ref store: a name-to-oid map behind the same /// [`RefStoreRead`] trait production code consumes. /// -/// Mutation happens through [`MemRefStore::set`] and -/// [`MemRefStore::remove`] — deliberately *not* through the `RefStore` -/// write half, because fixtures seed state directly; the CAS discipline -/// itself is `gix-ref-store`'s to test. +/// Fixture *seeding* goes through [`MemRefStore::set`] and +/// [`MemRefStore::remove`] — deliberately bypassing compare-and-swap, +/// because setup is not a transaction under test. [`MemRefStore`] also +/// implements the real [`RefStore`] write half: a straightforward in-memory +/// CAS transaction over the same map, checked against one consistent +/// snapshot exactly per [`RefStore::transaction`]'s contract. This exists +/// so a crate orchestrating *real* writes through the seam under test +/// (`ents-receive`'s `receive`, and later `ents-sync`) has a fixture that +/// can be written through `RefStore` itself, not only seeded directly — +/// the CAS discipline a genuine backend must uphold under concurrent, +/// racing writers (lock ordering, precondition-read races) stays +/// `gix-ref-store`'s own [`gix_ref_store::LooseRefStore`] to test, via its +/// conformance suite; this type's transaction is single-threaded-simple by +/// construction (one `Mutex`; the whole batch runs under one lock +/// acquisition). /// /// # Examples /// @@ -120,3 +132,39 @@ Ok(RefIter::new(snapshot.into_iter())) } } + +impl RefStore for MemRefStore { + /// Apply `edits` as one atomic compare-and-swap transaction: every + /// precondition is checked against the same locked snapshot, and + /// either every edit applies or none do, per [`RefStore::transaction`]'s + /// contract. + fn transaction(&self, edits: &[RefEdit]) -> Result<TxOutcome> { + let mut refs = self.locked(); + for edit in edits { + let key = edit.name.as_bstr().to_string(); + let current = refs.get(&key).copied(); + let precondition_met = match edit.expected { + Expected::Any => true, + Expected::MustNotExist => current.is_none(), + Expected::MustExistAndMatch(oid) => current == Some(oid), + }; + if !precondition_met { + return Ok(TxOutcome::Rejected { + name: edit.name.clone(), + }); + } + } + for edit in edits { + let key = edit.name.as_bstr().to_string(); + match edit.new { + Some(oid) => { + refs.insert(key, oid); + } + None => { + refs.remove(&key); + } + } + } + Ok(TxOutcome::Applied) + } +}