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/// Maximum number of edges from `node` to its deepest descendant leaf.
329fn max_depth(node: &RevNode) -> u64 {
330    if node.children.is_empty() {
331        return 0;
332    }
333    node.children
334        .iter()
335        .map(|c| 1 + max_depth(c))
336        .max()
337        .unwrap_or(0)
338}
339
340/// Prune revisions beyond `depth` from each leaf. Returns the list of
341/// revision hashes that were removed.
342///
343/// Stemming is applied per branch: when a branch point sits above the cut
344/// line, the common ancestor is dropped and each child subtree is re-rooted
345/// into its own `RevPath`, mirroring `pouchdb-merge`. This is why a `RevTree`
346/// is a list of roots.
347pub fn stem(tree: &mut RevTree, depth: u64) -> Vec<String> {
348    let mut stemmed = Vec::new();
349    let mut new_roots: RevTree = Vec::new();
350    for path in tree.drain(..) {
351        new_roots.extend(stem_path(path, depth, &mut stemmed));
352    }
353    // Remove any paths that became empty
354    new_roots.retain(|p| !is_empty_node(&p.tree));
355    *tree = new_roots;
356    stemmed
357}
358
359/// Stem one rooted path, returning one or more re-rooted paths so every
360/// root-to-leaf chain keeps at most `depth` revisions.
361fn stem_path(path: RevPath, depth: u64, stemmed: &mut Vec<String>) -> Vec<RevPath> {
362    let mut pos = path.pos;
363    let mut node = path.tree;
364    loop {
365        if max_depth(&node) < depth {
366            // The deepest leaf is already within the limit — keep as-is.
367            return vec![RevPath { pos, tree: node }];
368        }
369        if node.children.len() <= 1 {
370            // Linear prefix: drop the root and descend into the only child.
371            stemmed.push(node.hash.clone());
372            match node.children.pop() {
373                Some(child) => {
374                    node = child;
375                    pos += 1;
376                }
377                None => return vec![], // Tree emptied
378            }
379        } else {
380            // Branch point above the cut line: drop it and re-root each child,
381            // stemming each subtree independently.
382            stemmed.push(node.hash.clone());
383            let children = std::mem::take(&mut node.children);
384            let mut out = Vec::new();
385            for child in children {
386                out.extend(stem_path(
387                    RevPath {
388                        pos: pos + 1,
389                        tree: child,
390                    },
391                    depth,
392                    stemmed,
393                ));
394            }
395            return out;
396        }
397    }
398}
399
400fn is_empty_node(node: &RevNode) -> bool {
401    node.hash.is_empty() && node.children.is_empty()
402}
403
404/// Find the winning leaf revision that descends from `(pos, hash)`.
405///
406/// Used for `latest=true`: walk to the tip of the requested rev's branch.
407/// If the rev is itself a leaf it is returned; among multiple descendant
408/// leaves the deterministic winner (non-deleted > higher generation > higher
409/// hash) is chosen. Returns `None` if the rev is not present in the tree.
410pub fn latest_leaf(tree: &RevTree, pos: u64, hash: &str) -> Option<Revision> {
411    fn find_node<'a>(
412        node: &'a RevNode,
413        cur: u64,
414        tpos: u64,
415        thash: &str,
416    ) -> Option<(&'a RevNode, u64)> {
417        if cur == tpos && node.hash == thash {
418            return Some((node, cur));
419        }
420        for c in &node.children {
421            if let Some(found) = find_node(c, cur + 1, tpos, thash) {
422                return Some(found);
423            }
424        }
425        None
426    }
427
428    fn collect(node: &RevNode, pos: u64, out: &mut Vec<(u64, String, bool)>) {
429        if node.children.is_empty() {
430            out.push((pos, node.hash.clone(), node.opts.deleted));
431        } else {
432            for c in &node.children {
433                collect(c, pos + 1, out);
434            }
435        }
436    }
437
438    for path in tree {
439        if let Some((node, node_pos)) = find_node(&path.tree, path.pos, pos, hash) {
440            let mut leaves: Vec<(u64, String, bool)> = Vec::new();
441            collect(node, node_pos, &mut leaves);
442            // Winner order: non-deleted first, then highest pos, then hash desc.
443            leaves.sort_by(|a, b| {
444                a.2.cmp(&b.2)
445                    .then_with(|| b.0.cmp(&a.0))
446                    .then_with(|| b.1.cmp(&a.1))
447            });
448            return leaves
449                .into_iter()
450                .next()
451                .map(|(p, h, _)| Revision::new(p, h));
452        }
453    }
454    None
455}
456
457// ---------------------------------------------------------------------------
458// Utility: find the latest available revision on a branch
459// ---------------------------------------------------------------------------
460
461/// Find the latest available (non-missing) revision following the branch
462/// that contains `rev`. If `rev` itself is available, returns it. Otherwise
463/// walks toward the leaf looking for the closest available revision.
464pub fn latest_rev(tree: &RevTree, pos: u64, hash: &str) -> Option<Revision> {
465    for path in tree {
466        if let Some(rev) = find_latest_in_node(&path.tree, path.pos, pos, hash) {
467            return Some(rev);
468        }
469    }
470    None
471}
472
473fn find_latest_in_node(
474    node: &RevNode,
475    current_pos: u64,
476    target_pos: u64,
477    target_hash: &str,
478) -> Option<Revision> {
479    if current_pos == target_pos && node.hash == target_hash {
480        // Found the target. If it's available, return it.
481        // Otherwise, walk to the first available leaf.
482        if node.status == RevStatus::Available {
483            return Some(Revision::new(current_pos, node.hash.clone()));
484        }
485        return find_first_available_leaf(node, current_pos);
486    }
487
488    for child in &node.children {
489        if let Some(rev) = find_latest_in_node(child, current_pos + 1, target_pos, target_hash) {
490            return Some(rev);
491        }
492    }
493
494    None
495}
496
497fn find_first_available_leaf(node: &RevNode, pos: u64) -> Option<Revision> {
498    if node.children.is_empty() {
499        if node.status == RevStatus::Available {
500            return Some(Revision::new(pos, node.hash.clone()));
501        }
502        return None;
503    }
504
505    for child in &node.children {
506        if let Some(rev) = find_first_available_leaf(child, pos + 1) {
507            return Some(rev);
508        }
509    }
510    None
511}
512
513// ---------------------------------------------------------------------------
514// Tests
515// ---------------------------------------------------------------------------
516
517#[cfg(test)]
518mod tests {
519    use super::*;
520    use crate::rev_tree::{NodeOpts, RevNode, RevPath, build_path_from_revs};
521
522    fn leaf(hash: &str) -> RevNode {
523        RevNode {
524            hash: hash.into(),
525            status: RevStatus::Available,
526            opts: NodeOpts::default(),
527            children: vec![],
528        }
529    }
530
531    fn deleted_leaf(hash: &str) -> RevNode {
532        RevNode {
533            hash: hash.into(),
534            status: RevStatus::Available,
535            opts: NodeOpts { deleted: true },
536            children: vec![],
537        }
538    }
539
540    fn node(hash: &str, children: Vec<RevNode>) -> RevNode {
541        RevNode {
542            hash: hash.into(),
543            status: RevStatus::Available,
544            opts: NodeOpts::default(),
545            children,
546        }
547    }
548
549    fn simple_tree() -> RevTree {
550        // 1-a -> 2-b -> 3-c
551        vec![RevPath {
552            pos: 1,
553            tree: node("a", vec![node("b", vec![leaf("c")])]),
554        }]
555    }
556
557    // --- winning_rev ---
558
559    #[test]
560    fn winning_rev_simple() {
561        let tree = simple_tree();
562        let winner = winning_rev(&tree).unwrap();
563        assert_eq!(winner.pos, 3);
564        assert_eq!(winner.hash, "c");
565    }
566
567    #[test]
568    fn winning_rev_conflict_picks_higher_hash() {
569        // 1-a -> 2-b
570        //     -> 2-c
571        let tree = vec![RevPath {
572            pos: 1,
573            tree: node("a", vec![leaf("b"), leaf("c")]),
574        }];
575        let winner = winning_rev(&tree).unwrap();
576        assert_eq!(winner.hash, "c"); // "c" > "b" lexicographically
577    }
578
579    #[test]
580    fn winning_rev_conflict_prefers_longer() {
581        // 1-a -> 2-b -> 3-d
582        //     -> 2-c
583        let tree = vec![RevPath {
584            pos: 1,
585            tree: node("a", vec![node("b", vec![leaf("d")]), leaf("c")]),
586        }];
587        let winner = winning_rev(&tree).unwrap();
588        assert_eq!(winner.pos, 3);
589        assert_eq!(winner.hash, "d"); // pos 3 beats pos 2
590    }
591
592    #[test]
593    fn winning_rev_non_deleted_beats_deleted() {
594        // 1-a -> 2-b (non-deleted)
595        //     -> 2-z (deleted) — z > b but deleted loses
596        let tree = vec![RevPath {
597            pos: 1,
598            tree: node("a", vec![leaf("b"), deleted_leaf("z")]),
599        }];
600        let winner = winning_rev(&tree).unwrap();
601        assert_eq!(winner.hash, "b");
602    }
603
604    // --- collect_conflicts ---
605
606    #[test]
607    fn no_conflicts_on_linear() {
608        let tree = simple_tree();
609        assert!(collect_conflicts(&tree).is_empty());
610    }
611
612    #[test]
613    fn conflicts_on_branches() {
614        // 1-a -> 2-b, 2-c
615        let tree = vec![RevPath {
616            pos: 1,
617            tree: node("a", vec![leaf("b"), leaf("c")]),
618        }];
619        let conflicts = collect_conflicts(&tree);
620        assert_eq!(conflicts.len(), 1);
621        assert_eq!(conflicts[0].hash, "b"); // loser
622    }
623
624    // --- is_deleted ---
625
626    #[test]
627    fn is_deleted_false_for_normal() {
628        assert!(!is_deleted(&simple_tree()));
629    }
630
631    #[test]
632    fn is_deleted_true_when_winner_deleted() {
633        let tree = vec![RevPath {
634            pos: 1,
635            tree: deleted_leaf("a"),
636        }];
637        assert!(is_deleted(&tree));
638    }
639
640    // --- merge_tree ---
641
642    #[test]
643    fn merge_extends_linear_chain() {
644        // Start: 1-a -> 2-b
645        let tree = vec![RevPath {
646            pos: 1,
647            tree: node("a", vec![leaf("b")]),
648        }];
649
650        // Add: 3-c extending from 2-b
651        let new_path = build_path_from_revs(
652            3,
653            &["c".into(), "b".into()],
654            NodeOpts::default(),
655            RevStatus::Available,
656        );
657
658        let (merged, result) = merge_tree(&tree, &new_path, 1000);
659        assert_eq!(result, MergeResult::NewLeaf);
660
661        let winner = winning_rev(&merged).unwrap();
662        assert_eq!(winner.pos, 3);
663        assert_eq!(winner.hash, "c");
664    }
665
666    #[test]
667    fn merge_creates_conflict_branch() {
668        // Start: 1-a -> 2-b
669        let tree = vec![RevPath {
670            pos: 1,
671            tree: node("a", vec![leaf("b")]),
672        }];
673
674        // Add: 2-c branching from 1-a (conflict)
675        let new_path = build_path_from_revs(
676            2,
677            &["c".into(), "a".into()],
678            NodeOpts::default(),
679            RevStatus::Available,
680        );
681
682        let (merged, result) = merge_tree(&tree, &new_path, 1000);
683        assert_eq!(result, MergeResult::NewBranch);
684
685        let conflicts = collect_conflicts(&merged);
686        assert_eq!(conflicts.len(), 1);
687    }
688
689    #[test]
690    fn merge_duplicate_is_internal_node() {
691        // Start: 1-a -> 2-b
692        let tree = vec![RevPath {
693            pos: 1,
694            tree: node("a", vec![leaf("b")]),
695        }];
696
697        // Add: 2-b (already exists)
698        let new_path = build_path_from_revs(
699            2,
700            &["b".into(), "a".into()],
701            NodeOpts::default(),
702            RevStatus::Available,
703        );
704
705        let (_merged, result) = merge_tree(&tree, &new_path, 1000);
706        assert_eq!(result, MergeResult::InternalNode);
707    }
708
709    #[test]
710    fn merge_disjoint_creates_new_root() {
711        // Start: 1-a -> 2-b
712        let tree = vec![RevPath {
713            pos: 1,
714            tree: node("a", vec![leaf("b")]),
715        }];
716
717        // Add: 1-x -> 2-y (completely disjoint)
718        let new_path = build_path_from_revs(
719            2,
720            &["y".into(), "x".into()],
721            NodeOpts::default(),
722            RevStatus::Available,
723        );
724
725        let (merged, result) = merge_tree(&tree, &new_path, 1000);
726        assert_eq!(result, MergeResult::NewBranch);
727        assert_eq!(merged.len(), 2); // Two separate roots
728    }
729
730    // --- stem ---
731
732    #[test]
733    fn stem_prunes_old_revisions() {
734        // 1-a -> 2-b -> 3-c -> 4-d -> 5-e
735        let mut tree = vec![RevPath {
736            pos: 1,
737            tree: node(
738                "a",
739                vec![node("b", vec![node("c", vec![node("d", vec![leaf("e")])])])],
740            ),
741        }];
742
743        let stemmed = stem(&mut tree, 3);
744        assert!(!stemmed.is_empty());
745
746        // Tree should now start at a higher position
747        assert!(tree[0].pos > 1);
748
749        // Leaf should still be present
750        let leaves = collect_leaves(&tree);
751        assert_eq!(leaves[0].hash, "e");
752    }
753
754    #[test]
755    fn stem_splits_at_branch_point() {
756        // 1-a -> 2-b -> 3-c
757        //            -> 3-d
758        let mut tree = vec![RevPath {
759            pos: 1,
760            tree: node("a", vec![node("b", vec![leaf("c"), leaf("d")])]),
761        }];
762
763        // depth=1 means each leaf keeps a single revision. The shared
764        // ancestors 1-a and 2-b are pruned and each leaf becomes its own root.
765        let stemmed = stem(&mut tree, 1);
766        assert_eq!(stemmed.len(), 2); // a and b removed
767        assert!(stemmed.contains(&"a".to_string()));
768        assert!(stemmed.contains(&"b".to_string()));
769
770        assert_eq!(tree.len(), 2); // two separate roots, one per leaf
771        let leaves = collect_leaves(&tree);
772        let hashes: Vec<&str> = leaves.iter().map(|l| l.hash.as_str()).collect();
773        assert!(hashes.contains(&"c"));
774        assert!(hashes.contains(&"d"));
775        assert!(leaves.iter().all(|l| l.pos == 3));
776    }
777
778    #[test]
779    fn stem_prunes_deep_branch_above_cut_line() {
780        // Regression: a conflict branch at generation 1 used to prevent any
781        // stemming, letting the deep branch grow without bound.
782        //   1-a -> 2-b (leaf)
783        //       -> 2-c -> 3-d -> 4-e -> 5-f
784        let mut tree = vec![RevPath {
785            pos: 1,
786            tree: node(
787                "a",
788                vec![
789                    leaf("b"),
790                    node("c", vec![node("d", vec![node("e", vec![leaf("f")])])]),
791                ],
792            ),
793        }];
794
795        let stemmed = stem(&mut tree, 2);
796        // a, c, d are pruned; each leaf keeps at most 2 revisions.
797        assert!(stemmed.contains(&"a".to_string()));
798        assert!(stemmed.contains(&"c".to_string()));
799        assert!(stemmed.contains(&"d".to_string()));
800
801        // Roots become [2-b] and [4-e -> 5-f].
802        assert_eq!(tree.len(), 2);
803        for path in &tree {
804            assert!(max_depth(&path.tree) < 2, "every chain must fit the limit");
805        }
806        // Winner is still 5-f (highest generation across roots).
807        let winner = winning_rev(&tree).unwrap();
808        assert_eq!(winner.pos, 5);
809        assert_eq!(winner.hash, "f");
810    }
811
812    #[test]
813    fn stem_short_tree_unchanged() {
814        // 1-a -> 2-b (depth 1, limit 3 => nothing to prune)
815        let mut tree = vec![RevPath {
816            pos: 1,
817            tree: node("a", vec![leaf("b")]),
818        }];
819
820        let stemmed = stem(&mut tree, 3);
821        assert!(stemmed.is_empty());
822        assert_eq!(tree[0].pos, 1);
823    }
824
825    // --- latest_rev ---
826
827    #[test]
828    fn latest_rev_finds_available_node() {
829        let tree = simple_tree(); // 1-a -> 2-b -> 3-c
830        let rev = latest_rev(&tree, 3, "c").unwrap();
831        assert_eq!(rev.pos, 3);
832        assert_eq!(rev.hash, "c");
833    }
834
835    #[test]
836    fn latest_rev_walks_to_leaf_from_missing() {
837        // 1-a(missing) -> 2-b(available)
838        let tree = vec![RevPath {
839            pos: 1,
840            tree: RevNode {
841                hash: "a".into(),
842                status: RevStatus::Missing,
843                opts: NodeOpts::default(),
844                children: vec![leaf("b")],
845            },
846        }];
847        let rev = latest_rev(&tree, 1, "a").unwrap();
848        assert_eq!(rev.pos, 2);
849        assert_eq!(rev.hash, "b");
850    }
851
852    #[test]
853    fn latest_rev_none_for_nonexistent() {
854        let tree = simple_tree();
855        assert!(latest_rev(&tree, 5, "zzz").is_none());
856    }
857
858    #[test]
859    fn latest_rev_finds_internal_node() {
860        let tree = simple_tree(); // 1-a -> 2-b -> 3-c
861        let rev = latest_rev(&tree, 2, "b").unwrap();
862        assert_eq!(rev.pos, 2);
863        assert_eq!(rev.hash, "b");
864    }
865
866    #[test]
867    fn latest_rev_on_empty_tree() {
868        let tree: RevTree = vec![];
869        assert!(latest_rev(&tree, 1, "a").is_none());
870    }
871
872    #[test]
873    fn latest_leaf_walks_linear_chain_to_tip() {
874        // 1-a -> 2-b -> 3-c : latest from an internal node returns the leaf.
875        let tree = simple_tree();
876        let rev = latest_leaf(&tree, 1, "a").unwrap();
877        assert_eq!(rev.pos, 3);
878        assert_eq!(rev.hash, "c");
879        // A leaf returns itself.
880        assert_eq!(latest_leaf(&tree, 3, "c").unwrap().hash, "c");
881    }
882
883    #[test]
884    fn latest_leaf_stays_on_requested_branch() {
885        // 1-a -> 2-b -> 3-c   (losing branch)
886        //     -> 2-z -> 3-d -> 4-e   (winning branch by generation)
887        let tree = vec![RevPath {
888            pos: 1,
889            tree: node(
890                "a",
891                vec![
892                    node("b", vec![leaf("c")]),
893                    node("z", vec![node("d", vec![leaf("e")])]),
894                ],
895            ),
896        }];
897        // Global winner is 4-e, but latest from 2-b must stay on its branch.
898        assert_eq!(latest_leaf(&tree, 2, "b").unwrap().to_string(), "3-c");
899        assert_eq!(latest_leaf(&tree, 2, "z").unwrap().to_string(), "4-e");
900    }
901
902    // --- merge edge cases ---
903
904    #[test]
905    fn merge_exact_root_match_no_children() {
906        // Tree: 1-a (single node)
907        let tree = vec![RevPath {
908            pos: 1,
909            tree: leaf("a"),
910        }];
911
912        // Add same node: 1-a
913        let new_path = RevPath {
914            pos: 1,
915            tree: leaf("a"),
916        };
917
918        let (_, result) = merge_tree(&tree, &new_path, 1000);
919        assert_eq!(result, MergeResult::InternalNode);
920    }
921
922    #[test]
923    fn merge_same_branch_extends_deeper() {
924        // Tree: 1-a -> 2-b -> 3-c
925        let tree = simple_tree();
926
927        // Add: 1-a -> 2-b -> 3-c -> 4-d (full ancestry extending leaf)
928        let new_path = build_path_from_revs(
929            4,
930            &["d".into(), "c".into(), "b".into(), "a".into()],
931            NodeOpts::default(),
932            RevStatus::Available,
933        );
934
935        let (merged, result) = merge_tree(&tree, &new_path, 1000);
936        assert_eq!(result, MergeResult::NewLeaf);
937        let winner = winning_rev(&merged).unwrap();
938        assert_eq!(winner.pos, 4);
939        assert_eq!(winner.hash, "d");
940    }
941
942    #[test]
943    fn winning_rev_empty_tree() {
944        let tree: RevTree = vec![];
945        assert!(winning_rev(&tree).is_none());
946    }
947
948    #[test]
949    fn is_deleted_empty_tree() {
950        let tree: RevTree = vec![];
951        assert!(!is_deleted(&tree));
952    }
953
954    #[test]
955    fn collect_conflicts_deleted_leaves_excluded() {
956        // 1-a -> 2-b (normal), 2-c (deleted)
957        // Winner: 2-b, conflict: none (2-c is deleted)
958        let tree = vec![RevPath {
959            pos: 1,
960            tree: node("a", vec![leaf("b"), deleted_leaf("c")]),
961        }];
962        let conflicts = collect_conflicts(&tree);
963        assert!(conflicts.is_empty());
964    }
965}