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 },
92 [],
93 );
94 let t = StageTransition::Create {
95 sig_id: "fac".into(),
96 stage_id: "s1".into(),
97 };
98 (op, t)
99 }
100
101 #[test]
102 fn parentless_op_against_empty_head_succeeds() {
103 let tmp = tempfile::tempdir().unwrap();
104 let log = OpLog::open(tmp.path()).unwrap();
105 let (op, t) = add_fac();
106 let head = apply(&log, None, op, t).unwrap();
107 assert!(log.get(&head.op_id).unwrap().is_some());
108 }
109
110 #[test]
111 fn parentless_op_against_non_empty_head_is_stale() {
112 let tmp = tempfile::tempdir().unwrap();
113 let log = OpLog::open(tmp.path()).unwrap();
114 let (op1, t1) = add_fac();
115 let head1 = apply(&log, None, op1, t1).unwrap();
116 let (op2, t2) = add_fac(); let err = apply(&log, Some(&head1.op_id), op2, t2).unwrap_err();
118 match err {
119 ApplyError::StaleParent { expected, op_parents } => {
120 assert_eq!(expected.as_deref(), Some(head1.op_id.as_str()));
121 assert!(op_parents.is_empty());
122 }
123 other => panic!("expected StaleParent, got {other:?}"),
124 }
125 }
126
127 #[test]
128 fn single_parent_matching_head_succeeds() {
129 let tmp = tempfile::tempdir().unwrap();
130 let log = OpLog::open(tmp.path()).unwrap();
131 let (op1, t1) = add_fac();
132 let head1 = apply(&log, None, op1, t1).unwrap();
133 let modify = Operation::new(
134 OperationKind::ModifyBody {
135 sig_id: "fac".into(),
136 from_stage_id: "s1".into(),
137 to_stage_id: "s2".into(),
138 },
139 [head1.op_id.clone()],
140 );
141 let t = StageTransition::Replace {
142 sig_id: "fac".into(),
143 from: "s1".into(),
144 to: "s2".into(),
145 };
146 let head2 = apply(&log, Some(&head1.op_id), modify, t).unwrap();
147 assert_ne!(head2.op_id, head1.op_id);
148 }
149
150 #[test]
151 fn single_parent_not_matching_head_is_stale() {
152 let tmp = tempfile::tempdir().unwrap();
153 let log = OpLog::open(tmp.path()).unwrap();
154 let (op1, t1) = add_fac();
155 let head1 = apply(&log, None, op1, t1).unwrap();
156 let bogus = Operation::new(
157 OperationKind::ModifyBody {
158 sig_id: "fac".into(),
159 from_stage_id: "s1".into(),
160 to_stage_id: "s2".into(),
161 },
162 ["someone-else".into()],
163 );
164 let t = StageTransition::Replace {
165 sig_id: "fac".into(),
166 from: "s1".into(),
167 to: "s2".into(),
168 };
169 let err = apply(&log, Some(&head1.op_id), bogus, t).unwrap_err();
170 match err {
171 ApplyError::StaleParent { expected, op_parents } => {
172 assert_eq!(expected.as_deref(), Some(head1.op_id.as_str()));
173 assert_eq!(op_parents, vec!["someone-else".to_string()]);
174 }
175 other => panic!("expected StaleParent, got {other:?}"),
176 }
177 }
178
179 #[test]
180 fn merge_op_with_known_second_parent_succeeds() {
181 let tmp = tempfile::tempdir().unwrap();
182 let log = OpLog::open(tmp.path()).unwrap();
183 let (op_a, t_a) = add_fac();
184 let head_a = apply(&log, None, op_a, t_a).unwrap();
185 let other = Operation::new(
186 OperationKind::AddFunction {
187 sig_id: "double".into(),
188 stage_id: "d1".into(),
189 effects: BTreeSet::new(),
190 },
191 [],
192 );
193 let head_b = apply(&log, None, other, StageTransition::Create {
194 sig_id: "double".into(), stage_id: "d1".into(),
195 }).unwrap();
196 let merge = Operation::new(
198 OperationKind::Merge { resolved: 1 },
199 [head_a.op_id.clone(), head_b.op_id.clone()],
200 );
201 let t = StageTransition::Merge {
202 entries: std::iter::once(("double".to_string(), Some("d1".to_string())))
203 .collect(),
204 };
205 let merged = apply(&log, Some(&head_a.op_id), merge, t).unwrap();
206 assert!(log.get(&merged.op_id).unwrap().is_some());
207 }
208
209 #[test]
210 fn merge_op_with_unknown_second_parent_fails() {
211 let tmp = tempfile::tempdir().unwrap();
212 let log = OpLog::open(tmp.path()).unwrap();
213 let (op_a, t_a) = add_fac();
214 let head_a = apply(&log, None, op_a, t_a).unwrap();
215 let merge = Operation::new(
216 OperationKind::Merge { resolved: 0 },
217 [head_a.op_id.clone(), "ghost".into()],
218 );
219 let t = StageTransition::Merge { entries: Default::default() };
220 let err = apply(&log, Some(&head_a.op_id), merge, t).unwrap_err();
221 match err {
222 ApplyError::UnknownMergeParent(id) => {
223 assert_eq!(id, "ghost");
224 }
225 other => panic!("expected UnknownMergeParent, got {other:?}"),
226 }
227 }
228
229 #[test]
230 fn three_parent_op_is_stale() {
231 let tmp = tempfile::tempdir().unwrap();
233 let log = OpLog::open(tmp.path()).unwrap();
234 let (op_a, t_a) = add_fac();
235 let head_a = apply(&log, None, op_a, t_a).unwrap();
236
237 let weird = Operation::new(
240 OperationKind::ModifyBody {
241 sig_id: "fac".into(),
242 from_stage_id: "s1".into(),
243 to_stage_id: "s2".into(),
244 },
245 [head_a.op_id.clone(), "p2".into(), "p3".into()],
246 );
247 let t = StageTransition::Replace {
248 sig_id: "fac".into(), from: "s1".into(), to: "s2".into(),
249 };
250 let err = apply(&log, Some(&head_a.op_id), weird, t).unwrap_err();
251 assert!(matches!(err, ApplyError::StaleParent { .. }));
252 }
253
254 #[test]
255 fn single_parent_against_empty_head_is_stale() {
256 let tmp = tempfile::tempdir().unwrap();
258 let log = OpLog::open(tmp.path()).unwrap();
259 let modify = Operation::new(
260 OperationKind::ModifyBody {
261 sig_id: "fac".into(),
262 from_stage_id: "s1".into(),
263 to_stage_id: "s2".into(),
264 },
265 ["claimed-parent".into()],
266 );
267 let t = StageTransition::Replace {
268 sig_id: "fac".into(), from: "s1".into(), to: "s2".into(),
269 };
270 let err = apply(&log, None, modify, t).unwrap_err();
271 match err {
272 ApplyError::StaleParent { expected, op_parents } => {
273 assert_eq!(expected, None);
274 assert_eq!(op_parents, vec!["claimed-parent".to_string()]);
275 }
276 other => panic!("expected StaleParent, got {other:?}"),
277 }
278 }
279
280 #[test]
281 fn self_merge_is_stale() {
282 let tmp = tempfile::tempdir().unwrap();
285 let log = OpLog::open(tmp.path()).unwrap();
286 let (op_a, t_a) = add_fac();
287 let head_a = apply(&log, None, op_a, t_a).unwrap();
288
289 let json = serde_json::json!({
292 "op": "merge",
293 "resolved": 0,
294 "parents": [head_a.op_id.clone(), head_a.op_id.clone()],
295 });
296 let weird: Operation = serde_json::from_value(json).unwrap();
297 assert_eq!(weird.parents.len(), 2,
298 "round-trip should preserve duplicates if Operation deserialization doesn't dedup");
299 let t = StageTransition::Merge { entries: Default::default() };
300 let err = apply(&log, Some(&head_a.op_id), weird, t).unwrap_err();
301 assert!(matches!(err, ApplyError::StaleParent { .. }));
302 }
303}