1use crate::patch::types::{OperationType, Patch};
13
14#[derive(Clone, Debug)]
16pub struct ComposeResult {
17 pub patch: Patch,
19 pub count: usize,
21}
22
23pub fn compose(
37 p1: &Patch,
38 p2: &Patch,
39 author: &str,
40 message: &str,
41) -> Result<ComposeResult, ComposeError> {
42 if !p2.parent_ids.contains(&p1.id) {
44 return Err(ComposeError::NotAncestor {
45 p1_id: p1.id.to_hex(),
46 p2_id: p2.id.to_hex(),
47 });
48 }
49
50 let mut composed_touch = p1.touch_set.clone();
52 for addr in p2.touch_set.iter() {
53 composed_touch.insert(addr.clone());
54 }
55
56 let composed_path = p2.target_path.clone().or(p1.target_path.clone());
58 let composed_payload = if !p2.payload.is_empty() {
59 p2.payload.clone()
60 } else {
61 p1.payload.clone()
62 };
63
64 let composed_op = match (&p1.operation_type, &p2.operation_type) {
70 (_, OperationType::Delete) => OperationType::Delete,
71 (_, OperationType::Move) => OperationType::Move,
72 (_, OperationType::Create) => {
73 if p1.operation_type == OperationType::Create {
74 OperationType::Create
75 } else {
76 OperationType::Modify
77 }
78 }
79 (OperationType::Create, OperationType::Modify) => OperationType::Create,
80 _ => OperationType::Modify,
81 };
82
83 let composed_patch = Patch::new(
84 composed_op,
85 composed_touch,
86 composed_path,
87 composed_payload,
88 p1.parent_ids.clone(), author.to_string(),
90 message.to_string(),
91 );
92
93 Ok(ComposeResult {
94 patch: composed_patch,
95 count: 2,
96 })
97}
98
99pub fn compose_chain(
106 patches: &[Patch],
107 author: &str,
108 message: &str,
109) -> Result<ComposeResult, ComposeError> {
110 if patches.is_empty() {
111 return Err(ComposeError::EmptyChain);
112 }
113 if patches.len() == 1 {
114 return Ok(ComposeResult {
115 patch: patches[0].clone(),
116 count: 1,
117 });
118 }
119
120 let mut composed_touch = patches[0].touch_set.clone();
121 let mut composed_path = patches[0].target_path.clone();
122 let mut composed_payload = patches[0].payload.clone();
123 let mut composed_op = patches[0].operation_type.clone();
124
125 for p in &patches[1..] {
126 for addr in p.touch_set.iter() {
127 composed_touch.insert(addr.clone());
128 }
129 if !p.payload.is_empty() {
130 composed_payload = p.payload.clone();
131 }
132 if p.target_path.is_some() {
133 composed_path = p.target_path.clone();
134 }
135 composed_op = match (&composed_op, &p.operation_type) {
136 (_, OperationType::Delete) => OperationType::Delete,
137 (_, OperationType::Move) => OperationType::Move,
138 (_, OperationType::Create) => {
139 if composed_op == OperationType::Create {
140 OperationType::Create
141 } else {
142 OperationType::Modify
143 }
144 }
145 (OperationType::Create, OperationType::Modify) => OperationType::Create,
146 _ => OperationType::Modify,
147 };
148 }
149
150 let composed_patch = Patch::new(
151 composed_op,
152 composed_touch,
153 composed_path,
154 composed_payload,
155 patches[0].parent_ids.clone(),
156 author.to_string(),
157 message.to_string(),
158 );
159
160 Ok(ComposeResult {
161 patch: composed_patch,
162 count: patches.len(),
163 })
164}
165
166#[derive(Debug, thiserror::Error)]
167pub enum ComposeError {
168 #[error("patches do not form a chain: {p2_id} does not have {p1_id} as ancestor")]
169 NotAncestor { p1_id: String, p2_id: String },
170 #[error("cannot compose an empty patch chain")]
171 EmptyChain,
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use crate::patch::types::{PatchId, TouchSet};
178 use suture_common::Hash;
179
180 fn make_patch(
181 op: OperationType,
182 touch: &[&str],
183 path: Option<&str>,
184 payload: &[u8],
185 parents: &[PatchId],
186 author: &str,
187 message: &str,
188 ) -> Patch {
189 Patch::new(
190 op,
191 TouchSet::from_addrs(touch.iter().copied()),
192 path.map(|s| s.to_string()),
193 payload.to_vec(),
194 parents.to_vec(),
195 author.to_string(),
196 message.to_string(),
197 )
198 }
199
200 #[test]
201 fn test_compose_linear_chain() {
202 let root = Hash::from_data(b"root");
203 let p1 = make_patch(
204 OperationType::Modify,
205 &["file_a"],
206 Some("file_a"),
207 b"content_a",
208 &[root],
209 "alice",
210 "edit file_a",
211 );
212 let p2 = make_patch(
213 OperationType::Modify,
214 &["file_b"],
215 Some("file_b"),
216 b"content_b",
217 &[p1.id],
218 "alice",
219 "edit file_b",
220 );
221
222 let result = compose(&p1, &p2, "alice", "composed").unwrap();
223 assert_eq!(result.count, 2);
224 assert_eq!(result.patch.parent_ids, vec![root]);
225 assert!(result.patch.touch_set.contains("file_a"));
226 assert!(result.patch.touch_set.contains("file_b"));
227 }
228
229 #[test]
230 fn test_compose_disjoint_touch_sets() {
231 let root = Hash::from_data(b"root");
232 let p1 = make_patch(
233 OperationType::Modify,
234 &["alpha"],
235 Some("alpha"),
236 b"aaa",
237 &[root],
238 "bob",
239 "change alpha",
240 );
241 let p2 = make_patch(
242 OperationType::Modify,
243 &["beta"],
244 Some("beta"),
245 b"bbb",
246 &[p1.id],
247 "bob",
248 "change beta",
249 );
250
251 let result = compose(&p1, &p2, "bob", "merge disjoint").unwrap();
252 assert_eq!(result.patch.touch_set.len(), 2);
253 assert!(result.patch.touch_set.contains("alpha"));
254 assert!(result.patch.touch_set.contains("beta"));
255 }
256
257 #[test]
258 fn test_compose_overlapping_touch_sets() {
259 let root = Hash::from_data(b"root");
260 let p1 = make_patch(
261 OperationType::Modify,
262 &["shared"],
263 Some("shared"),
264 b"v1",
265 &[root],
266 "carol",
267 "first edit",
268 );
269 let p2 = make_patch(
270 OperationType::Modify,
271 &["shared"],
272 Some("shared"),
273 b"v2",
274 &[p1.id],
275 "carol",
276 "second edit",
277 );
278
279 let result = compose(&p1, &p2, "carol", "merge overlap").unwrap();
280 assert_eq!(result.patch.touch_set.len(), 1);
282 assert!(result.patch.touch_set.contains("shared"));
283 assert_eq!(result.patch.payload, b"v2".to_vec());
285 }
286
287 #[test]
288 fn test_compose_not_ancestor_error() {
289 let root = Hash::from_data(b"root");
290 let p1 = make_patch(
291 OperationType::Modify,
292 &["a"],
293 Some("a"),
294 b"aaa",
295 &[root],
296 "dave",
297 "p1",
298 );
299 let p2 = make_patch(
301 OperationType::Modify,
302 &["b"],
303 Some("b"),
304 b"bbb",
305 &[root],
306 "dave",
307 "p2",
308 );
309
310 let err = compose(&p1, &p2, "dave", "fail").unwrap_err();
311 let msg = err.to_string();
312 assert!(msg.contains("does not have"), "unexpected error: {msg}");
313 }
314
315 #[test]
316 fn test_compose_empty_chain_error() {
317 let err = compose_chain(&[], "alice", "empty").unwrap_err();
318 assert!(matches!(err, ComposeError::EmptyChain));
319 }
320
321 #[test]
322 fn test_compose_single_patch() {
323 let root = Hash::from_data(b"root");
324 let p1 = make_patch(
325 OperationType::Modify,
326 &["solo"],
327 Some("solo"),
328 b"data",
329 &[root],
330 "eve",
331 "solo commit",
332 );
333
334 let result = compose_chain(&[p1.clone()], "eve", "noop").unwrap();
335 assert_eq!(result.count, 1);
336 assert_eq!(result.patch.id, p1.id);
337 }
338
339 #[test]
340 fn test_compose_chain_multiple() {
341 let root = Hash::from_data(b"root");
342 let p1 = make_patch(
343 OperationType::Modify,
344 &["x"],
345 Some("x"),
346 b"x1",
347 &[root],
348 "frank",
349 "first",
350 );
351 let p2 = make_patch(
352 OperationType::Modify,
353 &["y"],
354 Some("y"),
355 b"y1",
356 &[p1.id],
357 "frank",
358 "second",
359 );
360 let p3 = make_patch(
361 OperationType::Modify,
362 &["z"],
363 Some("z"),
364 b"z1",
365 &[p2.id],
366 "frank",
367 "third",
368 );
369
370 let result = compose_chain(&[p1, p2, p3], "frank", "all three").unwrap();
371 assert_eq!(result.count, 3);
372 assert_eq!(result.patch.parent_ids, vec![root]);
373 assert!(result.patch.touch_set.contains("x"));
374 assert!(result.patch.touch_set.contains("y"));
375 assert!(result.patch.touch_set.contains("z"));
376 }
377
378 #[test]
379 fn test_compose_preserves_union_touch_set() {
380 let root = Hash::from_data(b"root");
381 let p1 = make_patch(
382 OperationType::Modify,
383 &["a", "b", "c"],
384 Some("a"),
385 b"data",
386 &[root],
387 "grace",
388 "batch 1",
389 );
390 let p2 = make_patch(
391 OperationType::Modify,
392 &["c", "d", "e"],
393 Some("d"),
394 b"data2",
395 &[p1.id],
396 "grace",
397 "batch 2",
398 );
399
400 let result = compose(&p1, &p2, "grace", "union test").unwrap();
401
402 assert!(result.patch.touch_set.contains("a"));
404 assert!(result.patch.touch_set.contains("b"));
405 assert!(result.patch.touch_set.contains("c"));
406 assert!(result.patch.touch_set.contains("d"));
407 assert!(result.patch.touch_set.contains("e"));
408 assert_eq!(result.patch.touch_set.len(), 5);
409 }
410}