Skip to main content

rouchdb_core/
merge.rs

1/// Revision tree merge algorithm.
2///
3/// Implements the same logic as PouchDB's `pouchdb-merge` module:
4/// - Merge incoming revision paths into an existing tree
5/// - Determine the winning revision deterministically
6/// - Stem (prune) old revisions beyond a configurable limit
7
8use crate::document::Revision;
9use crate::rev_tree::{collect_leaves, RevNode, RevPath, RevStatus, RevTree};
10
11/// Result of merging a new path into the tree.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum MergeResult {
14    /// The path extended an existing branch (normal edit).
15    NewLeaf,
16    /// The path created a new branch (conflict).
17    NewBranch,
18    /// The path's leaf already existed in the tree (duplicate/no-op).
19    InternalNode,
20}
21
22/// Merge a new revision path into the existing tree.
23///
24/// Returns the updated tree and a `MergeResult` indicating what happened.
25pub fn merge_tree(tree: &RevTree, new_path: &RevPath, rev_limit: u64) -> (RevTree, MergeResult) {
26    let mut result_tree = tree.clone();
27    let merge_result = do_merge(&mut result_tree, new_path);
28
29    // Apply stemming if we have a rev_limit
30    if rev_limit > 0 {
31        let _stemmed = stem(&mut result_tree, rev_limit);
32    }
33
34    (result_tree, merge_result)
35}
36
37/// Core merge logic. Tries to merge `new_path` into `tree`, modifying it
38/// in place.
39fn do_merge(tree: &mut RevTree, new_path: &RevPath) -> MergeResult {
40    // Try to merge the new path into each existing root
41    for existing in tree.iter_mut() {
42        let result = try_merge_path(existing, new_path);
43        if let Some(merge_result) = result {
44            return merge_result;
45        }
46    }
47
48    // No overlap found — this is a completely new branch
49    tree.push(new_path.clone());
50    MergeResult::NewBranch
51}
52
53/// Try to merge `new_path` into a single existing `RevPath`.
54///
55/// Returns `None` if the paths don't share any common ancestor (no overlap),
56/// meaning the new path should be tried against other roots or added as a
57/// new root.
58fn try_merge_path(existing: &mut RevPath, new_path: &RevPath) -> Option<MergeResult> {
59    // Find the overlap point between the two paths
60    let overlap = find_overlap(existing, new_path);
61
62    match overlap {
63        None => None, // No common point, can't merge here
64        Some(OverlapInfo {
65            existing_node_path,
66            new_remainder,
67            is_exact_match,
68        }) => {
69            if is_exact_match && new_remainder.is_empty() {
70                // The new path's leaf already exists in the tree
71                return Some(MergeResult::InternalNode);
72            }
73
74            // Navigate to the overlap point and graft the new nodes
75            let target = navigate_to_mut(&mut existing.tree, &existing_node_path);
76
77            if new_remainder.is_empty() {
78                // Leaf already exists
79                Some(MergeResult::InternalNode)
80            } else {
81                // Check if this extends an existing branch or creates a new one
82                let result = graft_nodes(target, &new_remainder);
83                Some(result)
84            }
85        }
86    }
87}
88
89struct OverlapInfo {
90    /// Path of indices to navigate from the existing root to the overlap node.
91    existing_node_path: Vec<usize>,
92    /// Remaining new nodes to graft after the overlap point.
93    new_remainder: Vec<RevNode>,
94    /// Whether the overlap was an exact hash match (vs. positional).
95    is_exact_match: bool,
96}
97
98/// Find where `new_path` overlaps with `existing`.
99fn find_overlap(existing: &RevPath, new_path: &RevPath) -> Option<OverlapInfo> {
100    // Flatten the new path into a linear chain of hashes with positions
101    let new_chain = flatten_chain(&new_path.tree, new_path.pos);
102
103    // Try to find any node in the new chain that exists in the existing tree
104    for (i, (new_pos, new_hash)) in new_chain.iter().enumerate() {
105        if let Some(path_indices) = find_node_path(&existing.tree, existing.pos, *new_pos, new_hash)
106        {
107            // Build the remainder: nodes in the new chain after this overlap point
108            let remainder = build_remainder_from_chain(&new_chain, i, &new_path.tree, new_path.pos);
109
110            return Some(OverlapInfo {
111                existing_node_path: path_indices,
112                new_remainder: remainder,
113                is_exact_match: true,
114            });
115        }
116    }
117
118    // Check if the new path starts right after where the existing tree ends,
119    // or if there's a positional overlap we can use
120    // Check if the new path's root is a child-level continuation of any leaf
121    let existing_leaves = collect_leaf_positions(existing);
122    let new_root_pos = new_path.pos;
123    let new_root_hash = &new_path.tree.hash;
124
125    // Check if the new path starts exactly where an existing leaf is
126    for (leaf_pos, leaf_hash, leaf_path) in &existing_leaves {
127        // Does the new chain start with this leaf's hash at this position?
128        if *leaf_pos == new_root_pos && leaf_hash == new_root_hash {
129            let remainder = if new_path.tree.children.is_empty() {
130                vec![]
131            } else {
132                new_path.tree.children.clone()
133            };
134            return Some(OverlapInfo {
135                existing_node_path: leaf_path.clone(),
136                new_remainder: remainder,
137                is_exact_match: true,
138            });
139        }
140    }
141
142    None
143}
144
145/// Flatten a tree node into a linear chain of (pos, hash) pairs.
146fn flatten_chain(node: &RevNode, start_pos: u64) -> Vec<(u64, String)> {
147    let mut chain = Vec::new();
148    fn walk(node: &RevNode, pos: u64, chain: &mut Vec<(u64, String)>) {
149        chain.push((pos, node.hash.clone()));
150        // Follow first child only (linear chain for the new path)
151        if let Some(child) = node.children.first() {
152            walk(child, pos + 1, chain);
153        }
154    }
155    walk(node, start_pos, &mut chain);
156    chain
157}
158
159/// Find the index path to a node with the given position and hash.
160fn find_node_path(
161    node: &RevNode,
162    current_pos: u64,
163    target_pos: u64,
164    target_hash: &str,
165) -> Option<Vec<usize>> {
166    if current_pos == target_pos && node.hash == target_hash {
167        return Some(vec![]);
168    }
169
170    for (i, child) in node.children.iter().enumerate() {
171        if let Some(mut path) = find_node_path(child, current_pos + 1, target_pos, target_hash) {
172            path.insert(0, i);
173            return Some(path);
174        }
175    }
176
177    None
178}
179
180/// Collect all leaf nodes with their positions and index paths.
181fn collect_leaf_positions(path: &RevPath) -> Vec<(u64, String, Vec<usize>)> {
182    let mut leaves = Vec::new();
183    fn walk(
184        node: &RevNode,
185        pos: u64,
186        current_path: &mut Vec<usize>,
187        leaves: &mut Vec<(u64, String, Vec<usize>)>,
188    ) {
189        if node.children.is_empty() {
190            leaves.push((pos, node.hash.clone(), current_path.clone()));
191        }
192        for (i, child) in node.children.iter().enumerate() {
193            current_path.push(i);
194            walk(child, pos + 1, current_path, leaves);
195            current_path.pop();
196        }
197    }
198    let mut current = Vec::new();
199    walk(&path.tree, path.pos, &mut current, &mut leaves);
200    leaves
201}
202
203/// Build the remaining nodes after the overlap point from the new chain.
204fn build_remainder_from_chain(
205    _chain: &[(u64, String)],
206    overlap_index: usize,
207    original_tree: &RevNode,
208    _original_pos: u64,
209) -> Vec<RevNode> {
210    // Navigate to the overlap point in the original tree, then return
211    // everything after it
212    let depth_to_overlap = overlap_index;
213
214    fn get_subtree_at_depth(node: &RevNode, depth: usize) -> Option<&RevNode> {
215        if depth == 0 {
216            return Some(node);
217        }
218        if let Some(child) = node.children.first() {
219            get_subtree_at_depth(child, depth - 1)
220        } else {
221            None
222        }
223    }
224
225    if let Some(overlap_node) = get_subtree_at_depth(original_tree, depth_to_overlap) {
226        overlap_node.children.clone()
227    } else {
228        vec![]
229    }
230}
231
232/// Navigate to a node in the tree using a path of child indices.
233fn navigate_to_mut<'a>(node: &'a mut RevNode, path: &[usize]) -> &'a mut RevNode {
234    let mut current = node;
235    for &idx in path {
236        current = &mut current.children[idx];
237    }
238    current
239}
240
241/// Graft new nodes onto a target node. Returns whether this created a new
242/// branch (conflict), extended an existing one, or was a no-op.
243fn graft_nodes(target: &mut RevNode, new_nodes: &[RevNode]) -> MergeResult {
244    let mut is_new_branch = false;
245    let mut added_anything = false;
246
247    for new_node in new_nodes {
248        // Check if a child with this hash already exists
249        let existing_child = target
250            .children
251            .iter_mut()
252            .find(|c| c.hash == new_node.hash);
253
254        match existing_child {
255            Some(existing) => {
256                // Recursively merge children
257                for grandchild in &new_node.children {
258                    let sub_nodes = vec![grandchild.clone()];
259                    let result = graft_nodes(existing, &sub_nodes);
260                    match result {
261                        MergeResult::NewBranch => {
262                            is_new_branch = true;
263                            added_anything = true;
264                        }
265                        MergeResult::NewLeaf => {
266                            added_anything = true;
267                        }
268                        MergeResult::InternalNode => {}
269                    }
270                }
271            }
272            None => {
273                // New child — this is either extending a leaf or creating a branch
274                if !target.children.is_empty() {
275                    is_new_branch = true;
276                }
277                target.children.push(new_node.clone());
278                added_anything = true;
279            }
280        }
281    }
282
283    if !added_anything {
284        MergeResult::InternalNode
285    } else if is_new_branch {
286        MergeResult::NewBranch
287    } else {
288        MergeResult::NewLeaf
289    }
290}
291
292// ---------------------------------------------------------------------------
293// Winning revision
294// ---------------------------------------------------------------------------
295
296/// Determine the winning revision of a document.
297///
298/// CouchDB's deterministic algorithm:
299/// 1. Non-deleted leaves win over deleted leaves
300/// 2. Higher position (generation) wins
301/// 3. Lexicographically greater hash breaks ties
302///
303/// Every replica independently arrives at the same winner.
304pub fn winning_rev(tree: &RevTree) -> Option<Revision> {
305    let leaves = collect_leaves(tree);
306    leaves.first().map(|l| Revision::new(l.pos, l.hash.clone()))
307}
308
309/// Check if the document's winning revision is deleted.
310pub fn is_deleted(tree: &RevTree) -> bool {
311    collect_leaves(tree)
312        .first()
313        .map(|l| l.deleted)
314        .unwrap_or(false)
315}
316
317/// Collect all conflicting (non-winning, non-deleted) leaf revisions.
318pub fn collect_conflicts(tree: &RevTree) -> Vec<Revision> {
319    let leaves = collect_leaves(tree);
320    leaves
321        .iter()
322        .skip(1) // skip the winner
323        .filter(|l| !l.deleted)
324        .map(|l| Revision::new(l.pos, l.hash.clone()))
325        .collect()
326}
327
328// ---------------------------------------------------------------------------
329// Stemming (pruning old revisions)
330// ---------------------------------------------------------------------------
331
332/// Prune revisions beyond `depth` from each leaf. Returns the list of
333/// revision hashes that were removed.
334pub fn stem(tree: &mut RevTree, depth: u64) -> Vec<String> {
335    let mut stemmed = Vec::new();
336
337    for path in tree.iter_mut() {
338        let s = stem_path(path, depth);
339        stemmed.extend(s);
340    }
341
342    // Remove any paths that became empty
343    tree.retain(|p| !is_empty_node(&p.tree));
344
345    stemmed
346}
347
348/// Stem a single path, adjusting `pos` if the root gets pruned.
349fn stem_path(path: &mut RevPath, depth: u64) -> Vec<String> {
350    let mut stemmed = Vec::new();
351
352    // Find the maximum depth of any leaf
353    fn max_depth(node: &RevNode) -> u64 {
354        if node.children.is_empty() {
355            return 0;
356        }
357        node.children
358            .iter()
359            .map(|c| 1 + max_depth(c))
360            .max()
361            .unwrap_or(0)
362    }
363
364    let tree_depth = max_depth(&path.tree);
365
366    if tree_depth < depth {
367        return stemmed; // Nothing to stem
368    }
369
370    // We need to remove nodes from the root until the deepest path
371    // is at most `depth` long
372    let levels_to_remove = tree_depth - depth + 1;
373
374    for _ in 0..levels_to_remove {
375        if path.tree.children.len() <= 1 {
376            stemmed.push(path.tree.hash.clone());
377            if let Some(child) = path.tree.children.pop() {
378                path.tree = child;
379                path.pos += 1;
380            } else {
381                // Tree is now empty
382                break;
383            }
384        } else {
385            // Can't stem past a branch point
386            break;
387        }
388    }
389
390    stemmed
391}
392
393fn is_empty_node(node: &RevNode) -> bool {
394    node.hash.is_empty() && node.children.is_empty()
395}
396
397// ---------------------------------------------------------------------------
398// Utility: find the latest available revision on a branch
399// ---------------------------------------------------------------------------
400
401/// Find the latest available (non-missing) revision following the branch
402/// that contains `rev`. If `rev` itself is available, returns it. Otherwise
403/// walks toward the leaf looking for the closest available revision.
404pub fn latest_rev(tree: &RevTree, pos: u64, hash: &str) -> Option<Revision> {
405    for path in tree {
406        if let Some(rev) = find_latest_in_node(&path.tree, path.pos, pos, hash) {
407            return Some(rev);
408        }
409    }
410    None
411}
412
413fn find_latest_in_node(
414    node: &RevNode,
415    current_pos: u64,
416    target_pos: u64,
417    target_hash: &str,
418) -> Option<Revision> {
419    if current_pos == target_pos && node.hash == target_hash {
420        // Found the target. If it's available, return it.
421        // Otherwise, walk to the first available leaf.
422        if node.status == RevStatus::Available {
423            return Some(Revision::new(current_pos, node.hash.clone()));
424        }
425        return find_first_available_leaf(node, current_pos);
426    }
427
428    for child in &node.children {
429        if let Some(rev) = find_latest_in_node(child, current_pos + 1, target_pos, target_hash) {
430            return Some(rev);
431        }
432    }
433
434    None
435}
436
437fn find_first_available_leaf(node: &RevNode, pos: u64) -> Option<Revision> {
438    if node.children.is_empty() {
439        if node.status == RevStatus::Available {
440            return Some(Revision::new(pos, node.hash.clone()));
441        }
442        return None;
443    }
444
445    for child in &node.children {
446        if let Some(rev) = find_first_available_leaf(child, pos + 1) {
447            return Some(rev);
448        }
449    }
450    None
451}
452
453// ---------------------------------------------------------------------------
454// Tests
455// ---------------------------------------------------------------------------
456
457#[cfg(test)]
458mod tests {
459    use super::*;
460    use crate::rev_tree::{build_path_from_revs, NodeOpts, RevNode, RevPath};
461
462    fn leaf(hash: &str) -> RevNode {
463        RevNode {
464            hash: hash.into(),
465            status: RevStatus::Available,
466            opts: NodeOpts::default(),
467            children: vec![],
468        }
469    }
470
471    fn deleted_leaf(hash: &str) -> RevNode {
472        RevNode {
473            hash: hash.into(),
474            status: RevStatus::Available,
475            opts: NodeOpts { deleted: true },
476            children: vec![],
477        }
478    }
479
480    fn node(hash: &str, children: Vec<RevNode>) -> RevNode {
481        RevNode {
482            hash: hash.into(),
483            status: RevStatus::Available,
484            opts: NodeOpts::default(),
485            children,
486        }
487    }
488
489    fn simple_tree() -> RevTree {
490        // 1-a -> 2-b -> 3-c
491        vec![RevPath {
492            pos: 1,
493            tree: node("a", vec![node("b", vec![leaf("c")])]),
494        }]
495    }
496
497    // --- winning_rev ---
498
499    #[test]
500    fn winning_rev_simple() {
501        let tree = simple_tree();
502        let winner = winning_rev(&tree).unwrap();
503        assert_eq!(winner.pos, 3);
504        assert_eq!(winner.hash, "c");
505    }
506
507    #[test]
508    fn winning_rev_conflict_picks_higher_hash() {
509        // 1-a -> 2-b
510        //     -> 2-c
511        let tree = vec![RevPath {
512            pos: 1,
513            tree: node("a", vec![leaf("b"), leaf("c")]),
514        }];
515        let winner = winning_rev(&tree).unwrap();
516        assert_eq!(winner.hash, "c"); // "c" > "b" lexicographically
517    }
518
519    #[test]
520    fn winning_rev_conflict_prefers_longer() {
521        // 1-a -> 2-b -> 3-d
522        //     -> 2-c
523        let tree = vec![RevPath {
524            pos: 1,
525            tree: node("a", vec![node("b", vec![leaf("d")]), leaf("c")]),
526        }];
527        let winner = winning_rev(&tree).unwrap();
528        assert_eq!(winner.pos, 3);
529        assert_eq!(winner.hash, "d"); // pos 3 beats pos 2
530    }
531
532    #[test]
533    fn winning_rev_non_deleted_beats_deleted() {
534        // 1-a -> 2-b (non-deleted)
535        //     -> 2-z (deleted) — z > b but deleted loses
536        let tree = vec![RevPath {
537            pos: 1,
538            tree: node("a", vec![leaf("b"), deleted_leaf("z")]),
539        }];
540        let winner = winning_rev(&tree).unwrap();
541        assert_eq!(winner.hash, "b");
542    }
543
544    // --- collect_conflicts ---
545
546    #[test]
547    fn no_conflicts_on_linear() {
548        let tree = simple_tree();
549        assert!(collect_conflicts(&tree).is_empty());
550    }
551
552    #[test]
553    fn conflicts_on_branches() {
554        // 1-a -> 2-b, 2-c
555        let tree = vec![RevPath {
556            pos: 1,
557            tree: node("a", vec![leaf("b"), leaf("c")]),
558        }];
559        let conflicts = collect_conflicts(&tree);
560        assert_eq!(conflicts.len(), 1);
561        assert_eq!(conflicts[0].hash, "b"); // loser
562    }
563
564    // --- is_deleted ---
565
566    #[test]
567    fn is_deleted_false_for_normal() {
568        assert!(!is_deleted(&simple_tree()));
569    }
570
571    #[test]
572    fn is_deleted_true_when_winner_deleted() {
573        let tree = vec![RevPath {
574            pos: 1,
575            tree: deleted_leaf("a"),
576        }];
577        assert!(is_deleted(&tree));
578    }
579
580    // --- merge_tree ---
581
582    #[test]
583    fn merge_extends_linear_chain() {
584        // Start: 1-a -> 2-b
585        let tree = vec![RevPath {
586            pos: 1,
587            tree: node("a", vec![leaf("b")]),
588        }];
589
590        // Add: 3-c extending from 2-b
591        let new_path = build_path_from_revs(
592            3,
593            &["c".into(), "b".into()],
594            NodeOpts::default(),
595            RevStatus::Available,
596        );
597
598        let (merged, result) = merge_tree(&tree, &new_path, 1000);
599        assert_eq!(result, MergeResult::NewLeaf);
600
601        let winner = winning_rev(&merged).unwrap();
602        assert_eq!(winner.pos, 3);
603        assert_eq!(winner.hash, "c");
604    }
605
606    #[test]
607    fn merge_creates_conflict_branch() {
608        // Start: 1-a -> 2-b
609        let tree = vec![RevPath {
610            pos: 1,
611            tree: node("a", vec![leaf("b")]),
612        }];
613
614        // Add: 2-c branching from 1-a (conflict)
615        let new_path = build_path_from_revs(
616            2,
617            &["c".into(), "a".into()],
618            NodeOpts::default(),
619            RevStatus::Available,
620        );
621
622        let (merged, result) = merge_tree(&tree, &new_path, 1000);
623        assert_eq!(result, MergeResult::NewBranch);
624
625        let conflicts = collect_conflicts(&merged);
626        assert_eq!(conflicts.len(), 1);
627    }
628
629    #[test]
630    fn merge_duplicate_is_internal_node() {
631        // Start: 1-a -> 2-b
632        let tree = vec![RevPath {
633            pos: 1,
634            tree: node("a", vec![leaf("b")]),
635        }];
636
637        // Add: 2-b (already exists)
638        let new_path = build_path_from_revs(
639            2,
640            &["b".into(), "a".into()],
641            NodeOpts::default(),
642            RevStatus::Available,
643        );
644
645        let (_merged, result) = merge_tree(&tree, &new_path, 1000);
646        assert_eq!(result, MergeResult::InternalNode);
647    }
648
649    #[test]
650    fn merge_disjoint_creates_new_root() {
651        // Start: 1-a -> 2-b
652        let tree = vec![RevPath {
653            pos: 1,
654            tree: node("a", vec![leaf("b")]),
655        }];
656
657        // Add: 1-x -> 2-y (completely disjoint)
658        let new_path = build_path_from_revs(
659            2,
660            &["y".into(), "x".into()],
661            NodeOpts::default(),
662            RevStatus::Available,
663        );
664
665        let (merged, result) = merge_tree(&tree, &new_path, 1000);
666        assert_eq!(result, MergeResult::NewBranch);
667        assert_eq!(merged.len(), 2); // Two separate roots
668    }
669
670    // --- stem ---
671
672    #[test]
673    fn stem_prunes_old_revisions() {
674        // 1-a -> 2-b -> 3-c -> 4-d -> 5-e
675        let mut tree = vec![RevPath {
676            pos: 1,
677            tree: node(
678                "a",
679                vec![node("b", vec![node("c", vec![node("d", vec![leaf("e")])])])],
680            ),
681        }];
682
683        let stemmed = stem(&mut tree, 3);
684        assert!(!stemmed.is_empty());
685
686        // Tree should now start at a higher position
687        assert!(tree[0].pos > 1);
688
689        // Leaf should still be present
690        let leaves = collect_leaves(&tree);
691        assert_eq!(leaves[0].hash, "e");
692    }
693
694    #[test]
695    fn stem_stops_at_branch_point() {
696        // 1-a -> 2-b -> 3-c
697        //            -> 3-d
698        let mut tree = vec![RevPath {
699            pos: 1,
700            tree: node("a", vec![node("b", vec![leaf("c"), leaf("d")])]),
701        }];
702
703        // Even with depth=1, cannot stem past the branch point at 2-b
704        let stemmed = stem(&mut tree, 1);
705        // Stemmed should remove at most 1-a (stop at branch)
706        // Actually, can stem 1-a since 2-b has multiple children
707        // but 2-b cannot be stemmed because it has >1 child
708        assert!(stemmed.len() <= 1);
709    }
710
711    #[test]
712    fn stem_short_tree_unchanged() {
713        // 1-a -> 2-b (depth 1, limit 3 => nothing to prune)
714        let mut tree = vec![RevPath {
715            pos: 1,
716            tree: node("a", vec![leaf("b")]),
717        }];
718
719        let stemmed = stem(&mut tree, 3);
720        assert!(stemmed.is_empty());
721        assert_eq!(tree[0].pos, 1);
722    }
723
724    // --- latest_rev ---
725
726    #[test]
727    fn latest_rev_finds_available_node() {
728        let tree = simple_tree(); // 1-a -> 2-b -> 3-c
729        let rev = latest_rev(&tree, 3, "c").unwrap();
730        assert_eq!(rev.pos, 3);
731        assert_eq!(rev.hash, "c");
732    }
733
734    #[test]
735    fn latest_rev_walks_to_leaf_from_missing() {
736        // 1-a(missing) -> 2-b(available)
737        let tree = vec![RevPath {
738            pos: 1,
739            tree: RevNode {
740                hash: "a".into(),
741                status: RevStatus::Missing,
742                opts: NodeOpts::default(),
743                children: vec![leaf("b")],
744            },
745        }];
746        let rev = latest_rev(&tree, 1, "a").unwrap();
747        assert_eq!(rev.pos, 2);
748        assert_eq!(rev.hash, "b");
749    }
750
751    #[test]
752    fn latest_rev_none_for_nonexistent() {
753        let tree = simple_tree();
754        assert!(latest_rev(&tree, 5, "zzz").is_none());
755    }
756
757    #[test]
758    fn latest_rev_finds_internal_node() {
759        let tree = simple_tree(); // 1-a -> 2-b -> 3-c
760        let rev = latest_rev(&tree, 2, "b").unwrap();
761        assert_eq!(rev.pos, 2);
762        assert_eq!(rev.hash, "b");
763    }
764
765    #[test]
766    fn latest_rev_on_empty_tree() {
767        let tree: RevTree = vec![];
768        assert!(latest_rev(&tree, 1, "a").is_none());
769    }
770
771    // --- merge edge cases ---
772
773    #[test]
774    fn merge_exact_root_match_no_children() {
775        // Tree: 1-a (single node)
776        let tree = vec![RevPath {
777            pos: 1,
778            tree: leaf("a"),
779        }];
780
781        // Add same node: 1-a
782        let new_path = RevPath {
783            pos: 1,
784            tree: leaf("a"),
785        };
786
787        let (_, result) = merge_tree(&tree, &new_path, 1000);
788        assert_eq!(result, MergeResult::InternalNode);
789    }
790
791    #[test]
792    fn merge_same_branch_extends_deeper() {
793        // Tree: 1-a -> 2-b -> 3-c
794        let tree = simple_tree();
795
796        // Add: 1-a -> 2-b -> 3-c -> 4-d (full ancestry extending leaf)
797        let new_path = build_path_from_revs(
798            4,
799            &["d".into(), "c".into(), "b".into(), "a".into()],
800            NodeOpts::default(),
801            RevStatus::Available,
802        );
803
804        let (merged, result) = merge_tree(&tree, &new_path, 1000);
805        assert_eq!(result, MergeResult::NewLeaf);
806        let winner = winning_rev(&merged).unwrap();
807        assert_eq!(winner.pos, 4);
808        assert_eq!(winner.hash, "d");
809    }
810
811    #[test]
812    fn winning_rev_empty_tree() {
813        let tree: RevTree = vec![];
814        assert!(winning_rev(&tree).is_none());
815    }
816
817    #[test]
818    fn is_deleted_empty_tree() {
819        let tree: RevTree = vec![];
820        assert!(!is_deleted(&tree));
821    }
822
823    #[test]
824    fn collect_conflicts_deleted_leaves_excluded() {
825        // 1-a -> 2-b (normal), 2-c (deleted)
826        // Winner: 2-b, conflict: none (2-c is deleted)
827        let tree = vec![RevPath {
828            pos: 1,
829            tree: node("a", vec![leaf("b"), deleted_leaf("c")]),
830        }];
831        let conflicts = collect_conflicts(&tree);
832        assert!(conflicts.is_empty());
833    }
834}