-
Notifications
You must be signed in to change notification settings - Fork 3k
/
Copy pathThreeWayTreeMerger.swift
1423 lines (1217 loc) · 66.5 KB
/
ThreeWayTreeMerger.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import Deferred
import Foundation
import Shared
import Storage
import XCGLogger
private let log = Logger.syncLogger
private func negate<T>(_ f: @escaping (T) throws -> Bool) -> (T) throws -> Bool {
return { try !f($0) }
}
extension Collection {
func exclude(_ predicate: @escaping (Self.Iterator.Element) throws -> Bool) throws -> [Self.Iterator.Element] {
return try self.filter(negate(predicate))
}
}
/**
* This class takes as input three 'trees'.
*
* The mirror is always complete, never contains deletions, never has
* orphans, and has a single root.
*
* Each of local and remote can contain a number of subtrees (each of which must
* be a folder or root), a number of deleted GUIDs, and a number of orphans (records
* with no known parent).
*
* As output it produces a merged tree. The tree contains the new structure,
* including every record that we're keeping, and also makes note of any deletions.
*
* The merged tree can be walked to yield a set of operations against the original
* three trees. Those operations will make the upstream source and local storage
* match the merge output.
*
* It's very likely that there's almost no overlap between local and remote, and
* thus no real conflicts to resolve -- a three-way merge isn't always a bad thing
* -- but we won't know until we compare records.
*
*
* Even though this is called 'three way merge', it also handles the case
* of a two-way merge (one without a shared parent; for the roots, this will only
* be on a first sync): content-based and structural merging is needed at all
* layers of the hierarchy, so we simply generalize that to also apply to roots.
*
* In a sense, a two-way merge is solved by constructing a shared parent consisting of
* roots, which are implicitly shared.
*
* (Special care must be taken to not deduce that one side has deleted a root, of course,
* as would be the case of a Sync server that doesn't contain
* a Mobile Bookmarks folder -- the set of roots can only grow, not shrink.)
*
*
* To begin with we structurally reconcile. If necessary we will lazily fetch the
* attributes of records in order to do a content-based reconciliation. Once we've
* matched up any records that match (including remapping local GUIDs), we're able to
* process _content_ changes, which is much simpler.
*
* We have to handle an arbitrary combination of the following structural operations:
*
* * Creating a folder.
* Created folders might now hold existing items, new items, or nothing at all.
* * Creating a bookmark.
* It might be in a new folder or an existing folder.
* * Moving one or more leaf records to an existing or new folder.
* * Reordering the children of a folder.
* * Deleting an entire subtree.
* * Deleting an entire subtree apart from some moved nodes.
* * Deleting a leaf node.
* * Transplanting a subtree: moving a folder but not changing its children.
*
* And, of course, the non-structural operations such as renaming or changing URLs.
*
* We ignore all changes to roots themselves; the only acceptable operation on a root
* is to change its children. The Places root is entirely immutable.
*
* Steps:
* * Construct a collection of subtrees for local and buffer, and a complete tree for the mirror.
* The more thorough this step, the more confidence we have in consistency.
* * Fetch all local and remote deletions. These won't be included in structure (for obvious
* reasons); we hold on to them explicitly so we can spot the difference between a move
* and a deletion.
* * Walk each subtree, top-down. At each point if there are two back-pointers to
* the mirror node for a GUID, we have a potential conflict, and we have all three
* parts that we need to resolve it via a content-based or structure-based 3WM.
*
* Observations:
* * If every GUID on each side is present in the mirror, we have no new records.
* * If a non-root GUID is present on both sides but not in the mirror, then either
* we're re-syncing from scratch, or (unlikely) we have a random collision.
* * Otherwise, we have a GUID that we don't recognize. We will structure+content reconcile
* this later -- we first make sure we have handled any tree moves, so that the addition
* of a bookmark to a moved folder on A, and the addition of the same bookmark to the non-
* moved version of the folder, will collide successfully.
*
* When we look at a child list:
* * It's the same. Great! Keep walking down.
* * There are added GUIDs.
* * An added GUID might be a move from elsewhere. Coordinate with the removal step.
* * An added GUID might be a brand new record. If there are local additions too,
* check to see if they value-reconcile, and keep the remote GUID.
* * There are removed GUIDs.
* * A removed GUID might have been deleted. Deletions win.
* * A missing GUID might be a move -- removed from here and added to another folder.
* Process this as a move.
* * The order has changed.
*
* When we get to a subtree that contains no changes, we can never hit conflicts, and
* application becomes easier.
*
* When we run out of subtrees on both sides, we're done.
*
* Note that these trees don't include values. This is because we usually don't need them:
* if there are no conflicts, or no shared parents, we can do everything we need to do
* at this stage simply with structure and GUIDs, and then flush rows straight from the
* buffer into the mirror with a single SQL INSERT. We do need to fetch values later
* in some cases: to amend child lists or otherwise construct outbound records. We do
* need to fetch values immediately in other cases: in order to reconcile conflicts.
* Those should ordinarily be uncommon, and because we know most of the conflicting
* GUIDs up-front, we can prime a cache of records.
*/
class ThreeWayTreeMerger {
let local: BookmarkTree
let mirror: BookmarkTree
let remote: BookmarkTree
var merged: MergedTree
// Don't merge twice.
var mergeAttempted: Bool = false
let itemSources: ItemSources
// Sets computed by looking at the three trees. These are used for diagnostics,
// to simplify operations, to pre-fetch items for value comparison, and for testing.
let mirrorAllGUIDs: Set<GUID> // Excluding deletions.
let localAllGUIDs: Set<GUID> // Excluding deletions.
let remoteAllGUIDs: Set<GUID> // Excluding deletions.
let localAdditions: Set<GUID> // New records added locally, not present in the mirror.
let remoteAdditions: Set<GUID> // New records from the server, not present in the mirror.
let allDeletions: Set<GUID> // Records deleted locally or remotely.
var allChangedGUIDs: Set<GUID> // Everything added or changed locally or remotely.
let conflictingGUIDs: Set<GUID> // Anything added or changed both locally and remotely.
let nonRemoteKnownGUIDs: Set<GUID> // Everything existing, added, or deleted locally or in the mirror.
// For now, track just one list. We might need to split this later.
var done: Set<GUID> = Set()
// Local records that we identified as being the same as remote records.
var duped: Set<GUID> = Set()
init(local: BookmarkTree, mirror: BookmarkTree, remote: BookmarkTree, itemSources: ItemSources) {
precondition(mirror.root != nil)
assert((mirror.root!.children?.count ?? 0) == BookmarkRoots.RootChildren.count)
precondition(mirror.orphans.isEmpty)
precondition(mirror.deleted.isEmpty)
precondition(mirror.subtrees.count == 1)
// These are runtime-tested in merge(). assert to make sure that tests
// don't do anything stupid, and we don't slip past those constraints.
//assert(local.isFullyRootedIn(mirror))
//assert(remote.isFullyRootedIn(mirror))
self.local = local
self.mirror = mirror
self.remote = remote
self.itemSources = itemSources
self.merged = MergedTree(mirrorRoot: self.mirror.root!)
// We won't get here unless every local and remote orphan is correctly rooted
// when overlaid on the mirror, so we don't need to exclude orphans here.
self.mirrorAllGUIDs = self.mirror.modified
self.localAllGUIDs = self.local.modified
self.remoteAllGUIDs = self.remote.modified
self.localAdditions = localAllGUIDs.subtracting(mirrorAllGUIDs)
self.remoteAdditions = remoteAllGUIDs.subtracting(mirrorAllGUIDs)
self.allDeletions = self.local.deleted.union(self.remote.deleted)
self.allChangedGUIDs = localAllGUIDs.union(self.remoteAllGUIDs)
self.conflictingGUIDs = localAllGUIDs.intersection(remoteAllGUIDs)
self.nonRemoteKnownGUIDs = self.mirrorAllGUIDs.union(self.localAllGUIDs).union(self.local.deleted)
}
fileprivate func nullOrMatch(_ a: String?, _ b: String?) -> Bool {
guard let a = a, let b = b else {
return true
}
return a == b
}
/**
* When we have a folder match and new records on each side -- records
* not mentioned in the buffer -- it's possible that the new records
* are the same but have different GUIDs.
* This function will look at value matches to identify a local
* equivalent in this folder, returning nil if none are found.
*
* Note that we don't match records that have already been matched, and
* we don't match any for which a GUID is known in the mirror or remote.
*/
fileprivate func findNewLocalNodeMatchingContentOfRemoteNote(_ remote: BookmarkTreeNode, inFolder parent: GUID, withLocalChildren children: [BookmarkTreeNode], havingSeen seen: Set<GUID>) -> BookmarkTreeNode? {
// TODO: don't compute this list once per incoming child! Profile me.
let candidates = children.filter { child in
let childGUID = child.recordGUID
return !seen.contains(childGUID) && // Not already used in this folder.
!self.remoteAdditions.contains(childGUID) && // Not locally and remotely added with same GUID.
!self.remote.deleted.contains(childGUID) && // Not remotely deleted.
!self.done.contains(childGUID) // Not already processed elsewhere in the tree.
}
guard let remoteValue = self.itemSources.buffer.getBufferItemWithGUID(remote.recordGUID).value.successValue else {
log.error("Couldn't find remote value for \(remote.recordGUID).")
return nil
}
let guids = candidates.map { $0.recordGUID }
guard let items = self.itemSources.local.getLocalItemsWithGUIDs(guids).value.successValue else {
log.error("Couldn't find local values for \(candidates.count) candidates.")
return nil
}
// Return the first candidate that's a value match.
guard let localItem = (guids.flatMap { items[$0] }.find { $0.sameAs(remoteValue) }) else {
log.debug("Didn't find a local value match for new remote record \(remote.recordGUID).")
return nil
}
log.debug("Found a local match \(localItem.guid) for new remote record \(remote.recordGUID) in parent \(parent).")
// Find the original contributing child node by GUID.
return children.find { $0.recordGUID == localItem.guid }
}
fileprivate func takeMirrorChildrenInMergedNode(_ result: MergedTreeNode) throws {
guard let mirrorChildren = result.mirror?.children else {
preconditionFailure("Expected children.")
}
let out: [MergedTreeNode] = try mirrorChildren.flatMap { child in
// TODO: handle deletions. That might change the below from 'Unchanged'
// to 'New'.
let childGUID = child.recordGUID
if self.done.contains(childGUID) {
log.warning("Not taking mirror child \(childGUID): already done. This is unexpected.")
return nil
}
let localCounterpart = self.local.find(childGUID)
let remoteCounterpart = self.remote.find(childGUID)
return try self.mergeNode(childGUID, localNode: localCounterpart, mirrorNode: child, remoteNode: remoteCounterpart)
}
result.mergedChildren = out
result.structureState = MergeState.unchanged
}
fileprivate func oneWayMergeChildListsIntoMergedNode(_ result: MergedTreeNode, fromRemote remote: BookmarkTreeNode) throws {
guard case .folder = remote else {
preconditionFailure("Expected folder from which to merge children.")
}
result.structureState = MergeState.remote // If the list changes, this will switch to .new.
try self.mergeChildListsIntoMergedNode(result, fromLocal: nil, remote: remote, mirror: self.mirror.find(remote.recordGUID))
}
fileprivate func oneWayMergeChildListsIntoMergedNode(_ result: MergedTreeNode, fromLocal local: BookmarkTreeNode) throws {
guard case .folder = local else {
preconditionFailure("Expected folder from which to merge children.")
}
result.structureState = MergeState.local // If the list changes, this will switch to .new.
try self.mergeChildListsIntoMergedNode(result, fromLocal: local, remote: nil, mirror: self.mirror.find(local.recordGUID))
}
fileprivate func mergeChildListsIntoMergedNode(_ result: MergedTreeNode, fromLocal local: BookmarkTreeNode?, remote: BookmarkTreeNode?, mirror: BookmarkTreeNode?) throws {
precondition(local != nil || remote != nil, "Expected either local or remote folder for merge.")
// The most trivial implementation: take everything in the first list, then append
// everything new in the second list.
// Anything present in both is resolved.
// We can't get away from handling deletions and moves, etc. -- one might have
// created a folder on two devices and moved existing items on one, some of which
// might have been deleted on the other.
// This kind of shit is why bookmark sync is hard.
// See each of the test cases in TestBookmarkTreeMerging, which have helpful diagrams.
var out: [MergedTreeNode] = []
var seen: Set<GUID> = Set()
var changed = false
func processRemoteOrphansForNode(_ node: BookmarkTreeNode) throws -> [MergedTreeNode]? {
// Now we recursively merge down into our list of orphans. If those contain deleted
// subtrees, excess leaves will be flattened up; we'll get a single list of nodes
// here, and we'll take them as additional children.
let guid = node.recordGUID
func isLocallyDeleted(_ child: BookmarkTreeNode) throws -> Bool {
return try checkForLocalDeletionOfRemoteNode(child, mirrorNode: self.mirror.find(child.recordGUID))
}
guard let orphans = try node.children?.exclude(isLocallyDeleted), !orphans.isEmpty else {
log.debug("No remote orphans from local deletion of \(guid).")
return nil
}
let mergedOrphans = try orphans.map { (orphan: BookmarkTreeNode) throws -> MergedTreeNode in
let guidO = orphan.recordGUID
let locO = self.local.find(guidO)
let remO = orphan
let mirO = self.mirror.find(guidO)
log.debug("Merging up remote orphan \(guidO).")
return try self.mergeNode(guidO, localNode: locO, mirrorNode: mirO, remoteNode: remO)
}
log.debug("Collected \(mergedOrphans.count) remote orphans for deleted folder \(guid).")
if mergedOrphans.isEmpty {
return nil
}
changed = true
return mergedOrphans
}
func processLocalOrphansForNode(_ node: BookmarkTreeNode) throws -> [MergedTreeNode]? {
// Now we recursively merge down into our list of orphans. If those contain deleted
// subtrees, excess leaves will be flattened up; we'll get a single list of nodes
// here, and we'll take them as additional children.
let guid = node.recordGUID
if case .folder = node {} else {
log.debug("\(guid) isn't a folder, so it won't have orphans.")
return nil
}
func isRemotelyDeleted(_ child: BookmarkTreeNode) throws -> Bool {
return try checkForRemoteDeletionOfLocalNode(child, mirrorNode: self.mirror.find(child.recordGUID))
}
guard let orphans = try node.children?.exclude(isRemotelyDeleted), !orphans.isEmpty else {
log.debug("No local orphans from remote deletion of folder \(guid).")
return nil
}
let mergedOrphans = try orphans.map { (orphan: BookmarkTreeNode) throws -> MergedTreeNode in
let guidO = orphan.recordGUID
let locO = orphan
let remO = self.remote.find(guidO)
let mirO = self.mirror.find(guidO)
log.debug("Merging up local orphan \(guidO).")
return try self.mergeNode(guidO, localNode: locO, mirrorNode: mirO, remoteNode: remO)
}
log.debug("Collected \(mergedOrphans.count) local orphans for deleted folder \(guid).")
if mergedOrphans.isEmpty {
return nil
}
changed = true
return mergedOrphans
}
func checkForLocalDeletionOfRemoteNode(_ node: BookmarkTreeNode, mirrorNode: BookmarkTreeNode?) throws -> Bool {
let guid = node.recordGUID
guard self.local.deleted.contains(guid) else {
return false
}
// It was locally deleted. This would be good enough for us,
// but we need to ensure that any remote children are recursively
// deleted or handled as orphans.
log.warning("Quietly accepting local deletion of record \(guid).")
changed = true
self.merged.deleteRemotely.insert(guid)
self.merged.acceptLocalDeletion.insert(guid)
if mirrorNode != nil {
self.merged.deleteFromMirror.insert(guid)
}
if let orphans = try processRemoteOrphansForNode(node) {
out.append(contentsOf: try self.relocateOrphansTo(result, orphans: orphans))
}
return true
}
func checkForRemoteDeletionOfLocalNode(_ node: BookmarkTreeNode, mirrorNode: BookmarkTreeNode?) throws -> Bool {
let guid = node.recordGUID
guard self.remote.deleted.contains(guid) else {
return false
}
// It was remotely deleted. This would be good enough for us,
// but we need to ensure that any local children are recursively
// deleted or handled as orphans.
log.warning("Quietly accepting remote deletion of record \(guid).")
self.merged.deleteLocally.insert(guid)
self.merged.acceptRemoteDeletion.insert(guid)
if mirrorNode != nil {
self.merged.deleteFromMirror.insert(guid)
}
if let orphans = try processLocalOrphansForNode(node) {
out.append(contentsOf: try self.relocateOrphansTo(result, orphans: orphans))
}
return true
}
// Do a recursive merge of each child.
if let remote = remote, let children = remote.children {
try children.forEach { rem in
let guid = rem.recordGUID
seen.insert(guid)
if self.done.contains(guid) {
log.debug("Processing children of \(result.guid). Child \(guid) already seen elsewhere!")
return
}
if try checkForLocalDeletionOfRemoteNode(rem, mirrorNode: self.mirror.find(guid)) {
log.debug("Child \(guid) is locally deleted.")
return
}
let mir = self.mirror.find(guid)
if let localByGUID = self.local.find(guid) {
// Let's check the parent of the local match. If it differs, then the matching
// record is elsewhere in the local tree, and we need to decide which place to
// keep it.
// We do so by finding the modification time of the parent on each side,
// unless one of the records is explicitly non-modified.
if let localParentGUID = self.local.parents[guid] {
// Oh hey look! Ad hoc three-way merge!
let mirrorParentGUID = self.mirror.parents[guid]
if localParentGUID != result.guid {
log.debug("Local child \(guid) is in folder \(localParentGUID), but remotely is in \(result.guid).")
if mirrorParentGUID != localParentGUID {
log.debug("… and locally it has changed since our last sync, moving from \(mirrorParentGUID ?? "nil") to \(localParentGUID).")
// Find out which parent is most recent.
if let localRecords = self.itemSources.local.getLocalItemsWithGUIDs([localParentGUID, guid]).value.successValue,
let remoteRecords = self.itemSources.buffer.getBufferItemsWithGUIDs([result.guid, guid]).value.successValue {
let latestLocalTimestamp = max(localRecords[guid]?.localModified ?? 0, localRecords[localParentGUID]?.localModified ?? 0)
let latestRemoteTimestamp = max(remoteRecords[guid]?.serverModified ?? 0, remoteRecords[result.guid]?.serverModified ?? 0)
log.debug("Latest remote timestamp: \(latestRemoteTimestamp). Latest local timestamp: \(latestLocalTimestamp).")
if latestLocalTimestamp > latestRemoteTimestamp {
log.debug("Keeping record in its local position. We'll merge these later.")
return
}
log.debug("Taking remote, because it's later. Merging now.")
}
} else {
log.debug("\(guid) didn't move from \(mirrorParentGUID ?? "nil") since our last sync. Taking remote parent.")
}
} else {
log.debug("\(guid) is locally in \(localParentGUID) and remotely in \(result.guid). Easy.")
}
}
out.append(try self.mergeNode(guid, localNode: localByGUID, mirrorNode: mir, remoteNode: rem))
return
}
// We don't ever have to handle moves in this case: we only search this directory.
let localByContent: BookmarkTreeNode?
if let localChildren = local?.children {
localByContent = self.findNewLocalNodeMatchingContentOfRemoteNote(rem, inFolder: result.guid, withLocalChildren: localChildren, havingSeen: seen)
} else {
localByContent = nil
}
out.append(try self.mergeNode(guid, localNode: localByContent, mirrorNode: mir, remoteNode: rem))
}
}
if let local = local, let children = local.children {
try children.forEach { loc in
let guid = loc.recordGUID
if seen.contains(guid) {
log.debug("Already saw local child \(guid).")
return
}
if self.done.contains(guid) {
log.debug("Already saw local child \(guid) elsewhere.")
return
}
if try checkForRemoteDeletionOfLocalNode(loc, mirrorNode: self.mirror.find(guid)) {
return
}
let mir = self.mirror.find(guid)
let rem = self.remote.find(guid)
changed = true
out.append(try self.mergeNode(guid, localNode: loc, mirrorNode: mir, remoteNode: rem))
}
}
// Walk the mirror node's children. Any that are deleted on only one side might contribute
// orphans, so descend into those nodes' children on the other side.
if let expectedParent = mirror?.recordGUID, let mirrorChildren = mirror?.children {
try mirrorChildren.forEach { child in
let potentiallyDeleted = child.recordGUID
if seen.contains(potentiallyDeleted) || self.done.contains(potentiallyDeleted) {
return
}
let locallyDeleted = self.local.deleted.contains(potentiallyDeleted)
let remotelyDeleted = self.remote.deleted.contains(potentiallyDeleted)
if !locallyDeleted && !remotelyDeleted {
log.debug("Mirror child \(potentiallyDeleted) no longer here, but not deleted on either side: must be elsewhere.")
return
}
if locallyDeleted && remotelyDeleted {
log.debug("Mirror child \(potentiallyDeleted) was deleted both locally and remoted. We cool.")
self.merged.deleteFromMirror.insert(potentiallyDeleted)
self.merged.acceptLocalDeletion.insert(potentiallyDeleted)
self.merged.acceptRemoteDeletion.insert(potentiallyDeleted)
return
}
if locallyDeleted {
// See if the remote side still thinks this is the parent.
let parent = self.remote.parents[potentiallyDeleted]
if parent == nil || parent == expectedParent {
log.debug("Remote still thinks \(potentiallyDeleted) is here. Processing for orphans.")
if let parentOfOrphans = self.remote.find(potentiallyDeleted),
let orphans = try processRemoteOrphansForNode(parentOfOrphans) {
out.append(contentsOf: try self.relocateOrphansTo(result, orphans: orphans))
}
}
// Accept the local deletion, and make a note to apply it elsewhere.
self.merged.deleteFromMirror.insert(potentiallyDeleted)
self.merged.deleteRemotely.insert(potentiallyDeleted)
self.merged.acceptLocalDeletion.insert(potentiallyDeleted)
return
}
// Remotely deleted.
let parent = self.local.parents[potentiallyDeleted]
if parent == nil || parent == expectedParent {
log.debug("Local still thinks \(potentiallyDeleted) is here. Processing for orphans.")
if let parentOfOrphans = self.local.find(potentiallyDeleted),
let orphans = try processLocalOrphansForNode(parentOfOrphans) {
out.append(contentsOf: try self.relocateOrphansTo(result, orphans: orphans))
}
}
// Accept the remote deletion, and make a note to apply it elsewhere.
self.merged.deleteFromMirror.insert(potentiallyDeleted)
self.merged.deleteLocally.insert(potentiallyDeleted)
self.merged.acceptRemoteDeletion.insert(potentiallyDeleted)
}
}
log.debug("Setting \(result.guid)'s children to \(out.map { $0.guid }).")
result.mergedChildren = out
// If the child list didn't change, then we don't need .new.
if changed {
let newStructure = out.map { $0.asMergedTreeNode() }
result.structureState = MergeState.new(value: BookmarkTreeNode.folder(guid: result.guid, children: newStructure))
return
}
log.debug("Child list didn't change for \(result.guid). Keeping structure state \(result.structureState).")
}
fileprivate func resolveThreeWayValueConflict(_ guid: GUID) throws -> MergeState<BookmarkMirrorItem> {
// TODO
return try self.resolveTwoWayValueConflict(guid, localGUID: guid)
}
fileprivate func resolveTwoWayValueConflict(_ guid: GUID, localGUID: GUID) throws -> MergeState<BookmarkMirrorItem> {
// We don't check for all roots, because we might have to
// copy them to the mirror or buffer, so we need to pick
// a direction. The Places root is never uploaded.
if BookmarkRoots.RootGUID == guid {
log.debug("Two-way value merge on the root: always unaltered.")
return MergeState.unchanged
}
let localRecord = self.itemSources.local.getLocalItemWithGUID(localGUID).value.successValue
let remoteRecord = self.itemSources.buffer.getBufferItemWithGUID(guid).value.successValue
if let local = localRecord {
if let remote = remoteRecord {
// Two-way.
// If they're the same, take the remote record. It saves us having to rewrite
// local values to keep a remote GUID.
if local.sameAs(remote) {
log.debug("Local record \(local.guid) same as remote \(remote.guid). Taking remote.")
return MergeState.remote
}
log.debug("Comparing local (\(local.localModified ??? "0")) to remote (\(remote.serverModified)) clock for two-way value merge of \(guid).")
if let localModified = local.localModified, localModified > remote.serverModified {
// If we're taking the local record because it was modified, check to see
// whether the remote record has a creation date that we want to keep.
let dateAddedRemote = remote.dateAdded ?? remote.serverModified
if (local.dateAdded ?? UInt64.max) > dateAddedRemote {
return MergeState.new(value: local.copyWithDateAdded(dateAddedRemote))
}
return MergeState.local
}
return MergeState.remote
}
// No remote!
log.debug("Expected two-way merge for \(guid), but no remote item found.")
return MergeState.local
}
if let _ = remoteRecord {
// No local!
log.debug("Expected two-way merge for \(guid), but no local item found.")
return MergeState.remote
}
// Can't two-way merge with nothing!
log.error("Expected two-way merge for \(guid), but no local or remote item found!")
throw BookmarksMergeError()
}
// This will never be called with two primary .unknown values.
fileprivate func threeWayMerge(_ guid: GUID, localNode: BookmarkTreeNode, remoteNode: BookmarkTreeNode, mirrorNode: BookmarkTreeNode?) throws -> MergedTreeNode {
if mirrorNode == nil {
log.debug("Two-way merge for \(guid).")
} else {
log.debug("Three-way merge for \(guid).")
}
if remoteNode.isUnknown {
if localNode.isUnknown {
preconditionFailure("Two unknown nodes!")
}
log.debug("Taking local node in two/three-way merge: remote bafflingly unchanged.")
// TODO: value-unchanged
return MergedTreeNode.forLocal(localNode)
}
if localNode.isUnknown {
log.debug("Taking remote node in two/three-way merge: local bafflingly unchanged.")
// TODO: value-unchanged
return MergedTreeNode.forRemote(remoteNode)
}
let result = MergedTreeNode(guid: guid, mirror: mirrorNode)
result.local = localNode
result.remote = remoteNode
// Value merge. This applies regardless.
if localNode.isUnknown {
result.valueState = MergeState.remote
} else if remoteNode.isUnknown {
result.valueState = MergeState.local
} else {
if mirrorNode == nil {
result.valueState = try self.resolveTwoWayValueConflict(guid, localGUID: localNode.recordGUID)
} else {
result.valueState = try self.resolveThreeWayValueConflict(guid)
}
}
switch localNode {
case let .folder(_, localChildren):
if case let .folder(_, remoteChildren) = remoteNode {
// Structural merge.
if localChildren.sameElements(remoteChildren) {
// Great!
log.debug("Local and remote records have same children in two-way merge.")
result.structureState = MergeState.new(value: localNode) // TODO: what if it's the same as the mirror?
try self.mergeChildListsIntoMergedNode(result, fromLocal: localNode, remote: remoteNode, mirror: mirrorNode)
return result
}
// Merge the two folder lists.
// We know that each side is internally consistent: that is, each
// node in this list is present in the tree once only. But when we
// combine the two lists, we might be inadvertently duplicating a
// record that has already been, or will soon be, found in the other
// tree. We need to be careful to make sure that we don't feature
// a node in the tree more than once.
// Remember to check deletions.
log.debug("Local and remote records have different children. Merging.")
// Assume it'll be the same as the remote one; mergeChildListsIntoMergedNode
// sets this to New if the structure changes.
result.structureState = MergeState.remote
try self.mergeChildListsIntoMergedNode(result, fromLocal: localNode, remote: remoteNode, mirror: mirrorNode)
return result
}
case .nonFolder:
if case .nonFolder = remoteNode {
log.debug("Two non-folders with GUID \(guid) collide. Taking remote.")
return result
}
default:
break
}
// Otherwise, this must be a GUID collision between different types.
// TODO: Assign a new GUID to the local record
// but do not upload a deletion; these shouldn't merge.
log.debug("Remote and local records with same GUID \(guid) but different types. Consistency error.")
throw BookmarksMergeConsistencyError()
}
fileprivate func twoWayMerge(_ guid: GUID, localNode: BookmarkTreeNode, remoteNode: BookmarkTreeNode) throws -> MergedTreeNode {
return try self.threeWayMerge(guid, localNode: localNode, remoteNode: remoteNode, mirrorNode: nil)
}
fileprivate func unchangedIf(_ out: MergedTreeNode, original: BookmarkMirrorItem?, new: BookmarkMirrorItem?) -> MergedTreeNode {
guard let original = original, let new = new else {
return out
}
if new.sameAs(original) {
out.valueState = MergeState.unchanged
}
return out
}
fileprivate func takeLocalIfChanged(_ local: BookmarkTreeNode, mirror: BookmarkTreeNode?=nil) -> MergedTreeNode {
let guid = local.recordGUID
let localValues = self.itemSources.local.getLocalItemWithGUID(guid).value.successValue
let mirrorValues = self.itemSources.mirror.getMirrorItemWithGUID(guid).value.successValue
// We don't expect these to ever fail to exist.
assert(localValues != nil)
let merged = MergedTreeNode.forLocal(local, mirror: mirror)
return unchangedIf(merged, original: mirrorValues, new: localValues)
}
fileprivate func takeRemoteIfChanged(_ remote: BookmarkTreeNode, mirror: BookmarkTreeNode?=nil) -> MergedTreeNode {
let guid = remote.recordGUID
let remoteValues = self.itemSources.buffer.getBufferItemWithGUID(guid).value.successValue
let mirrorValues = self.itemSources.mirror.getMirrorItemWithGUID(guid).value.successValue
assert(remoteValues != nil)
let merged = MergedTreeNode.forRemote(remote, mirror: mirror)
return unchangedIf(merged, original: mirrorValues, new: remoteValues)
}
fileprivate var folderNameCache: [GUID: String?] = [:]
func getNameForFolder(_ folder: MergedTreeNode) throws -> String? {
if let name = self.folderNameCache[folder.guid] {
return name
}
let name = try self.fetchNameForFolder(folder)
self.folderNameCache[folder.guid] = name
return name
}
func fetchNameForFolder(_ folder: MergedTreeNode) throws -> String? {
switch folder.valueState {
case let .new(v):
return v.title
case .unchanged:
if let mirror = folder.mirror?.recordGUID,
let title = self.itemSources.mirror.getMirrorItemWithGUID(mirror).value.successValue?.title {
return title
}
case .remote:
if let remote = folder.remote?.recordGUID,
let title = self.itemSources.buffer.getBufferItemWithGUID(remote).value.successValue?.title {
return title
}
case .local:
if let local = folder.local?.recordGUID,
let title = self.itemSources.local.getLocalItemWithGUID(local).value.successValue?.title {
return title
}
case .unknown:
break
}
throw BookmarksMergeConsistencyError()
}
func relocateOrphansTo(_ mergedNode: MergedTreeNode, orphans: [MergedTreeNode]?) throws -> [MergedTreeNode] {
guard let orphans = orphans else {
return []
}
let parentName = try self.getNameForFolder(mergedNode)
return try orphans.map {
try self.relocateMergedTreeNode($0, parentID: mergedNode.guid, parentName: parentName)
}
}
func relocateMergedTreeNode(_ node: MergedTreeNode, parentID: GUID, parentName: String?) throws -> MergedTreeNode {
func copyWithMirrorItem(_ item: BookmarkMirrorItem?) throws -> MergedTreeNode {
guard let item = item else {
throw BookmarksMergeConsistencyError()
}
if item.parentID == parentID && item.parentName == parentName {
log.debug("Don't need to relocate \(node.guid)'s for value table.")
return node
}
log.debug("Relocating \(node.guid) to parent \(parentID).")
let n = MergedTreeNode(guid: node.guid, mirror: node.mirror)
n.local = node.local
n.remote = node.remote
n.mergedChildren = node.mergedChildren
n.structureState = node.structureState
n.valueState = .new(value: item.copyWithParentID(parentID, parentName: parentName))
return n
}
switch node.valueState {
case .unknown:
return node
case .unchanged:
return try copyWithMirrorItem(self.itemSources.mirror.getMirrorItemWithGUID(node.guid).value.successValue)
case .local:
return try copyWithMirrorItem(self.itemSources.local.getLocalItemWithGUID(node.guid).value.successValue)
case .remote:
return try copyWithMirrorItem(self.itemSources.buffer.getBufferItemWithGUID(node.guid).value.successValue)
case let .new(value):
return try copyWithMirrorItem(value)
}
}
// A helper that'll rewrite the resulting node's value to have the right parent.
func mergeNode(_ guid: GUID, intoFolder parentID: GUID, withParentName parentName: String?, localNode: BookmarkTreeNode?, mirrorNode: BookmarkTreeNode?, remoteNode: BookmarkTreeNode?) throws -> MergedTreeNode {
let m = try self.mergeNode(guid, localNode: localNode, mirrorNode: mirrorNode, remoteNode: remoteNode)
// We could check the parent pointers in the tree, but looking at the values themselves
// will catch any mismatches between the value and structure tables.
return try self.relocateMergedTreeNode(m, parentID: parentID, parentName: parentName)
}
// We'll end up looking at deletions and such as we go.
// TODO: accumulate deletions into the three buckets as we go.
//
// TODO: if a local or remote node is kept but put in a different folder, we actually
// need to generate a .new node, so we can take the parentid and parentNode that we
// must preserve.
func mergeNode(_ guid: GUID, localNode: BookmarkTreeNode?, mirrorNode: BookmarkTreeNode?, remoteNode: BookmarkTreeNode?) throws -> MergedTreeNode {
if let localGUID = localNode?.recordGUID {
log.verbose("Merging nodes with GUID \(guid). Local match is \(localGUID).")
} else {
log.verbose("Merging nodes with GUID \(guid). No local match.")
}
// TODO: if the local node has a different GUID, it's because we did a value-based
// merge. Make sure the local row with the differing local GUID doesn't
// stick around.
// We'll never get here with no nodes at all… right?
precondition((localNode != nil) || (mirrorNode != nil) || (remoteNode != nil))
// Note that not all of the input nodes must share a GUID: we might have decided, based on
// value comparison in an earlier recursive call, that a local node will be replaced by a
// remote node, and we'll need to mark the local GUID as a deletion, using its values and
// structure during our reconciling.
// But we will never have the mirror and remote differ.
precondition(nullOrMatch(remoteNode?.recordGUID, mirrorNode?.recordGUID))
precondition(nullOrMatch(remoteNode?.recordGUID, guid))
precondition(nullOrMatch(mirrorNode?.recordGUID, guid))
// Immediately mark this GUID -- and the local GUID, if it differs -- as done.
// This avoids repeated code in each conditional branch, and avoids the possibility of
// certain moves causing us to hit the same node again.
self.done.insert(guid)
if let otherGUID = localNode?.recordGUID, otherGUID != guid {
log.debug("Marking superseded local record \(otherGUID) as merged.")
self.done.insert(otherGUID)
self.duped.insert(otherGUID)
}
func takeRemoteAndMergeChildren(_ remote: BookmarkTreeNode, mirror: BookmarkTreeNode?=nil) throws -> MergedTreeNode {
let merged = self.takeRemoteIfChanged(remote, mirror: mirror)
if case .folder = remote {
log.debug("… and it's a folder. Taking remote children.")
try self.oneWayMergeChildListsIntoMergedNode(merged, fromRemote: remote)
}
return merged
}
func takeLocalAndMergeChildren(_ local: BookmarkTreeNode, mirror: BookmarkTreeNode?=nil) throws -> MergedTreeNode {
let merged = self.takeLocalIfChanged(local, mirror: mirror)
if case .folder = local {
log.debug("… and it's a folder. Taking local children.")
try self.oneWayMergeChildListsIntoMergedNode(merged, fromLocal: local)
}
return merged
}
func takeMirrorNode(_ mirror: BookmarkTreeNode) throws -> MergedTreeNode {
let merged = MergedTreeNode.forUnchanged(mirror)
if case .folder = mirror {
try self.takeMirrorChildrenInMergedNode(merged)
}
return merged
}
// If we ended up here with two unknowns, just proceed down the mirror.
// We know we have a mirror, else we'd have a non-unknown edge.
if (localNode?.isUnknown ?? true) && (remoteNode?.isUnknown ?? true) {
precondition(mirrorNode != nil)
log.verbose("Record \(guid) didn't change from mirror.")
self.done.insert(guid)
return try takeMirrorNode(mirrorNode!)
}
guard let mirrorNode = mirrorNode else {
// No mirror node: at most a two-way merge.
if let loc = localNode, !loc.isUnknown {
if let rem = remoteNode {
// Two-way merge; probably a disconnect-reconnect scenario.
return try self.twoWayMerge(guid, localNode: loc, remoteNode: rem)
}
// No remote. Node only exists locally.
// However! The children might be mentioned in the mirror or
// remote tree.
log.verbose("Node \(guid) only exists locally.")
return try takeLocalAndMergeChildren(loc)
}
// No local.
guard let rem = remoteNode, !rem.isUnknown else {
// No remote!
// This should not occur: we have preconditions above.
preconditionFailure("Unexpectedly got past our preconditions!")
}
// Node only exists remotely. Take it.
log.verbose("Node \(guid) only exists remotely.")
return try takeRemoteAndMergeChildren(rem)
}
// We have a mirror node.
if let loc = localNode, !loc.isUnknown {
if let rem = remoteNode, !rem.isUnknown {
log.debug("Both local and remote changes to mirror item \(guid). Resolving conflict.")
return try self.threeWayMerge(guid, localNode: loc, remoteNode: rem, mirrorNode: mirrorNode)
}
log.verbose("Local-only change to mirror item \(guid).")
return try takeLocalAndMergeChildren(loc, mirror: mirrorNode)
}
if let rem = remoteNode, !rem.isUnknown {
log.verbose("Remote-only change to mirror item \(guid).")
return try takeRemoteAndMergeChildren(rem, mirror: mirrorNode)
}
log.verbose("Record \(guid) didn't change from mirror.")
return try takeMirrorNode(mirrorNode)
}
func merge() -> Deferred<Maybe<BookmarksMergeResult>> {
return self.produceMergedTree()
>>== self.produceMergeResultFromMergedTree
}
func produceMergedTree() -> Deferred<Maybe<MergedTree>> {
// Don't ever do this work twice.
if self.mergeAttempted {
return deferMaybe(self.merged)
}
// Both local and remote should reach a single root when overlayed. If not, it means that
// the tree is inconsistent -- there isn't a full tree present either on the server (or
// local) or in the mirror, or the changes aren't congruent in some way. If we reach this
// state, we cannot proceed.
//
// This is assert()ed in the initializer, too, so that we crash hard and early in developer
// builds and tests.
if !self.local.isFullyRootedIn(self.mirror) {
log.warning("Local bookmarks not fully rooted when overlayed on mirror. This is most unusual.")
return deferMaybe(BookmarksMergeErrorTreeIsUnrooted(roots: self.local.subtreeGUIDs))
}
if !self.remote.isFullyRootedIn(mirror) {