just playing with tangled
1// Copyright 2020 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![allow(missing_docs)]
16
17use std::borrow::Borrow;
18use std::hash::Hash;
19use std::io::Write;
20use std::sync::Arc;
21
22use itertools::Itertools;
23
24use crate::backend::{BackendError, BackendResult, FileId, ObjectId, TreeId, TreeValue};
25use crate::content_hash::ContentHash;
26use crate::diff::{find_line_ranges, Diff, DiffHunk};
27use crate::files::{ContentHunk, MergeResult};
28use crate::merge::trivial_merge;
29use crate::repo_path::RepoPath;
30use crate::store::Store;
31use crate::tree::Tree;
32use crate::{backend, files};
33
34const CONFLICT_START_LINE: &[u8] = b"<<<<<<<\n";
35const CONFLICT_END_LINE: &[u8] = b">>>>>>>\n";
36const CONFLICT_DIFF_LINE: &[u8] = b"%%%%%%%\n";
37const CONFLICT_MINUS_LINE: &[u8] = b"-------\n";
38const CONFLICT_PLUS_LINE: &[u8] = b"+++++++\n";
39
40/// A generic representation of conflicting values.
41///
42/// There is exactly one more `adds()` than `removes()`. When interpreted as a
43/// series of diffs, the conflict's (i+1)-st add is matched with the i-th
44/// remove. The zeroth add is considered a diff from the non-existent state.
45#[derive(PartialEq, Eq, Hash, Clone, Debug)]
46pub struct Conflict<T> {
47 removes: Vec<T>,
48 adds: Vec<T>,
49}
50
51impl<T> Conflict<T> {
52 pub fn new(removes: Vec<T>, adds: Vec<T>) -> Self {
53 assert_eq!(adds.len(), removes.len() + 1);
54 Conflict { removes, adds }
55 }
56
57 /// Creates a `Conflict` with a single resolved value.
58 pub fn resolved(value: T) -> Self {
59 Conflict::new(vec![], vec![value])
60 }
61
62 /// Create a `Conflict` from a `removes` and `adds`, padding with `None` to
63 /// make sure that there is exactly one more `adds` than `removes`.
64 pub fn from_legacy_form(
65 removes: impl IntoIterator<Item = T>,
66 adds: impl IntoIterator<Item = T>,
67 ) -> Conflict<Option<T>> {
68 let mut removes = removes.into_iter().map(Some).collect_vec();
69 let mut adds = adds.into_iter().map(Some).collect_vec();
70 while removes.len() + 1 < adds.len() {
71 removes.push(None);
72 }
73 while adds.len() < removes.len() + 1 {
74 adds.push(None);
75 }
76 Conflict::new(removes, adds)
77 }
78
79 /// Returns the removes and adds as a pair.
80 pub fn take(self) -> (Vec<T>, Vec<T>) {
81 (self.removes, self.adds)
82 }
83
84 pub fn removes(&self) -> &[T] {
85 &self.removes
86 }
87
88 pub fn adds(&self) -> &[T] {
89 &self.adds
90 }
91
92 /// Whether this conflict is resolved. Does not resolve trivial conflicts.
93 pub fn is_resolved(&self) -> bool {
94 self.removes.is_empty()
95 }
96
97 /// Returns the resolved value, if this conflict is resolved. Does not
98 /// resolve trivial conflicts.
99 pub fn as_resolved(&self) -> Option<&T> {
100 if let [value] = &self.adds[..] {
101 Some(value)
102 } else {
103 None
104 }
105 }
106
107 /// Simplify the conflict by joining diffs like A->B and B->C into A->C.
108 /// Also drops trivial diffs like A->A.
109 pub fn simplify(mut self) -> Self
110 where
111 T: PartialEq,
112 {
113 let mut add_index = 0;
114 while add_index < self.adds.len() {
115 let add = &self.adds[add_index];
116 if let Some(remove_index) = self.removes.iter().position(|remove| remove == add) {
117 // Move the value to the `add_index-1`th diff, then delete the `remove_index`th
118 // diff.
119 self.adds.swap(remove_index + 1, add_index);
120 self.removes.remove(remove_index);
121 self.adds.remove(remove_index + 1);
122 } else {
123 add_index += 1;
124 }
125 }
126 self
127 }
128
129 pub fn resolve_trivial(&self) -> Option<&T>
130 where
131 T: Eq + Hash,
132 {
133 trivial_merge(&self.removes, &self.adds)
134 }
135
136 /// Creates a new conflict by applying `f` to each remove and add.
137 pub fn map<'a, U>(&'a self, mut f: impl FnMut(&'a T) -> U) -> Conflict<U> {
138 self.maybe_map(|term| Some(f(term))).unwrap()
139 }
140
141 /// Creates a new conflict by applying `f` to each remove and add, returning
142 /// `None if `f` returns `None` for any of them.
143 pub fn maybe_map<'a, U>(
144 &'a self,
145 mut f: impl FnMut(&'a T) -> Option<U>,
146 ) -> Option<Conflict<U>> {
147 let removes = self.removes.iter().map(&mut f).collect::<Option<_>>()?;
148 let adds = self.adds.iter().map(&mut f).collect::<Option<_>>()?;
149 Some(Conflict { removes, adds })
150 }
151
152 /// Creates a new conflict by applying `f` to each remove and add, returning
153 /// `Err if `f` returns `Err` for any of them.
154 pub fn try_map<'a, U, E>(
155 &'a self,
156 mut f: impl FnMut(&'a T) -> Result<U, E>,
157 ) -> Result<Conflict<U>, E> {
158 let removes = self.removes.iter().map(&mut f).try_collect()?;
159 let adds = self.adds.iter().map(&mut f).try_collect()?;
160 Ok(Conflict { removes, adds })
161 }
162}
163
164impl<T> Conflict<Option<T>> {
165 /// Creates lists of `removes` and `adds` from a `Conflict` by dropping
166 /// `None` values. Note that the conversion is lossy: the order of `None`
167 /// values is not preserved when converting back to a `Conflict`.
168 pub fn into_legacy_form(self) -> (Vec<T>, Vec<T>) {
169 (
170 self.removes.into_iter().flatten().collect(),
171 self.adds.into_iter().flatten().collect(),
172 )
173 }
174}
175
176impl<T> Conflict<Conflict<T>> {
177 /// Flattens a nested conflict into a regular conflict.
178 ///
179 /// Let's say we have a 3-way merge of 3-way merges like this:
180 ///
181 /// 4 5 7 8
182 /// 3 6
183 /// 1 2
184 /// 0
185 ///
186 /// Flattening that results in this 9-way conflict:
187 ///
188 /// 4 5 0 7 8
189 /// 3 2 1 6
190 pub fn flatten(mut self) -> Conflict<T> {
191 self.removes.reverse();
192 self.adds.reverse();
193 let mut result = self.adds.pop().unwrap();
194 while let Some(mut remove) = self.removes.pop() {
195 // Add removes reversed, and with the first element moved last, so we preserve
196 // the diffs
197 let first_add = remove.adds.remove(0);
198 result.removes.extend(remove.adds);
199 result.removes.push(first_add);
200 result.adds.extend(remove.removes);
201 let add = self.adds.pop().unwrap();
202 result.removes.extend(add.removes);
203 result.adds.extend(add.adds);
204 }
205 assert!(self.adds.is_empty());
206 result
207 }
208}
209
210impl<T: ContentHash> ContentHash for Conflict<T> {
211 fn hash(&self, state: &mut impl digest::Update) {
212 self.removes().hash(state);
213 self.adds().hash(state);
214 }
215}
216
217impl Conflict<TreeId> {
218 // Creates a resolved conflict for a legacy tree id (same as
219 // `Conflict::resolved()`).
220 // TODO(#1624): delete when all callers have been updated to support tree-level
221 // conflicts
222 pub fn from_legacy_tree_id(value: TreeId) -> Self {
223 Conflict {
224 removes: vec![],
225 adds: vec![value],
226 }
227 }
228
229 // TODO(#1624): delete when all callers have been updated to support tree-level
230 // conflicts
231 pub fn as_legacy_tree_id(&self) -> &TreeId {
232 self.as_resolved().unwrap()
233 }
234}
235
236impl Conflict<Option<TreeValue>> {
237 /// Create a `Conflict` from a `backend::Conflict`, padding with `None` to
238 /// make sure that there is exactly one more `adds()` than `removes()`.
239 pub fn from_backend_conflict(conflict: backend::Conflict) -> Self {
240 let removes = conflict.removes.into_iter().map(|term| term.value);
241 let adds = conflict.adds.into_iter().map(|term| term.value);
242 Conflict::from_legacy_form(removes, adds)
243 }
244
245 /// Creates a `backend::Conflict` from a `Conflict` by dropping `None`
246 /// values. Note that the conversion is lossy: the order of `None` values is
247 /// not preserved when converting back to a `Conflict`.
248 pub fn into_backend_conflict(self) -> backend::Conflict {
249 let (removes, adds) = self.into_legacy_form();
250 let removes = removes
251 .into_iter()
252 .map(|value| backend::ConflictTerm { value })
253 .collect();
254 let adds = adds
255 .into_iter()
256 .map(|value| backend::ConflictTerm { value })
257 .collect();
258 backend::Conflict { removes, adds }
259 }
260
261 pub fn materialize(
262 &self,
263 store: &Store,
264 path: &RepoPath,
265 output: &mut dyn Write,
266 ) -> std::io::Result<()> {
267 if let Some(file_conflict) = self.to_file_conflict() {
268 let content = file_conflict.extract_as_single_hunk(store, path);
269 materialize_merge_result(&content, output)
270 } else {
271 // Unless all terms are regular files, we can't do much better than to try to
272 // describe the conflict.
273 self.describe(output)
274 }
275 }
276
277 pub fn to_file_conflict(&self) -> Option<Conflict<Option<FileId>>> {
278 self.maybe_map(|term| match term {
279 None => Some(None),
280 Some(TreeValue::File {
281 id,
282 executable: false,
283 }) => Some(Some(id.clone())),
284 _ => None,
285 })
286 }
287
288 /// Give a summary description of the conflict's "removes" and "adds"
289 pub fn describe(&self, file: &mut dyn Write) -> std::io::Result<()> {
290 file.write_all(b"Conflict:\n")?;
291 for term in self.removes().iter().flatten() {
292 file.write_all(format!(" Removing {}\n", describe_conflict_term(term)).as_bytes())?;
293 }
294 for term in self.adds().iter().flatten() {
295 file.write_all(format!(" Adding {}\n", describe_conflict_term(term)).as_bytes())?;
296 }
297 Ok(())
298 }
299
300 /// Returns `None` if there are no conflict markers in `content`.
301 pub fn update_from_content(
302 &self,
303 store: &Store,
304 path: &RepoPath,
305 content: &[u8],
306 ) -> BackendResult<Option<Conflict<Option<TreeValue>>>> {
307 // TODO: Check that the conflict only involves files and convert it to a
308 // `Conflict<Option<FileId>>` so we can remove the wildcard pattern in the loops
309 // further down.
310
311 // First check if the new content is unchanged compared to the old content. If
312 // it is, we don't need parse the content or write any new objects to the
313 // store. This is also a way of making sure that unchanged tree/file
314 // conflicts (for example) are not converted to regular files in the working
315 // copy.
316 let mut old_content = Vec::with_capacity(content.len());
317 self.materialize(store, path, &mut old_content).unwrap();
318 if content == old_content {
319 return Ok(Some(self.clone()));
320 }
321
322 let mut removed_content = vec![vec![]; self.removes().len()];
323 let mut added_content = vec![vec![]; self.adds().len()];
324 let Some(hunks) = parse_conflict(content, self.removes().len(), self.adds().len()) else {
325 // Either there are no self markers of they don't have the expected arity
326 return Ok(None);
327 };
328 for hunk in hunks {
329 if let Some(slice) = hunk.as_resolved() {
330 for buf in &mut removed_content {
331 buf.extend_from_slice(&slice.0);
332 }
333 for buf in &mut added_content {
334 buf.extend_from_slice(&slice.0);
335 }
336 } else {
337 let (removes, adds) = hunk.take();
338 for (i, buf) in removes.into_iter().enumerate() {
339 removed_content[i].extend(buf.0);
340 }
341 for (i, buf) in adds.into_iter().enumerate() {
342 added_content[i].extend(buf.0);
343 }
344 }
345 }
346 // Now write the new files contents we found by parsing the file
347 // with conflict markers. Update the Conflict object with the new
348 // FileIds.
349 let mut new_removes = vec![];
350 for (i, buf) in removed_content.iter().enumerate() {
351 match &self.removes()[i] {
352 Some(TreeValue::File { id: _, executable }) => {
353 let file_id = store.write_file(path, &mut buf.as_slice())?;
354 let new_value = TreeValue::File {
355 id: file_id,
356 executable: *executable,
357 };
358 new_removes.push(Some(new_value));
359 }
360 None if buf.is_empty() => {
361 // The missing side of a conflict is still represented by
362 // the empty string we materialized it as
363 new_removes.push(None);
364 }
365 _ => {
366 // The user edited a non-file side. This should never happen. We consider the
367 // conflict resolved for now.
368 return Ok(None);
369 }
370 }
371 }
372 let mut new_adds = vec![];
373 for (i, buf) in added_content.iter().enumerate() {
374 match &self.adds()[i] {
375 Some(TreeValue::File { id: _, executable }) => {
376 let file_id = store.write_file(path, &mut buf.as_slice())?;
377 let new_value = TreeValue::File {
378 id: file_id,
379 executable: *executable,
380 };
381 new_adds.push(Some(new_value));
382 }
383 None if buf.is_empty() => {
384 // The missing side of a conflict is still represented by
385 // the empty string we materialized it as => nothing to do
386 new_adds.push(None);
387 }
388 _ => {
389 // The user edited a non-file side. This should never happen. We consider the
390 // conflict resolved for now.
391 return Ok(None);
392 }
393 }
394 }
395 Ok(Some(Conflict::new(new_removes, new_adds)))
396 }
397}
398
399impl Conflict<Option<FileId>> {
400 pub fn extract_as_single_hunk(&self, store: &Store, path: &RepoPath) -> Conflict<ContentHunk> {
401 self.map(|term| get_file_contents(store, path, term))
402 }
403}
404
405impl<T> Conflict<Option<T>>
406where
407 T: Borrow<TreeValue>,
408{
409 /// If every non-`None` term of a `Conflict<Option<TreeValue>>`
410 /// is a `TreeValue::Tree`, this converts it to
411 /// a `Conflict<Tree>`, with empty trees instead of
412 /// any `None` terms. Otherwise, returns `None`.
413 pub fn to_tree_conflict(
414 &self,
415 store: &Arc<Store>,
416 dir: &RepoPath,
417 ) -> Result<Option<Conflict<Tree>>, BackendError> {
418 let tree_id_conflict = self.maybe_map(|term| match term {
419 None => Some(None),
420 Some(value) => {
421 if let TreeValue::Tree(id) = value.borrow() {
422 Some(Some(id))
423 } else {
424 None
425 }
426 }
427 });
428 if let Some(tree_id_conflict) = tree_id_conflict {
429 let get_tree = |id: &Option<&TreeId>| -> Result<Tree, BackendError> {
430 if let Some(id) = id {
431 store.get_tree(dir, id)
432 } else {
433 Ok(Tree::null(store.clone(), dir.clone()))
434 }
435 };
436 Ok(Some(tree_id_conflict.try_map(get_tree)?))
437 } else {
438 Ok(None)
439 }
440 }
441}
442
443fn describe_conflict_term(value: &TreeValue) -> String {
444 match value {
445 TreeValue::File {
446 id,
447 executable: false,
448 } => {
449 format!("file with id {}", id.hex())
450 }
451 TreeValue::File {
452 id,
453 executable: true,
454 } => {
455 format!("executable file with id {}", id.hex())
456 }
457 TreeValue::Symlink(id) => {
458 format!("symlink with id {}", id.hex())
459 }
460 TreeValue::Tree(id) => {
461 format!("tree with id {}", id.hex())
462 }
463 TreeValue::GitSubmodule(id) => {
464 format!("Git submodule with id {}", id.hex())
465 }
466 TreeValue::Conflict(id) => {
467 format!("Conflict with id {}", id.hex())
468 }
469 }
470}
471
472fn get_file_contents(store: &Store, path: &RepoPath, term: &Option<FileId>) -> ContentHunk {
473 match term {
474 Some(id) => {
475 let mut content = vec![];
476 store
477 .read_file(path, id)
478 .unwrap()
479 .read_to_end(&mut content)
480 .unwrap();
481 ContentHunk(content)
482 }
483 // If the conflict had removed the file on one side, we pretend that the file
484 // was empty there.
485 None => ContentHunk(vec![]),
486 }
487}
488
489fn write_diff_hunks(hunks: &[DiffHunk], file: &mut dyn Write) -> std::io::Result<()> {
490 for hunk in hunks {
491 match hunk {
492 DiffHunk::Matching(content) => {
493 for line in content.split_inclusive(|b| *b == b'\n') {
494 file.write_all(b" ")?;
495 file.write_all(line)?;
496 }
497 }
498 DiffHunk::Different(content) => {
499 for line in content[0].split_inclusive(|b| *b == b'\n') {
500 file.write_all(b"-")?;
501 file.write_all(line)?;
502 }
503 for line in content[1].split_inclusive(|b| *b == b'\n') {
504 file.write_all(b"+")?;
505 file.write_all(line)?;
506 }
507 }
508 }
509 }
510 Ok(())
511}
512
513pub fn materialize_merge_result(
514 single_hunk: &Conflict<ContentHunk>,
515 output: &mut dyn Write,
516) -> std::io::Result<()> {
517 let removed_slices = single_hunk
518 .removes
519 .iter()
520 .map(|hunk| hunk.0.as_slice())
521 .collect_vec();
522 let added_slices = single_hunk
523 .adds
524 .iter()
525 .map(|hunk| hunk.0.as_slice())
526 .collect_vec();
527 let merge_result = files::merge(&removed_slices, &added_slices);
528 match merge_result {
529 MergeResult::Resolved(content) => {
530 output.write_all(&content.0)?;
531 }
532 MergeResult::Conflict(hunks) => {
533 for hunk in hunks {
534 if let Some(content) = hunk.as_resolved() {
535 output.write_all(&content.0)?;
536 } else {
537 output.write_all(CONFLICT_START_LINE)?;
538 let mut add_index = 0;
539 for left in hunk.removes() {
540 let right1 = if let Some(right1) = hunk.adds().get(add_index) {
541 right1
542 } else {
543 // If we have no more positive terms, emit the remaining negative
544 // terms as snapshots.
545 output.write_all(CONFLICT_MINUS_LINE)?;
546 output.write_all(&left.0)?;
547 continue;
548 };
549 let diff1 = Diff::for_tokenizer(&[&left.0, &right1.0], &find_line_ranges)
550 .hunks()
551 .collect_vec();
552 // Check if the diff against the next positive term is better. Since
553 // we want to preserve the order of the terms, we don't match against
554 // any later positive terms.
555 if let Some(right2) = hunk.adds().get(add_index + 1) {
556 let diff2 =
557 Diff::for_tokenizer(&[&left.0, &right2.0], &find_line_ranges)
558 .hunks()
559 .collect_vec();
560 if diff_size(&diff2) < diff_size(&diff1) {
561 // If the next positive term is a better match, emit
562 // the current positive term as a snapshot and the next
563 // positive term as a diff.
564 output.write_all(CONFLICT_PLUS_LINE)?;
565 output.write_all(&right1.0)?;
566 output.write_all(CONFLICT_DIFF_LINE)?;
567 write_diff_hunks(&diff2, output)?;
568 add_index += 2;
569 continue;
570 }
571 }
572
573 output.write_all(CONFLICT_DIFF_LINE)?;
574 write_diff_hunks(&diff1, output)?;
575 add_index += 1;
576 }
577
578 // Emit the remaining positive terms as snapshots.
579 for slice in &hunk.adds()[add_index..] {
580 output.write_all(CONFLICT_PLUS_LINE)?;
581 output.write_all(&slice.0)?;
582 }
583 output.write_all(CONFLICT_END_LINE)?;
584 }
585 }
586 }
587 }
588 Ok(())
589}
590
591fn diff_size(hunks: &[DiffHunk]) -> usize {
592 hunks
593 .iter()
594 .map(|hunk| match hunk {
595 DiffHunk::Matching(_) => 0,
596 DiffHunk::Different(slices) => slices.iter().map(|slice| slice.len()).sum(),
597 })
598 .sum()
599}
600
601/// Parses conflict markers from a slice. Returns None if there were no valid
602/// conflict markers. The caller has to provide the expected number of removed
603/// and added inputs to the conflicts. Conflict markers that are otherwise valid
604/// will be considered invalid if they don't have the expected arity.
605// TODO: "parse" is not usually the opposite of "materialize", so maybe we
606// should rename them to "serialize" and "deserialize"?
607pub fn parse_conflict(
608 input: &[u8],
609 num_removes: usize,
610 num_adds: usize,
611) -> Option<Vec<Conflict<ContentHunk>>> {
612 if input.is_empty() {
613 return None;
614 }
615 let mut hunks = vec![];
616 let mut pos = 0;
617 let mut resolved_start = 0;
618 let mut conflict_start = None;
619 for line in input.split_inclusive(|b| *b == b'\n') {
620 if line == CONFLICT_START_LINE {
621 conflict_start = Some(pos);
622 } else if conflict_start.is_some() && line == CONFLICT_END_LINE {
623 let conflict_body = &input[conflict_start.unwrap() + CONFLICT_START_LINE.len()..pos];
624 let hunk = parse_conflict_hunk(conflict_body);
625 if hunk.removes().len() == num_removes && hunk.adds().len() == num_adds {
626 let resolved_slice = &input[resolved_start..conflict_start.unwrap()];
627 if !resolved_slice.is_empty() {
628 hunks.push(Conflict::resolved(ContentHunk(resolved_slice.to_vec())));
629 }
630 hunks.push(hunk);
631 resolved_start = pos + line.len();
632 }
633 conflict_start = None;
634 }
635 pos += line.len();
636 }
637
638 if hunks.is_empty() {
639 None
640 } else {
641 if resolved_start < input.len() {
642 hunks.push(Conflict::resolved(ContentHunk(
643 input[resolved_start..].to_vec(),
644 )));
645 }
646 Some(hunks)
647 }
648}
649
650fn parse_conflict_hunk(input: &[u8]) -> Conflict<ContentHunk> {
651 enum State {
652 Diff,
653 Minus,
654 Plus,
655 Unknown,
656 }
657 let mut state = State::Unknown;
658 let mut removes = vec![];
659 let mut adds = vec![];
660 for line in input.split_inclusive(|b| *b == b'\n') {
661 match line {
662 CONFLICT_DIFF_LINE => {
663 state = State::Diff;
664 removes.push(ContentHunk(vec![]));
665 adds.push(ContentHunk(vec![]));
666 continue;
667 }
668 CONFLICT_MINUS_LINE => {
669 state = State::Minus;
670 removes.push(ContentHunk(vec![]));
671 continue;
672 }
673 CONFLICT_PLUS_LINE => {
674 state = State::Plus;
675 adds.push(ContentHunk(vec![]));
676 continue;
677 }
678 _ => {}
679 };
680 match state {
681 State::Diff => {
682 if let Some(rest) = line.strip_prefix(b"-") {
683 removes.last_mut().unwrap().0.extend_from_slice(rest);
684 } else if let Some(rest) = line.strip_prefix(b"+") {
685 adds.last_mut().unwrap().0.extend_from_slice(rest);
686 } else if let Some(rest) = line.strip_prefix(b" ") {
687 removes.last_mut().unwrap().0.extend_from_slice(rest);
688 adds.last_mut().unwrap().0.extend_from_slice(rest);
689 } else {
690 // Doesn't look like a conflict
691 return Conflict::resolved(ContentHunk(vec![]));
692 }
693 }
694 State::Minus => {
695 removes.last_mut().unwrap().0.extend_from_slice(line);
696 }
697 State::Plus => {
698 adds.last_mut().unwrap().0.extend_from_slice(line);
699 }
700 State::Unknown => {
701 // Doesn't look like a conflict
702 return Conflict::resolved(ContentHunk(vec![]));
703 }
704 }
705 }
706
707 Conflict::new(removes, adds)
708}
709
710#[cfg(test)]
711mod tests {
712 use super::*;
713
714 fn c<T: Clone>(removes: &[T], adds: &[T]) -> Conflict<T> {
715 Conflict::new(removes.to_vec(), adds.to_vec())
716 }
717
718 #[test]
719 fn test_legacy_form_conversion() {
720 fn test_equivalent<T>(legacy_form: (Vec<T>, Vec<T>), conflict: Conflict<Option<T>>)
721 where
722 T: Clone + PartialEq + std::fmt::Debug,
723 {
724 assert_eq!(conflict.clone().into_legacy_form(), legacy_form);
725 assert_eq!(
726 Conflict::from_legacy_form(legacy_form.0, legacy_form.1),
727 conflict
728 );
729 }
730 // Non-conflict
731 test_equivalent((vec![], vec![0]), Conflict::new(vec![], vec![Some(0)]));
732 // Regular 3-way conflict
733 test_equivalent(
734 (vec![0], vec![1, 2]),
735 Conflict::new(vec![Some(0)], vec![Some(1), Some(2)]),
736 );
737 // Modify/delete conflict
738 test_equivalent(
739 (vec![0], vec![1]),
740 Conflict::new(vec![Some(0)], vec![Some(1), None]),
741 );
742 // Add/add conflict
743 test_equivalent(
744 (vec![], vec![0, 1]),
745 Conflict::new(vec![None], vec![Some(0), Some(1)]),
746 );
747 // 5-way conflict
748 test_equivalent(
749 (vec![0, 1], vec![2, 3, 4]),
750 Conflict::new(vec![Some(0), Some(1)], vec![Some(2), Some(3), Some(4)]),
751 );
752 // 5-way delete/delete conflict
753 test_equivalent(
754 (vec![0, 1], vec![]),
755 Conflict::new(vec![Some(0), Some(1)], vec![None, None, None]),
756 );
757 }
758
759 #[test]
760 fn test_as_resolved() {
761 assert_eq!(Conflict::new(vec![], vec![0]).as_resolved(), Some(&0));
762 // Even a trivially resolvable conflict is not resolved
763 assert_eq!(Conflict::new(vec![0], vec![0, 1]).as_resolved(), None);
764 }
765
766 #[test]
767 fn test_simplify() {
768 // 1-way "conflict"
769 assert_eq!(c(&[], &[0]).simplify(), c(&[], &[0]));
770 // 3-way conflict
771 assert_eq!(c(&[0], &[0, 0]).simplify(), c(&[], &[0]));
772 assert_eq!(c(&[0], &[0, 1]).simplify(), c(&[], &[1]));
773 assert_eq!(c(&[0], &[1, 0]).simplify(), c(&[], &[1]));
774 assert_eq!(c(&[0], &[1, 1]).simplify(), c(&[0], &[1, 1]));
775 assert_eq!(c(&[0], &[1, 2]).simplify(), c(&[0], &[1, 2]));
776 // 5-way conflict
777 assert_eq!(c(&[0, 0], &[0, 0, 0]).simplify(), c(&[], &[0]));
778 assert_eq!(c(&[0, 0], &[0, 0, 1]).simplify(), c(&[], &[1]));
779 assert_eq!(c(&[0, 0], &[0, 1, 0]).simplify(), c(&[], &[1]));
780 assert_eq!(c(&[0, 0], &[0, 1, 1]).simplify(), c(&[0], &[1, 1]));
781 assert_eq!(c(&[0, 0], &[0, 1, 2]).simplify(), c(&[0], &[1, 2]));
782 assert_eq!(c(&[0, 0], &[1, 0, 0]).simplify(), c(&[], &[1]));
783 assert_eq!(c(&[0, 0], &[1, 0, 1]).simplify(), c(&[0], &[1, 1]));
784 assert_eq!(c(&[0, 0], &[1, 0, 2]).simplify(), c(&[0], &[1, 2]));
785 assert_eq!(c(&[0, 0], &[1, 1, 0]).simplify(), c(&[0], &[1, 1]));
786 assert_eq!(c(&[0, 0], &[1, 1, 1]).simplify(), c(&[0, 0], &[1, 1, 1]));
787 assert_eq!(c(&[0, 0], &[1, 1, 2]).simplify(), c(&[0, 0], &[1, 1, 2]));
788 assert_eq!(c(&[0, 0], &[1, 2, 0]).simplify(), c(&[0], &[1, 2]));
789 assert_eq!(c(&[0, 0], &[1, 2, 1]).simplify(), c(&[0, 0], &[1, 2, 1]));
790 assert_eq!(c(&[0, 0], &[1, 2, 2]).simplify(), c(&[0, 0], &[1, 2, 2]));
791 assert_eq!(c(&[0, 0], &[1, 2, 3]).simplify(), c(&[0, 0], &[1, 2, 3]));
792 assert_eq!(c(&[0, 1], &[0, 0, 0]).simplify(), c(&[1], &[0, 0]));
793 assert_eq!(c(&[0, 1], &[0, 0, 1]).simplify(), c(&[], &[0]));
794 assert_eq!(c(&[0, 1], &[0, 0, 2]).simplify(), c(&[1], &[0, 2]));
795 assert_eq!(c(&[0, 1], &[0, 1, 0]).simplify(), c(&[], &[0]));
796 assert_eq!(c(&[0, 1], &[0, 1, 1]).simplify(), c(&[], &[1]));
797 assert_eq!(c(&[0, 1], &[0, 1, 2]).simplify(), c(&[], &[2]));
798 assert_eq!(c(&[0, 1], &[0, 2, 0]).simplify(), c(&[1], &[2, 0]));
799 assert_eq!(c(&[0, 1], &[0, 2, 1]).simplify(), c(&[], &[2]));
800 assert_eq!(c(&[0, 1], &[0, 2, 2]).simplify(), c(&[1], &[2, 2]));
801 assert_eq!(c(&[0, 1], &[0, 2, 3]).simplify(), c(&[1], &[2, 3]));
802 assert_eq!(c(&[0, 1], &[1, 0, 0]).simplify(), c(&[], &[0]));
803 assert_eq!(c(&[0, 1], &[1, 0, 1]).simplify(), c(&[], &[1]));
804 assert_eq!(c(&[0, 1], &[1, 0, 2]).simplify(), c(&[], &[2]));
805 assert_eq!(c(&[0, 1], &[1, 1, 0]).simplify(), c(&[], &[1]));
806 assert_eq!(c(&[0, 1], &[1, 1, 1]).simplify(), c(&[0], &[1, 1]));
807 assert_eq!(c(&[0, 1], &[1, 1, 2]).simplify(), c(&[0], &[2, 1]));
808 assert_eq!(c(&[0, 1], &[1, 2, 0]).simplify(), c(&[], &[2]));
809 assert_eq!(c(&[0, 1], &[1, 2, 1]).simplify(), c(&[0], &[1, 2]));
810 assert_eq!(c(&[0, 1], &[1, 2, 2]).simplify(), c(&[0], &[2, 2]));
811 assert_eq!(c(&[0, 1], &[1, 2, 3]).simplify(), c(&[0], &[3, 2]));
812 assert_eq!(c(&[0, 1], &[2, 0, 0]).simplify(), c(&[1], &[2, 0]));
813 assert_eq!(c(&[0, 1], &[2, 0, 1]).simplify(), c(&[], &[2]));
814 assert_eq!(c(&[0, 1], &[2, 0, 2]).simplify(), c(&[1], &[2, 2]));
815 assert_eq!(c(&[0, 1], &[2, 0, 3]).simplify(), c(&[1], &[2, 3]));
816 assert_eq!(c(&[0, 1], &[2, 1, 0]).simplify(), c(&[], &[2]));
817 assert_eq!(c(&[0, 1], &[2, 1, 1]).simplify(), c(&[0], &[2, 1]));
818 assert_eq!(c(&[0, 1], &[2, 1, 2]).simplify(), c(&[0], &[2, 2]));
819 assert_eq!(c(&[0, 1], &[2, 1, 3]).simplify(), c(&[0], &[2, 3]));
820 assert_eq!(c(&[0, 1], &[2, 2, 0]).simplify(), c(&[1], &[2, 2]));
821 assert_eq!(c(&[0, 1], &[2, 2, 1]).simplify(), c(&[0], &[2, 2]));
822 assert_eq!(c(&[0, 1], &[2, 2, 2]).simplify(), c(&[0, 1], &[2, 2, 2]));
823 assert_eq!(c(&[0, 1], &[2, 2, 3]).simplify(), c(&[0, 1], &[2, 2, 3]));
824 assert_eq!(c(&[0, 1], &[2, 3, 0]).simplify(), c(&[1], &[2, 3]));
825 assert_eq!(c(&[0, 1], &[2, 3, 1]).simplify(), c(&[0], &[2, 3]));
826 assert_eq!(c(&[0, 1], &[2, 3, 2]).simplify(), c(&[0, 1], &[2, 3, 2]));
827 assert_eq!(c(&[0, 1], &[2, 3, 3]).simplify(), c(&[0, 1], &[2, 3, 3]));
828 assert_eq!(c(&[0, 1], &[2, 3, 4]).simplify(), c(&[0, 1], &[2, 3, 4]));
829 assert_eq!(
830 c(&[0, 1, 2], &[3, 4, 5, 0]).simplify(),
831 c(&[1, 2], &[3, 5, 4])
832 );
833 }
834
835 #[test]
836 fn test_conflict_invariants() {
837 fn check_invariants(removes: &[u32], adds: &[u32]) {
838 let conflict = Conflict::new(removes.to_vec(), adds.to_vec());
839 // `simplify()` is idempotent
840 assert_eq!(
841 conflict.clone().simplify().simplify(),
842 conflict.clone().simplify(),
843 "simplify() not idempotent for {conflict:?}"
844 );
845 // `resolve_trivial()` is unaffected by `simplify()`
846 assert_eq!(
847 conflict.clone().simplify().resolve_trivial(),
848 conflict.resolve_trivial(),
849 "simplify() changed result of resolve_trivial() for {conflict:?}"
850 );
851 }
852 // 1-way "conflict"
853 check_invariants(&[], &[0]);
854 for i in 0..=1 {
855 for j in 0..=i + 1 {
856 // 3-way conflict
857 check_invariants(&[0], &[i, j]);
858 for k in 0..=j + 1 {
859 for l in 0..=k + 1 {
860 // 5-way conflict
861 check_invariants(&[0, i], &[j, k, l]);
862 }
863 }
864 }
865 }
866 }
867
868 #[test]
869 fn test_map() {
870 fn increment(i: &i32) -> i32 {
871 i + 1
872 }
873 // 1-way conflict
874 assert_eq!(c(&[], &[1]).map(increment), c(&[], &[2]));
875 // 3-way conflict
876 assert_eq!(c(&[1], &[3, 5]).map(increment), c(&[2], &[4, 6]));
877 }
878
879 #[test]
880 fn test_maybe_map() {
881 fn sqrt(i: &i32) -> Option<i32> {
882 if *i >= 0 {
883 Some((*i as f64).sqrt() as i32)
884 } else {
885 None
886 }
887 }
888 // 1-way conflict
889 assert_eq!(c(&[], &[1]).maybe_map(sqrt), Some(c(&[], &[1])));
890 assert_eq!(c(&[], &[-1]).maybe_map(sqrt), None);
891 // 3-way conflict
892 assert_eq!(c(&[1], &[4, 9]).maybe_map(sqrt), Some(c(&[1], &[2, 3])));
893 assert_eq!(c(&[-1], &[4, 9]).maybe_map(sqrt), None);
894 assert_eq!(c(&[1], &[-4, 9]).maybe_map(sqrt), None);
895 }
896
897 #[test]
898 fn test_try_map() {
899 fn sqrt(i: &i32) -> Result<i32, ()> {
900 if *i >= 0 {
901 Ok((*i as f64).sqrt() as i32)
902 } else {
903 Err(())
904 }
905 }
906 // 1-way conflict
907 assert_eq!(c(&[], &[1]).try_map(sqrt), Ok(c(&[], &[1])));
908 assert_eq!(c(&[], &[-1]).try_map(sqrt), Err(()));
909 // 3-way conflict
910 assert_eq!(c(&[1], &[4, 9]).try_map(sqrt), Ok(c(&[1], &[2, 3])));
911 assert_eq!(c(&[-1], &[4, 9]).try_map(sqrt), Err(()));
912 assert_eq!(c(&[1], &[-4, 9]).try_map(sqrt), Err(()));
913 }
914
915 #[test]
916 fn test_flatten() {
917 // 1-way conflict of 1-way conflict
918 assert_eq!(c(&[], &[c(&[], &[0])]).flatten(), c(&[], &[0]));
919 // 1-way conflict of 3-way conflict
920 assert_eq!(c(&[], &[c(&[0], &[1, 2])]).flatten(), c(&[0], &[1, 2]));
921 // 3-way conflict of 1-way conflicts
922 assert_eq!(
923 c(&[c(&[], &[0])], &[c(&[], &[1]), c(&[], &[2])]).flatten(),
924 c(&[0], &[1, 2])
925 );
926 // 3-way conflict of 3-way conflicts
927 assert_eq!(
928 c(&[c(&[0], &[1, 2])], &[c(&[3], &[4, 5]), c(&[6], &[7, 8])]).flatten(),
929 c(&[3, 2, 1, 6], &[4, 5, 0, 7, 8])
930 );
931 }
932}