1use crate::op_log::OpLog;
6use crate::operation::{OpId, Operation, OperationRecord, StageTransition};
7use std::io;
8
9#[derive(Debug)]
10pub struct NewHead {
11 pub op_id: OpId,
12 pub record: OperationRecord,
13}
14
15#[derive(Debug, thiserror::Error)]
16pub enum ApplyError {
17 #[error("stale parent: branch head is {expected:?} but op's parents are {op_parents:?}")]
18 StaleParent {
19 expected: Option<OpId>,
20 op_parents: Vec<OpId>,
21 },
22 #[error("merge op references unknown second parent {0}")]
23 UnknownMergeParent(OpId),
24 #[error(transparent)]
25 Persist(#[from] io::Error),
26}
27
28pub fn apply(
39 op_log: &OpLog,
40 head_op: Option<&OpId>,
41 op: Operation,
42 transition: StageTransition,
43) -> Result<NewHead, ApplyError> {
44 match (op.parents.len(), head_op) {
45 (0, None) => {}
46 (1, Some(h)) if op.parents[0] == *h => {}
47 (2, Some(h)) => {
48 if op.parents[0] == op.parents[1] {
49 return Err(ApplyError::StaleParent {
50 expected: head_op.cloned(),
51 op_parents: op.parents.clone(),
52 });
53 }
54 if op.parents[0] != *h && op.parents[1] != *h {
55 return Err(ApplyError::StaleParent {
56 expected: head_op.cloned(),
57 op_parents: op.parents.clone(),
58 });
59 }
60 let other = if op.parents[0] == *h { &op.parents[1] } else { &op.parents[0] };
62 if op_log.get(other)?.is_none() {
63 return Err(ApplyError::UnknownMergeParent(other.clone()));
64 }
65 }
66 _ => {
67 return Err(ApplyError::StaleParent {
68 expected: head_op.cloned(),
69 op_parents: op.parents.clone(),
70 });
71 }
72 }
73
74 let record = OperationRecord::new(op, transition);
75 op_log.put(&record)?;
76 Ok(NewHead { op_id: record.op_id.clone(), record })
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use crate::operation::{OperationKind, StageTransition};
83 use std::collections::BTreeSet;
84
85 fn add_fac() -> (Operation, StageTransition) {
86 let op = Operation::new(
87 OperationKind::AddFunction {
88 sig_id: "fac".into(),
89 stage_id: "s1".into(),
90 effects: BTreeSet::new(),
91 budget_cost: None,
92 in_file: None,
93 },
94 [],
95 );
96 let t = StageTransition::Create {
97 sig_id: "fac".into(),
98 stage_id: "s1".into(),
99 };
100 (op, t)
101 }
102
103 #[test]
104 fn parentless_op_against_empty_head_succeeds() {
105 let tmp = tempfile::tempdir().unwrap();
106 let log = OpLog::open(tmp.path()).unwrap();
107 let (op, t) = add_fac();
108 let head = apply(&log, None, op, t).unwrap();
109 assert!(log.get(&head.op_id).unwrap().is_some());
110 }
111
112 #[test]
113 fn parentless_op_against_non_empty_head_is_stale() {
114 let tmp = tempfile::tempdir().unwrap();
115 let log = OpLog::open(tmp.path()).unwrap();
116 let (op1, t1) = add_fac();
117 let head1 = apply(&log, None, op1, t1).unwrap();
118 let (op2, t2) = add_fac(); let err = apply(&log, Some(&head1.op_id), op2, t2).unwrap_err();
120 match err {
121 ApplyError::StaleParent { expected, op_parents } => {
122 assert_eq!(expected.as_deref(), Some(head1.op_id.as_str()));
123 assert!(op_parents.is_empty());
124 }
125 other => panic!("expected StaleParent, got {other:?}"),
126 }
127 }
128
129 #[test]
130 fn single_parent_matching_head_succeeds() {
131 let tmp = tempfile::tempdir().unwrap();
132 let log = OpLog::open(tmp.path()).unwrap();
133 let (op1, t1) = add_fac();
134 let head1 = apply(&log, None, op1, t1).unwrap();
135 let modify = Operation::new(
136 OperationKind::ModifyBody {
137 sig_id: "fac".into(),
138 from_stage_id: "s1".into(),
139 to_stage_id: "s2".into(),
140 from_budget: None,
141 to_budget: None,
142 to_sig_id: None,
143 },
144 [head1.op_id.clone()],
145 );
146 let t = StageTransition::Replace {
147 sig_id: "fac".into(),
148 from: "s1".into(),
149 to: "s2".into(),
150 };
151 let head2 = apply(&log, Some(&head1.op_id), modify, t).unwrap();
152 assert_ne!(head2.op_id, head1.op_id);
153 }
154
155 #[test]
156 fn single_parent_not_matching_head_is_stale() {
157 let tmp = tempfile::tempdir().unwrap();
158 let log = OpLog::open(tmp.path()).unwrap();
159 let (op1, t1) = add_fac();
160 let head1 = apply(&log, None, op1, t1).unwrap();
161 let bogus = Operation::new(
162 OperationKind::ModifyBody {
163 sig_id: "fac".into(),
164 from_stage_id: "s1".into(),
165 to_stage_id: "s2".into(),
166 from_budget: None,
167 to_budget: None,
168 to_sig_id: None,
169 },
170 ["someone-else".into()],
171 );
172 let t = StageTransition::Replace {
173 sig_id: "fac".into(),
174 from: "s1".into(),
175 to: "s2".into(),
176 };
177 let err = apply(&log, Some(&head1.op_id), bogus, t).unwrap_err();
178 match err {
179 ApplyError::StaleParent { expected, op_parents } => {
180 assert_eq!(expected.as_deref(), Some(head1.op_id.as_str()));
181 assert_eq!(op_parents, vec!["someone-else".to_string()]);
182 }
183 other => panic!("expected StaleParent, got {other:?}"),
184 }
185 }
186
187 #[test]
188 fn merge_op_with_known_second_parent_succeeds() {
189 let tmp = tempfile::tempdir().unwrap();
190 let log = OpLog::open(tmp.path()).unwrap();
191 let (op_a, t_a) = add_fac();
192 let head_a = apply(&log, None, op_a, t_a).unwrap();
193 let other = Operation::new(
194 OperationKind::AddFunction {
195 sig_id: "double".into(),
196 stage_id: "d1".into(),
197 effects: BTreeSet::new(),
198 budget_cost: None,
199 in_file: None,
200 },
201 [],
202 );
203 let head_b = apply(&log, None, other, StageTransition::Create {
204 sig_id: "double".into(), stage_id: "d1".into(),
205 }).unwrap();
206 let merge = Operation::new(
208 OperationKind::Merge { resolved: 1 },
209 [head_a.op_id.clone(), head_b.op_id.clone()],
210 );
211 let t = StageTransition::Merge {
212 entries: std::iter::once(("double".to_string(), Some("d1".to_string())))
213 .collect(),
214 };
215 let merged = apply(&log, Some(&head_a.op_id), merge, t).unwrap();
216 assert!(log.get(&merged.op_id).unwrap().is_some());
217 }
218
219 #[test]
220 fn merge_op_with_unknown_second_parent_fails() {
221 let tmp = tempfile::tempdir().unwrap();
222 let log = OpLog::open(tmp.path()).unwrap();
223 let (op_a, t_a) = add_fac();
224 let head_a = apply(&log, None, op_a, t_a).unwrap();
225 let merge = Operation::new(
226 OperationKind::Merge { resolved: 0 },
227 [head_a.op_id.clone(), "ghost".into()],
228 );
229 let t = StageTransition::Merge { entries: Default::default() };
230 let err = apply(&log, Some(&head_a.op_id), merge, t).unwrap_err();
231 match err {
232 ApplyError::UnknownMergeParent(id) => {
233 assert_eq!(id, "ghost");
234 }
235 other => panic!("expected UnknownMergeParent, got {other:?}"),
236 }
237 }
238
239 #[test]
240 fn three_parent_op_is_stale() {
241 let tmp = tempfile::tempdir().unwrap();
243 let log = OpLog::open(tmp.path()).unwrap();
244 let (op_a, t_a) = add_fac();
245 let head_a = apply(&log, None, op_a, t_a).unwrap();
246
247 let weird = Operation::new(
250 OperationKind::ModifyBody {
251 sig_id: "fac".into(),
252 from_stage_id: "s1".into(),
253 to_stage_id: "s2".into(),
254 from_budget: None,
255 to_budget: None,
256 to_sig_id: None,
257 },
258 [head_a.op_id.clone(), "p2".into(), "p3".into()],
259 );
260 let t = StageTransition::Replace {
261 sig_id: "fac".into(), from: "s1".into(), to: "s2".into(),
262 };
263 let err = apply(&log, Some(&head_a.op_id), weird, t).unwrap_err();
264 assert!(matches!(err, ApplyError::StaleParent { .. }));
265 }
266
267 #[test]
268 fn single_parent_against_empty_head_is_stale() {
269 let tmp = tempfile::tempdir().unwrap();
271 let log = OpLog::open(tmp.path()).unwrap();
272 let modify = Operation::new(
273 OperationKind::ModifyBody {
274 sig_id: "fac".into(),
275 from_stage_id: "s1".into(),
276 to_stage_id: "s2".into(),
277 from_budget: None,
278 to_budget: None,
279 to_sig_id: None,
280 },
281 ["claimed-parent".into()],
282 );
283 let t = StageTransition::Replace {
284 sig_id: "fac".into(), from: "s1".into(), to: "s2".into(),
285 };
286 let err = apply(&log, None, modify, t).unwrap_err();
287 match err {
288 ApplyError::StaleParent { expected, op_parents } => {
289 assert_eq!(expected, None);
290 assert_eq!(op_parents, vec!["claimed-parent".to_string()]);
291 }
292 other => panic!("expected StaleParent, got {other:?}"),
293 }
294 }
295
296 #[test]
297 fn self_merge_is_stale() {
298 let tmp = tempfile::tempdir().unwrap();
301 let log = OpLog::open(tmp.path()).unwrap();
302 let (op_a, t_a) = add_fac();
303 let head_a = apply(&log, None, op_a, t_a).unwrap();
304
305 let json = serde_json::json!({
308 "op": "merge",
309 "resolved": 0,
310 "parents": [head_a.op_id.clone(), head_a.op_id.clone()],
311 });
312 let weird: Operation = serde_json::from_value(json).unwrap();
313 assert_eq!(weird.parents.len(), 2,
314 "round-trip should preserve duplicates if Operation deserialization doesn't dedup");
315 let t = StageTransition::Merge { entries: Default::default() };
316 let err = apply(&log, Some(&head_a.op_id), weird, t).unwrap_err();
317 assert!(matches!(err, ApplyError::StaleParent { .. }));
318 }
319}