git-ents.gitmain
⌘K
foforge
refs.rs170 lines · 5.9 KB · rusthistorycomment on this file
1//! An in-memory [`RefStoreRead`] (and [`RefStore`]) implementation for
2//! fixtures.
3
4use std::collections::BTreeMap;
5use std::sync::Mutex;
6
7use gix::refs::{FullName, FullNameRef};
8use gix_hash::ObjectId;
9use gix_ref_store::{Expected, RefEdit, RefIter, RefStore, RefStoreRead, Result, TxOutcome};
10
11/// An in-memory ref store: a name-to-oid map behind the same
12/// [`RefStoreRead`] trait production code consumes.
13///
14/// Fixture *seeding* goes through [`MemRefStore::set`] and
15/// [`MemRefStore::remove`] — deliberately bypassing compare-and-swap,
16/// because setup is not a transaction under test. [`MemRefStore`] also
17/// implements the real [`RefStore`] write half: a straightforward in-memory
18/// CAS transaction over the same map, checked against one consistent
19/// snapshot exactly per [`RefStore::transaction`]'s contract. This exists
20/// so a crate orchestrating *real* writes through the seam under test
21/// (`ents-receive`'s `receive`, and later `ents-sync`) has a fixture that
22/// can be written through `RefStore` itself, not only seeded directly —
23/// the CAS discipline a genuine backend must uphold under concurrent,
24/// racing writers (lock ordering, precondition-read races) stays
25/// `gix-ref-store`'s own [`gix_ref_store::LooseRefStore`] to test, via its
26/// conformance suite; this type's transaction is single-threaded-simple by
27/// construction (one `Mutex`; the whole batch runs under one lock
28/// acquisition).
29///
30/// # Examples
31///
32/// ```
33/// use ents_testutil::MemRefStore;
34/// use gix_ref_store::RefStoreRead;
35///
36/// let store = MemRefStore::default();
37/// let name: gix::refs::FullName = "refs/heads/main".try_into().expect("valid");
38/// let oid = gix_hash::ObjectId::null(gix_hash::Kind::Sha1);
39///
40/// store.set(name.as_ref(), oid);
41/// assert_eq!(store.get(name.as_ref()).expect("readable"), Some(oid));
42///
43/// store.remove(name.as_ref());
44/// assert_eq!(store.get(name.as_ref()).expect("readable"), None);
45/// ```
46#[derive(Debug, Default)]
47pub struct MemRefStore {
48 refs: Mutex<BTreeMap<String, ObjectId>>,
49}
50
51impl MemRefStore {
52 /// Set `name` to `oid`, creating or overwriting it.
53 pub fn set(&self, name: &FullNameRef, oid: ObjectId) {
54 self.locked().insert(name.as_bstr().to_string(), oid);
55 }
56
57 /// Set the ref named by `name` (a full refname string) to `oid`.
58 ///
59 /// Panics if `name` is not a valid full refname.
60 ///
61 /// # Examples
62 ///
63 /// ```
64 /// use ents_testutil::MemRefStore;
65 ///
66 /// let store = MemRefStore::default();
67 /// store.set_str("refs/heads/main", gix_hash::ObjectId::null(gix_hash::Kind::Sha1));
68 /// ```
69 pub fn set_str(&self, name: &str, oid: ObjectId) {
70 let name: FullName = name.try_into().expect("valid refname in fixture");
71 self.set(name.as_ref(), oid);
72 }
73
74 /// Delete `name` if present.
75 pub fn remove(&self, name: &FullNameRef) {
76 let key = name.as_bstr().to_string();
77 let _removed = self.locked().remove(&key);
78 }
79
80 /// A deep copy of this store's current refs — the fixture analogue of
81 /// a fetch, for pre-flight call-site tests that evaluate against a
82 /// clone's ref state.
83 ///
84 /// # Examples
85 ///
86 /// ```
87 /// use ents_testutil::MemRefStore;
88 /// use gix_ref_store::RefStoreRead;
89 ///
90 /// let store = MemRefStore::default();
91 /// store.set_str("refs/heads/main", gix_hash::ObjectId::null(gix_hash::Kind::Sha1));
92 ///
93 /// let fetched = store.fetched_copy();
94 /// let name: gix::refs::FullName = "refs/heads/main".try_into().expect("valid");
95 /// assert_eq!(
96 /// fetched.get(name.as_ref()).expect("readable"),
97 /// store.get(name.as_ref()).expect("readable"),
98 /// );
99 /// ```
100 #[must_use]
101 pub fn fetched_copy(&self) -> Self {
102 Self {
103 refs: Mutex::new(self.locked().clone()),
104 }
105 }
106
107 fn locked(&self) -> std::sync::MutexGuard<'_, BTreeMap<String, ObjectId>> {
108 self.refs
109 .lock()
110 .expect("ref-store mutex poisoned in fixture")
111 }
112}
113
114impl RefStoreRead for MemRefStore {
115 fn get(&self, name: &FullNameRef) -> Result<Option<ObjectId>> {
116 Ok(self.locked().get(&name.as_bstr().to_string()).copied())
117 }
118
119 fn iter_prefix(&self, prefix: &str) -> Result<RefIter> {
120 let snapshot: Vec<_> = self
121 .locked()
122 .range(prefix.to_owned()..)
123 .take_while(|(name, _)| name.starts_with(prefix))
124 .map(|(name, oid)| {
125 let full: FullName = name
126 .as_str()
127 .try_into()
128 .expect("only valid refnames are ever inserted");
129 Ok((full, *oid))
130 })
131 .collect();
132 Ok(RefIter::new(snapshot.into_iter()))
133 }
134}
135
136impl RefStore for MemRefStore {
137 /// Apply `edits` as one atomic compare-and-swap transaction: every
138 /// precondition is checked against the same locked snapshot, and
139 /// either every edit applies or none do, per [`RefStore::transaction`]'s
140 /// contract.
141 fn transaction(&self, edits: &[RefEdit]) -> Result<TxOutcome> {
142 let mut refs = self.locked();
143 for edit in edits {
144 let key = edit.name.as_bstr().to_string();
145 let current = refs.get(&key).copied();
146 let precondition_met = match edit.expected {
147 Expected::Any => true,
148 Expected::MustNotExist => current.is_none(),
149 Expected::MustExistAndMatch(oid) => current == Some(oid),
150 };
151 if !precondition_met {
152 return Ok(TxOutcome::Rejected {
153 name: edit.name.clone(),
154 });
155 }
156 }
157 for edit in edits {
158 let key = edit.name.as_bstr().to_string();
159 match edit.new {
160 Some(oid) => {
161 refs.insert(key, oid);
162 }
163 None => {
164 refs.remove(&key);
165 }
166 }
167 }
168 Ok(TxOutcome::Applied)
169 }
170}