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