1use std::collections::BTreeMap;
12use std::path::PathBuf;
13
14use serde::{Deserialize, Serialize};
15
16use crate::crypto::PublicKey;
17use crate::linked_data::{BlockEncoded, DagCborCodec, Link};
18
19use super::conflict::{
20 operations_conflict, Conflict, ConflictResolver, MergeResult, Resolution, ResolvedConflict,
21};
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub enum OpType {
26 Add,
28 Remove,
30 Mkdir,
32 Mv {
34 from: PathBuf,
36 },
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
45pub struct OpId {
46 pub timestamp: u64,
48 pub peer_id: PublicKey,
50}
51
52impl PartialOrd for OpId {
53 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
54 Some(self.cmp(other))
55 }
56}
57
58impl Ord for OpId {
59 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
60 match self.timestamp.cmp(&other.timestamp) {
61 std::cmp::Ordering::Equal => self.peer_id.cmp(&other.peer_id),
62 ord => ord,
63 }
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69pub struct PathOperation {
70 pub id: OpId,
72 pub op_type: OpType,
74 pub path: PathBuf,
76 pub content_link: Option<Link>,
78 pub is_dir: bool,
80}
81
82#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
89pub struct PathOpLog {
90 operations: BTreeMap<OpId, PathOperation>,
92
93 #[serde(skip)]
95 local_clock: u64,
96}
97
98impl BlockEncoded<DagCborCodec> for PathOpLog {}
99
100impl PathOpLog {
101 pub fn new() -> Self {
103 Self::default()
104 }
105
106 pub fn from_operation(op: &PathOperation) -> Self {
108 let mut log = Self::new();
109 log.operations.insert(op.id.clone(), op.clone());
110 log.local_clock = op.id.timestamp;
111 log
112 }
113
114 pub fn rebuild_clock(&mut self) {
116 self.local_clock = self
117 .operations
118 .keys()
119 .map(|id| id.timestamp)
120 .max()
121 .unwrap_or(0);
122 }
123
124 pub fn record(
129 &mut self,
130 peer_id: PublicKey,
131 op_type: OpType,
132 path: impl Into<PathBuf>,
133 content_link: Option<Link>,
134 is_dir: bool,
135 ) -> OpId {
136 self.local_clock += 1;
137
138 let id = OpId {
139 timestamp: self.local_clock,
140 peer_id,
141 };
142
143 let op = PathOperation {
144 id: id.clone(),
145 op_type,
146 path: path.into(),
147 content_link,
148 is_dir,
149 };
150
151 self.operations.insert(id.clone(), op);
152 id
153 }
154
155 pub fn merge(&mut self, other: &PathOpLog) -> usize {
159 let mut added = 0;
160 for (id, op) in &other.operations {
161 if !self.operations.contains_key(id) {
162 self.operations.insert(id.clone(), op.clone());
163 added += 1;
164 if id.timestamp >= self.local_clock {
166 self.local_clock = id.timestamp + 1;
167 }
168 }
169 }
170 added
171 }
172
173 pub fn merge_with_resolver(
193 &mut self,
194 other: &PathOpLog,
195 resolver: &dyn ConflictResolver,
196 local_peer: &PublicKey,
197 ) -> MergeResult {
198 let mut result = MergeResult::new();
199
200 let mut conflicts_by_path: BTreeMap<PathBuf, Vec<(&OpId, &PathOperation)>> =
202 BTreeMap::new();
203
204 for (id, op) in &other.operations {
205 if self.operations.contains_key(id) {
207 continue;
208 }
209
210 let has_conflict = self
212 .operations
213 .values()
214 .any(|existing| operations_conflict(existing, op));
215
216 if has_conflict {
217 conflicts_by_path
218 .entry(op.path.clone())
219 .or_default()
220 .push((id, op));
221 }
222 }
223
224 for (id, op) in &other.operations {
226 if self.operations.contains_key(id) {
228 continue;
229 }
230
231 if id.timestamp >= self.local_clock {
233 self.local_clock = id.timestamp + 1;
234 }
235
236 let conflicting_base = self
238 .operations
239 .values()
240 .find(|existing| operations_conflict(existing, op));
241
242 match conflicting_base {
243 Some(base) => {
244 let conflict = Conflict::new(op.path.clone(), base.clone(), op.clone());
246 let resolution = resolver.resolve(&conflict, local_peer);
247
248 match resolution {
249 Resolution::UseBase => {
250 result.conflicts_resolved.push(ResolvedConflict {
252 conflict,
253 resolution,
254 });
255 }
256 Resolution::UseIncoming => {
257 self.operations.insert(id.clone(), op.clone());
259 result.operations_added += 1;
260 result.conflicts_resolved.push(ResolvedConflict {
261 conflict,
262 resolution,
263 });
264 }
265 Resolution::KeepBoth => {
266 self.operations.insert(id.clone(), op.clone());
268 result.operations_added += 1;
269 result.unresolved_conflicts.push(conflict);
270 }
271 Resolution::SkipBoth => {
272 result.conflicts_resolved.push(ResolvedConflict {
276 conflict,
277 resolution,
278 });
279 }
280 Resolution::RenameIncoming { new_path } => {
281 let mut renamed_op = op.clone();
283 renamed_op.path = new_path.clone();
284 self.operations.insert(id.clone(), renamed_op);
285 result.operations_added += 1;
286 result.conflicts_resolved.push(ResolvedConflict {
287 conflict,
288 resolution: Resolution::RenameIncoming { new_path },
289 });
290 }
291 }
292 }
293 None => {
294 self.operations.insert(id.clone(), op.clone());
296 result.operations_added += 1;
297 }
298 }
299 }
300
301 result
302 }
303
304 pub fn operations(&self) -> &BTreeMap<OpId, PathOperation> {
306 &self.operations
307 }
308
309 pub fn resolve_path(&self, path: impl AsRef<std::path::Path>) -> Option<&PathOperation> {
314 let path = path.as_ref();
315 let path_ops: Vec<&PathOperation> = self
317 .operations
318 .values()
319 .filter(|op| op.path == path)
320 .collect();
321
322 if path_ops.is_empty() {
323 return None;
324 }
325
326 path_ops.into_iter().max_by_key(|op| &op.id)
328 }
329
330 pub fn resolve_all(&self) -> BTreeMap<PathBuf, &PathOperation> {
335 let mut result: BTreeMap<PathBuf, &PathOperation> = BTreeMap::new();
336
337 let mut by_path: BTreeMap<&PathBuf, Vec<&PathOperation>> = BTreeMap::new();
339 for op in self.operations.values() {
340 by_path.entry(&op.path).or_default().push(op);
341 }
342
343 for (path, ops) in by_path {
345 if let Some(winner) = ops.into_iter().max_by_key(|op| &op.id) {
346 if !matches!(winner.op_type, OpType::Remove) {
348 result.insert(path.clone(), winner);
349 }
350 }
351 }
352
353 result
354 }
355
356 pub fn len(&self) -> usize {
358 self.operations.len()
359 }
360
361 pub fn is_empty(&self) -> bool {
363 self.operations.is_empty()
364 }
365
366 pub fn clear_preserving_clock(&mut self) {
372 self.operations.clear();
373 }
375
376 pub fn ops_for_path(&self, path: impl AsRef<std::path::Path>) -> Vec<&PathOperation> {
378 let path = path.as_ref();
379 self.operations
380 .values()
381 .filter(|op| op.path == path)
382 .collect()
383 }
384
385 pub fn ops_in_order(&self) -> impl Iterator<Item = &PathOperation> {
387 self.operations.values()
388 }
389}
390
391pub fn merge_logs<R: ConflictResolver>(
424 logs: &[&PathOpLog],
425 resolver: &R,
426 local_peer: &PublicKey,
427) -> (PathOpLog, Vec<MergeResult>) {
428 if logs.is_empty() {
429 return (PathOpLog::new(), Vec::new());
430 }
431
432 let mut merged = logs[0].clone();
434 let mut results = Vec::with_capacity(logs.len().saturating_sub(1));
435
436 for log in logs.iter().skip(1) {
438 let result = merged.merge_with_resolver(log, resolver, local_peer);
439 results.push(result);
440 }
441
442 (merged, results)
443}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448 use crate::crypto::SecretKey;
449
450 fn make_peer_id(seed: u8) -> PublicKey {
451 let mut seed_bytes = [0u8; 32];
454 seed_bytes[0] = seed;
455 let secret = SecretKey::from(seed_bytes);
456 secret.public()
457 }
458
459 #[test]
460 fn test_op_id_ordering() {
461 let peer1 = make_peer_id(1);
462 let peer2 = make_peer_id(2);
463
464 let id1 = OpId {
465 timestamp: 1,
466 peer_id: peer1,
467 };
468 let id2 = OpId {
469 timestamp: 2,
470 peer_id: peer1,
471 };
472 let id3 = OpId {
473 timestamp: 1,
474 peer_id: peer2,
475 };
476
477 assert!(id2 > id1);
479 assert!(id3 != id1);
481 if peer2 > peer1 {
483 assert!(id3 > id1);
484 } else {
485 assert!(id3 < id1);
486 }
487 }
488
489 #[test]
490 fn test_record_operation() {
491 let peer1 = make_peer_id(1);
492 let mut log = PathOpLog::new();
493
494 let id = log.record(peer1, OpType::Add, "file.txt", None, false);
495
496 assert_eq!(id.timestamp, 1);
497 assert_eq!(log.len(), 1);
498
499 let op = log.operations.get(&id).unwrap();
500 assert_eq!(op.path, PathBuf::from("file.txt"));
501 assert!(matches!(op.op_type, OpType::Add));
502 }
503
504 #[test]
505 fn test_record_multiple_operations() {
506 let peer1 = make_peer_id(1);
507 let mut log = PathOpLog::new();
508
509 let id1 = log.record(peer1, OpType::Add, "file1.txt", None, false);
510 let id2 = log.record(peer1, OpType::Add, "file2.txt", None, false);
511 let id3 = log.record(peer1, OpType::Remove, "file1.txt", None, false);
512
513 assert_eq!(id1.timestamp, 1);
514 assert_eq!(id2.timestamp, 2);
515 assert_eq!(id3.timestamp, 3);
516 assert_eq!(log.len(), 3);
517 }
518
519 #[test]
520 fn test_merge_logs() {
521 let peer1 = make_peer_id(1);
522 let peer2 = make_peer_id(2);
523
524 let mut log1 = PathOpLog::new();
525 log1.record(peer1, OpType::Add, "file1.txt", None, false);
526
527 let mut log2 = PathOpLog::new();
528 log2.record(peer2, OpType::Add, "file2.txt", None, false);
529
530 let added = log1.merge(&log2);
531
532 assert_eq!(added, 1);
533 assert_eq!(log1.len(), 2);
534 }
535
536 #[test]
537 fn test_merge_idempotent() {
538 let peer1 = make_peer_id(1);
539 let mut log1 = PathOpLog::new();
540 log1.record(peer1, OpType::Add, "file.txt", None, false);
541
542 let log1_clone = log1.clone();
543 let added = log1.merge(&log1_clone);
544
545 assert_eq!(added, 0);
546 assert_eq!(log1.len(), 1);
547 }
548
549 #[test]
550 fn test_resolve_path_single_op() {
551 let peer1 = make_peer_id(1);
552 let mut log = PathOpLog::new();
553 log.record(peer1, OpType::Add, "file.txt", None, false);
554
555 let resolved = log.resolve_path("file.txt");
556 assert!(resolved.is_some());
557 assert!(matches!(resolved.unwrap().op_type, OpType::Add));
558 }
559
560 #[test]
561 fn test_resolve_path_latest_wins() {
562 let peer1 = make_peer_id(1);
563 let mut log = PathOpLog::new();
564
565 log.record(peer1, OpType::Add, "file.txt", None, false);
566 log.record(peer1, OpType::Remove, "file.txt", None, false);
567
568 let resolved = log.resolve_path("file.txt");
569 assert!(resolved.is_some());
570 assert!(matches!(resolved.unwrap().op_type, OpType::Remove));
571 }
572
573 #[test]
574 fn test_resolve_all_excludes_removed() {
575 let peer1 = make_peer_id(1);
576 let mut log = PathOpLog::new();
577
578 log.record(peer1, OpType::Add, "file1.txt", None, false);
579 log.record(peer1, OpType::Add, "file2.txt", None, false);
580 log.record(peer1, OpType::Remove, "file1.txt", None, false);
581
582 let resolved = log.resolve_all();
583
584 assert_eq!(resolved.len(), 1);
585 assert!(resolved.contains_key(&PathBuf::from("file2.txt")));
586 assert!(!resolved.contains_key(&PathBuf::from("file1.txt")));
587 }
588
589 #[test]
590 fn test_concurrent_ops_different_peers() {
591 let peer1 = make_peer_id(1);
592 let peer2 = make_peer_id(2);
593
594 let mut log1 = PathOpLog::new();
595 log1.record(peer1, OpType::Add, "file.txt", None, false);
596
597 let mut log2 = PathOpLog::new();
598 log2.record(peer2, OpType::Remove, "file.txt", None, false);
599
600 log1.merge(&log2);
602
603 let resolved = log1.resolve_path("file.txt");
605 assert!(resolved.is_some());
606 let winning_op = resolved.unwrap();
607
608 if peer2 > peer1 {
610 assert!(matches!(winning_op.op_type, OpType::Remove));
611 } else {
612 assert!(matches!(winning_op.op_type, OpType::Add));
613 }
614 }
615
616 #[test]
617 fn test_mv_operation() {
618 let peer1 = make_peer_id(1);
619 let mut log = PathOpLog::new();
620
621 log.record(peer1, OpType::Add, "old.txt", None, false);
622 log.record(
623 peer1,
624 OpType::Mv {
625 from: PathBuf::from("old.txt"),
626 },
627 "new.txt",
628 None,
629 false,
630 );
631
632 assert_eq!(log.len(), 2);
633
634 let resolved = log.resolve_path("new.txt");
636 assert!(resolved.is_some());
637 assert!(matches!(resolved.unwrap().op_type, OpType::Mv { .. }));
638 }
639
640 #[test]
641 fn test_serialization_roundtrip() {
642 use crate::linked_data::BlockEncoded;
643
644 let peer1 = make_peer_id(1);
645 let mut log = PathOpLog::new();
646
647 log.record(peer1, OpType::Add, "file1.txt", None, false);
648 log.record(peer1, OpType::Mkdir, "dir", None, true);
649 log.record(
650 peer1,
651 OpType::Mv {
652 from: PathBuf::from("file1.txt"),
653 },
654 "dir/file1.txt",
655 None,
656 false,
657 );
658
659 let encoded = log.encode().unwrap();
660 let decoded = PathOpLog::decode(&encoded).unwrap();
661
662 assert_eq!(log.operations, decoded.operations);
664 }
665
666 #[test]
667 fn test_merge_with_resolver_no_conflicts() {
668 use super::super::conflict::LastWriteWins;
669
670 let peer1 = make_peer_id(1);
671 let peer2 = make_peer_id(2);
672
673 let mut log1 = PathOpLog::new();
674 log1.record(peer1, OpType::Add, "file1.txt", None, false);
675
676 let mut log2 = PathOpLog::new();
677 log2.record(peer2, OpType::Add, "file2.txt", None, false);
678
679 let resolver = LastWriteWins::new();
680 let result = log1.merge_with_resolver(&log2, &resolver, &peer1);
681
682 assert_eq!(result.operations_added, 1);
683 assert_eq!(result.conflicts_resolved.len(), 0);
684 assert!(!result.has_unresolved());
685 assert_eq!(log1.len(), 2);
686 }
687
688 #[test]
689 fn test_merge_with_resolver_last_write_wins() {
690 use super::super::conflict::{LastWriteWins, Resolution};
691
692 let peer1 = make_peer_id(1);
693 let peer2 = make_peer_id(2);
694
695 let mut log1 = PathOpLog::new();
696 log1.record(peer1, OpType::Add, "file.txt", None, false);
697
698 let mut log2 = PathOpLog::new();
699 log2.record(peer2, OpType::Add, "dummy", None, false); log2.record(peer2, OpType::Remove, "file.txt", None, false); let resolver = LastWriteWins::new();
704 let result = log1.merge_with_resolver(&log2, &resolver, &peer1);
705
706 assert_eq!(result.operations_added, 2);
709 assert_eq!(result.conflicts_resolved.len(), 1);
710
711 let resolved = &result.conflicts_resolved[0];
712 assert_eq!(resolved.resolution, Resolution::UseIncoming);
713 }
714
715 #[test]
716 fn test_merge_with_resolver_base_wins() {
717 use super::super::conflict::{BaseWins, Resolution};
718
719 let peer1 = make_peer_id(1);
720 let peer2 = make_peer_id(2);
721
722 let mut log1 = PathOpLog::new();
723 log1.record(peer1, OpType::Add, "file.txt", None, false);
724
725 let mut log2 = PathOpLog::new();
726 log2.record(peer2, OpType::Remove, "file.txt", None, false);
727
728 let resolver = BaseWins::new();
729 let result = log1.merge_with_resolver(&log2, &resolver, &peer1);
730
731 assert_eq!(result.operations_added, 0);
733 assert_eq!(result.conflicts_resolved.len(), 1);
734
735 let resolved = &result.conflicts_resolved[0];
736 assert_eq!(resolved.resolution, Resolution::UseBase);
737
738 let resolved_path = log1.resolve_path("file.txt");
740 assert!(matches!(resolved_path.unwrap().op_type, OpType::Add));
741 }
742
743 #[test]
744 fn test_merge_with_resolver_fork_on_conflict() {
745 use super::super::conflict::ForkOnConflict;
746
747 let peer1 = make_peer_id(1);
748 let peer2 = make_peer_id(2);
749
750 let mut log1 = PathOpLog::new();
751 log1.record(peer1, OpType::Add, "file.txt", None, false);
752
753 let mut log2 = PathOpLog::new();
754 log2.record(peer2, OpType::Add, "file.txt", None, false);
755
756 let resolver = ForkOnConflict::new();
757 let result = log1.merge_with_resolver(&log2, &resolver, &peer1);
758
759 assert_eq!(result.operations_added, 1);
762 assert_eq!(result.conflicts_resolved.len(), 0);
763 assert!(result.has_unresolved());
764 assert_eq!(result.unresolved_conflicts.len(), 1);
765
766 assert_eq!(log1.len(), 2);
768
769 let resolved = log1.resolve_path("file.txt").unwrap();
771 if peer2 > peer1 {
773 assert_eq!(resolved.id.peer_id, peer2);
774 } else {
775 assert_eq!(resolved.id.peer_id, peer1);
776 }
777 }
778
779 #[test]
780 fn test_merge_with_resolver_concurrent_ops() {
781 use super::super::conflict::LastWriteWins;
782
783 let peer1 = make_peer_id(1);
784 let peer2 = make_peer_id(2);
785
786 let mut log1 = PathOpLog::new();
788 log1.record(peer1, OpType::Add, "file.txt", None, false);
789
790 let mut log2 = PathOpLog::new();
791 log2.record(peer2, OpType::Add, "file.txt", None, false);
792
793 let resolver = LastWriteWins::new();
795 let result = log1.merge_with_resolver(&log2, &resolver, &peer1);
796
797 assert_eq!(result.total_conflicts(), 1);
799
800 if peer2 > peer1 {
803 assert_eq!(result.operations_added, 1);
805 } else {
806 assert_eq!(result.operations_added, 0);
808 }
809 }
810
811 #[test]
812 fn test_merge_with_resolver_idempotent() {
813 use super::super::conflict::LastWriteWins;
814
815 let peer1 = make_peer_id(1);
816 let mut log1 = PathOpLog::new();
817 log1.record(peer1, OpType::Add, "file.txt", None, false);
818
819 let log1_clone = log1.clone();
820 let resolver = LastWriteWins::new();
821 let result = log1.merge_with_resolver(&log1_clone, &resolver, &peer1);
822
823 assert_eq!(result.operations_added, 0);
825 assert_eq!(result.total_conflicts(), 0);
826 assert_eq!(log1.len(), 1);
827 }
828
829 #[test]
830 fn test_merge_with_resolver_mixed_conflicts() {
831 use super::super::conflict::LastWriteWins;
832
833 let peer1 = make_peer_id(1);
834 let peer2 = make_peer_id(2);
835
836 let mut log1 = PathOpLog::new();
837 log1.record(peer1, OpType::Add, "file1.txt", None, false);
838 log1.record(peer1, OpType::Add, "file2.txt", None, false);
839
840 let mut log2 = PathOpLog::new();
841 log2.record(peer2, OpType::Remove, "file1.txt", None, false); log2.record(peer2, OpType::Add, "file3.txt", None, false); let resolver = LastWriteWins::new();
845 let result = log1.merge_with_resolver(&log2, &resolver, &peer1);
846
847 assert_eq!(result.total_conflicts(), 1);
850
851 assert!(log1.resolve_path("file3.txt").is_some());
853 }
854
855 #[test]
856 fn test_merge_with_resolver_conflict_file() {
857 use super::super::conflict::{ConflictFile, Resolution};
858 use crate::linked_data::Link;
859
860 let peer1 = make_peer_id(1);
861 let peer2 = make_peer_id(2);
862
863 let make_link = |seed: u8| {
865 let mut hash_bytes = [0u8; 32];
866 hash_bytes[0] = seed;
867 let hash = iroh_blobs::Hash::from_bytes(hash_bytes);
868 Link::new(crate::linked_data::LD_RAW_CODEC, hash)
869 };
870
871 let link_local = make_link(0xAA);
872 let link_other = make_link(0xBB);
873 let link_incoming = make_link(0xCC);
874
875 let mut log1 = PathOpLog::new();
876 log1.record(peer1, OpType::Add, "document.txt", Some(link_local), false);
877
878 let mut log2 = PathOpLog::new();
879 log2.record(peer2, OpType::Add, "other.txt", Some(link_other), false); log2.record(
882 peer2,
883 OpType::Add,
884 "document.txt",
885 Some(link_incoming.clone()),
886 false,
887 ); let resolver = ConflictFile::new();
890 let result = log1.merge_with_resolver(&log2, &resolver, &peer1);
891
892 assert_eq!(result.operations_added, 2);
894 assert_eq!(result.conflicts_resolved.len(), 1);
895
896 let resolved = &result.conflicts_resolved[0];
898 match &resolved.resolution {
899 Resolution::RenameIncoming { new_path } => {
900 let expected_version: String =
902 link_incoming.hash().to_string().chars().take(8).collect();
903 assert_eq!(
904 new_path,
905 &std::path::PathBuf::from(format!("document.txt@{}", expected_version))
906 );
907 }
908 _ => panic!("Expected RenameIncoming, got {:?}", resolved.resolution),
909 }
910
911 let original = log1.resolve_path("document.txt").unwrap();
913 assert_eq!(original.id.peer_id, peer1);
914
915 let resolved_conflict = &result.conflicts_resolved[0];
917 if let Resolution::RenameIncoming { new_path } = &resolved_conflict.resolution {
918 let conflict = log1.resolve_path(new_path).unwrap();
919 assert_eq!(conflict.id.peer_id, peer2);
920 }
921 }
922
923 #[test]
924 fn test_merge_logs_empty() {
925 use super::super::conflict::LastWriteWins;
926 use super::merge_logs;
927
928 let peer1 = make_peer_id(1);
929 let resolver = LastWriteWins::new();
930
931 let (merged, results) = merge_logs::<LastWriteWins>(&[], &resolver, &peer1);
933 assert!(merged.is_empty());
934 assert!(results.is_empty());
935 }
936
937 #[test]
938 fn test_merge_logs_single() {
939 use super::super::conflict::LastWriteWins;
940 use super::merge_logs;
941
942 let peer1 = make_peer_id(1);
943 let mut log1 = PathOpLog::new();
944 log1.record(peer1, OpType::Add, "file.txt", None, false);
945
946 let resolver = LastWriteWins::new();
947 let (merged, results) = merge_logs(&[&log1], &resolver, &peer1);
948
949 assert_eq!(merged.len(), 1);
951 assert!(results.is_empty());
952 }
953
954 #[test]
955 fn test_merge_logs_two_no_conflict() {
956 use super::super::conflict::LastWriteWins;
957 use super::merge_logs;
958
959 let peer1 = make_peer_id(1);
960 let peer2 = make_peer_id(2);
961
962 let mut log1 = PathOpLog::new();
963 log1.record(peer1, OpType::Add, "file1.txt", None, false);
964
965 let mut log2 = PathOpLog::new();
966 log2.record(peer2, OpType::Add, "file2.txt", None, false);
967
968 let resolver = LastWriteWins::new();
969 let (merged, results) = merge_logs(&[&log1, &log2], &resolver, &peer1);
970
971 assert_eq!(merged.len(), 2);
973 assert!(merged.resolve_path("file1.txt").is_some());
974 assert!(merged.resolve_path("file2.txt").is_some());
975
976 assert_eq!(results.len(), 1);
978 assert_eq!(results[0].operations_added, 1);
979 assert_eq!(results[0].total_conflicts(), 0);
980 }
981
982 #[test]
983 fn test_merge_logs_three_way() {
984 use super::super::conflict::ConflictFile;
985 use super::merge_logs;
986 use crate::linked_data::Link;
987
988 let peer1 = make_peer_id(1);
989 let peer2 = make_peer_id(2);
990 let peer3 = make_peer_id(3);
991
992 let make_link = |seed: u8| {
993 let mut hash_bytes = [0u8; 32];
994 hash_bytes[0] = seed;
995 let hash = iroh_blobs::Hash::from_bytes(hash_bytes);
996 Link::new(crate::linked_data::LD_RAW_CODEC, hash)
997 };
998
999 let mut log_alice = PathOpLog::new();
1001 log_alice.record(peer1, OpType::Add, "file.txt", Some(make_link(0xAA)), false);
1002
1003 let mut log_bob = PathOpLog::new();
1005 log_bob.record(peer2, OpType::Add, "file.txt", Some(make_link(0xBB)), false);
1006
1007 let mut log_carol = PathOpLog::new();
1009 log_carol.record(peer3, OpType::Add, "file.txt", Some(make_link(0xCC)), false);
1010
1011 let resolver = ConflictFile::new();
1012 let (merged, results) = merge_logs(&[&log_alice, &log_bob, &log_carol], &resolver, &peer1);
1013
1014 assert_eq!(results.len(), 2);
1016
1017 assert_eq!(results[0].total_conflicts(), 1);
1019 assert_eq!(results[1].total_conflicts(), 1);
1020
1021 assert_eq!(merged.len(), 3);
1023
1024 assert!(merged.resolve_path("file.txt").is_some());
1026 }
1027
1028 #[test]
1029 fn test_merge_logs_accumulates_operations() {
1030 use super::super::conflict::LastWriteWins;
1031 use super::merge_logs;
1032
1033 let peer1 = make_peer_id(1);
1034 let peer2 = make_peer_id(2);
1035 let peer3 = make_peer_id(3);
1036
1037 let mut log1 = PathOpLog::new();
1038 log1.record(peer1, OpType::Add, "a.txt", None, false);
1039 log1.record(peer1, OpType::Add, "b.txt", None, false);
1040
1041 let mut log2 = PathOpLog::new();
1042 log2.record(peer2, OpType::Add, "c.txt", None, false);
1043
1044 let mut log3 = PathOpLog::new();
1045 log3.record(peer3, OpType::Add, "d.txt", None, false);
1046 log3.record(peer3, OpType::Add, "e.txt", None, false);
1047
1048 let resolver = LastWriteWins::new();
1049 let (merged, results) = merge_logs(&[&log1, &log2, &log3], &resolver, &peer1);
1050
1051 assert_eq!(merged.len(), 5);
1053
1054 assert_eq!(results.len(), 2);
1056 assert_eq!(results[0].operations_added, 1); assert_eq!(results[1].operations_added, 2); }
1059}