1use objects::object::MarkerName;
5use oplog::RecordedHead;
6
7use super::*;
8
9fn recorded_head(head: &Head) -> RecordedHead {
10 match head {
11 Head::Attached { thread } => RecordedHead::Attached {
12 thread: thread.to_string(),
13 },
14 Head::Detached { state } => RecordedHead::Detached { state: *state },
15 }
16}
17
18fn is_conflict(error: &HeddleError) -> bool {
19 matches!(error, HeddleError::Conflict(_))
20}
21
22impl Repository {
23 pub fn head_update_record(previous: &Head, new: &Head) -> OpRecord {
25 OpRecord::HeadUpdate {
26 previous: recorded_head(previous),
27 new: recorded_head(new),
28 }
29 }
30
31 pub fn set_thread_recorded(&self, name: &ThreadName, state: &StateId) -> Result<()> {
36 loop {
37 let expected = match self.refs.get_thread(name)? {
38 Some(current) if current == *state => return Ok(()),
39 Some(current) => RefExpectation::Value(current),
40 None => RefExpectation::Missing,
41 };
42 match self.set_thread_recorded_cas(name, expected, state) {
43 Err(error) if is_conflict(&error) => continue,
44 result => return result,
45 }
46 }
47 }
48
49 pub fn set_thread_recorded_cas(
51 &self,
52 name: &ThreadName,
53 expected: RefExpectation<StateId>,
54 state: &StateId,
55 ) -> Result<()> {
56 let record = match &expected {
57 RefExpectation::Missing => OpRecord::ThreadCreate {
58 name: name.to_string(),
59 state: *state,
60 manager_snapshot: None,
61 },
62 RefExpectation::Value(old_state) if old_state == state => {
63 return match self.refs.get_thread(name)? {
64 Some(current) if current == *old_state => Ok(()),
65 current => Err(HeddleError::Conflict(format!(
66 "thread {} expected {}, found {}",
67 name,
68 old_state,
69 current
70 .map(|value| value.to_string())
71 .unwrap_or_else(|| "missing".to_string())
72 ))),
73 };
74 }
75 RefExpectation::Value(old_state) => OpRecord::ThreadUpdate {
76 name: name.to_string(),
77 old_state: *old_state,
78 new_state: *state,
79 manager_snapshots: None,
80 },
81 RefExpectation::Any => {
82 return self.set_thread_recorded(name, state);
83 }
84 };
85 self.commit_and_publish(
86 vec![record],
87 &[RefUpdate::Thread {
88 name: name.clone(),
89 expected,
90 new: Some(*state),
91 }],
92 )
93 }
94
95 pub fn delete_thread_recorded(&self, name: &ThreadName) -> Result<Option<StateId>> {
98 loop {
99 let Some(current) = self.refs.get_thread(name)? else {
100 return Ok(None);
101 };
102 let result = self.commit_and_publish(
103 vec![OpRecord::ThreadDelete {
104 name: name.to_string(),
105 state: current,
106 }],
107 &[RefUpdate::Thread {
108 name: name.clone(),
109 expected: RefExpectation::Value(current),
110 new: None,
111 }],
112 );
113 match result {
114 Ok(()) => return Ok(Some(current)),
115 Err(error) if is_conflict(&error) => continue,
116 Err(error) => return Err(error),
117 }
118 }
119 }
120
121 pub fn write_head_recorded(&self, new: &Head) -> Result<()> {
123 loop {
124 let previous = self.refs.read_head()?;
125 if previous == *new {
126 return Ok(());
127 }
128 let result = self.commit_and_publish(
129 vec![Self::head_update_record(&previous, new)],
130 &[RefUpdate::Head {
131 expected: RefExpectation::Value(previous),
132 new: new.clone(),
133 }],
134 );
135 match result {
136 Err(error) if is_conflict(&error) => continue,
137 result => return result,
138 }
139 }
140 }
141
142 pub fn create_marker_recorded(&self, name: &MarkerName, state: &StateId) -> Result<()> {
144 self.commit_and_publish(
145 vec![OpRecord::MarkerCreate {
146 name: name.to_string(),
147 state: *state,
148 }],
149 &[RefUpdate::Marker {
150 name: name.clone(),
151 expected: RefExpectation::Missing,
152 new: Some(*state),
153 }],
154 )
155 }
156
157 pub fn set_marker_recorded_cas(
159 &self,
160 name: &MarkerName,
161 expected: RefExpectation<StateId>,
162 state: &StateId,
163 ) -> Result<()> {
164 let records = match &expected {
165 RefExpectation::Missing => vec![OpRecord::MarkerCreate {
166 name: name.to_string(),
167 state: *state,
168 }],
169 RefExpectation::Value(old_state) if old_state == state => {
170 return match self.refs.get_marker(name)? {
171 Some(current) if current == *old_state => Ok(()),
172 current => Err(HeddleError::Conflict(format!(
173 "marker {} expected {}, found {}",
174 name,
175 old_state,
176 current
177 .map(|value| value.to_string())
178 .unwrap_or_else(|| "missing".to_string())
179 ))),
180 };
181 }
182 RefExpectation::Value(old_state) => vec![
183 OpRecord::MarkerDelete {
184 name: name.to_string(),
185 state: *old_state,
186 },
187 OpRecord::MarkerCreate {
188 name: name.to_string(),
189 state: *state,
190 },
191 ],
192 RefExpectation::Any => loop {
193 let exact = match self.refs.get_marker(name)? {
194 Some(current) if current == *state => return Ok(()),
195 Some(current) => RefExpectation::Value(current),
196 None => RefExpectation::Missing,
197 };
198 match self.set_marker_recorded_cas(name, exact, state) {
199 Err(error) if is_conflict(&error) => continue,
200 result => return result,
201 }
202 },
203 };
204 self.commit_and_publish(
205 records,
206 &[RefUpdate::Marker {
207 name: name.clone(),
208 expected,
209 new: Some(*state),
210 }],
211 )
212 }
213
214 pub fn delete_marker_recorded(&self, name: &MarkerName) -> Result<Option<StateId>> {
216 loop {
217 let Some(current) = self.refs.get_marker(name)? else {
218 return Ok(None);
219 };
220 let result = self.commit_and_publish(
221 vec![OpRecord::MarkerDelete {
222 name: name.to_string(),
223 state: current,
224 }],
225 &[RefUpdate::Marker {
226 name: name.clone(),
227 expected: RefExpectation::Value(current),
228 new: None,
229 }],
230 );
231 match result {
232 Ok(()) => return Ok(Some(current)),
233 Err(error) if is_conflict(&error) => continue,
234 Err(error) => return Err(error),
235 }
236 }
237 }
238
239 pub fn set_remote_thread_recorded(
241 &self,
242 remote: &str,
243 thread: &ThreadName,
244 state: &StateId,
245 ) -> Result<()> {
246 loop {
247 let expected = match self.refs.get_remote_thread(remote, thread)? {
248 Some(current) if current == *state => return Ok(()),
249 Some(current) => RefExpectation::Value(current),
250 None => RefExpectation::Missing,
251 };
252 let result = self.commit_and_publish(
253 vec![OpRecord::RemoteThreadUpdate {
254 remote: remote.to_string(),
255 thread: thread.to_string(),
256 state: *state,
257 }],
258 &[RefUpdate::RemoteThread {
259 remote: remote.to_string(),
260 thread: thread.clone(),
261 expected,
262 new: Some(*state),
263 }],
264 );
265 match result {
266 Err(error) if is_conflict(&error) => continue,
267 result => return result,
268 }
269 }
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use tempfile::TempDir;
276
277 use super::*;
278
279 fn test_repo() -> (TempDir, Repository) {
280 let temp = TempDir::new().unwrap();
281 let repo = Repository::init_default(temp.path()).unwrap();
282 (temp, repo)
283 }
284
285 #[test]
286 fn recorded_ref_helpers_publish_with_exact_records() {
287 let (_temp, repo) = test_repo();
288 let thread = ThreadName::new("feature");
289 let first = crate::test_state_id();
290 let second = crate::test_state_id();
291
292 repo.set_thread_recorded(&thread, &first).unwrap();
293 repo.set_thread_recorded(&thread, &second).unwrap();
294
295 assert_eq!(repo.refs().get_thread(&thread).unwrap(), Some(second));
296 let recent = repo.oplog().recent(8).unwrap();
297 assert!(recent.iter().any(|entry| matches!(
298 &entry.operation,
299 OpRecord::ThreadCreate { name, state, .. }
300 if name == "feature" && *state == first
301 )));
302 assert!(recent.iter().any(|entry| matches!(
303 &entry.operation,
304 OpRecord::ThreadUpdate {
305 name,
306 old_state,
307 new_state,
308 ..
309 } if name == "feature" && *old_state == first && *new_state == second
310 )));
311 }
312
313 #[test]
314 fn head_and_remote_updates_are_replayable_records() {
315 let (_temp, repo) = test_repo();
316 let previous = repo.refs().read_head().unwrap();
317 let detached = crate::test_state_id();
318 repo.write_head_recorded(&Head::Detached { state: detached })
319 .unwrap();
320
321 let remote_state = crate::test_state_id();
322 let remote_thread = ThreadName::new("main");
323 repo.set_remote_thread_recorded("origin", &remote_thread, &remote_state)
324 .unwrap();
325
326 assert_eq!(
327 repo.refs().read_head().unwrap(),
328 Head::Detached { state: detached }
329 );
330 assert_eq!(
331 repo.refs()
332 .get_remote_thread("origin", &remote_thread)
333 .unwrap(),
334 Some(remote_state)
335 );
336 let recent = repo.oplog().recent(8).unwrap();
337 assert!(recent.iter().any(|entry| matches!(
338 &entry.operation,
339 OpRecord::HeadUpdate {
340 previous: recorded_previous,
341 new: RecordedHead::Detached { state },
342 } if recorded_previous == &recorded_head(&previous) && *state == detached
343 )));
344 assert!(recent.iter().any(|entry| matches!(
345 &entry.operation,
346 OpRecord::RemoteThreadUpdate {
347 remote,
348 thread,
349 state,
350 } if remote == "origin" && thread == "main" && *state == remote_state
351 )));
352 }
353}