Skip to main content

xml_3dm/merge/
mod.rs

1//! 3-way merge algorithm.
2//!
3//! This module implements the core 3-way merge algorithm that combines
4//! two branch trees relative to a common base tree. The algorithm handles
5//! insert, delete, update, move, and copy operations on subtrees.
6//!
7//! # Algorithm Overview
8//!
9//! The merge proceeds recursively through the matched trees:
10//! 1. Create MergeLists for each branch's children
11//! 2. Remove deleted/moved nodes and resolve conflicts
12//! 3. Merge the lists into MergePairs
13//! 4. For each pair, merge node content and recurse for children
14
15mod conflict_log;
16mod edit_log;
17mod merge_list;
18mod merge_pair;
19
20pub use conflict_log::{ConflictLog, ConflictType};
21pub use edit_log::{EditLog, EditType};
22pub use merge_list::{MergeEntry, MergeList};
23pub use merge_pair::{MergePair, MergePairList};
24
25use std::io::Write;
26
27use crate::matching::TriMatching;
28use crate::node::base::BaseNode;
29use crate::node::branch::BranchNode;
30use crate::node::{new_node_ref, MatchType, NodeInner, NodeRef, XmlContent, XmlElement};
31use crate::xml::XmlPrinter;
32
33/// Operation codes for merge list processing.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
35pub enum Operation {
36    /// No operation - node unchanged.
37    Nop,
38    /// Move inside child list.
39    MoveI,
40    /// Move far (to different parent).
41    MoveF,
42    /// Node deleted.
43    Delete,
44}
45
46/// The main merge struct that orchestrates the 3-way merge.
47pub struct Merge {
48    /// The tri-matching with base, left, and right trees.
49    matching: TriMatching,
50    /// Log of conflicts encountered during merge.
51    pub conflict_log: ConflictLog,
52    /// Log of edit operations performed.
53    pub edit_log: EditLog,
54}
55
56impl Merge {
57    /// Creates a new Merge from a TriMatching.
58    pub fn new(matching: TriMatching) -> Self {
59        Merge {
60            matching,
61            conflict_log: ConflictLog::new(),
62            edit_log: EditLog::new(),
63        }
64    }
65
66    /// Returns a reference to the tri-matching.
67    pub fn matching(&self) -> &TriMatching {
68        &self.matching
69    }
70
71    /// Runs the 3-way merge and writes the result.
72    ///
73    /// The output is written as XML to the provided writer.
74    pub fn merge<W: Write>(&mut self, writer: &mut W) -> std::io::Result<()> {
75        // Build the merged tree
76        let tree = self.merge_to_tree();
77
78        // Use XmlPrinter for proper text content handling
79        let mut printer = XmlPrinter::new(writer);
80        printer.print(&tree)
81    }
82
83    /// Runs the 3-way merge and returns the merged tree.
84    ///
85    /// This is useful when you need to programmatically inspect or
86    /// further modify the merge result before output.
87    pub fn merge_to_tree(&mut self) -> NodeRef {
88        // Create synthetic root node
89        let root_content = XmlContent::Element(XmlElement::new(
90            "$ROOT$".to_string(),
91            std::collections::HashMap::new(),
92        ));
93        let root = new_node_ref(NodeInner::new_base(Some(root_content)));
94
95        // Build children recursively
96        self.tree_merge_to_node(
97            Some(self.matching.left_root().clone()),
98            Some(self.matching.right_root().clone()),
99            &root,
100        );
101
102        root
103    }
104
105    /// Recursive tree merge that builds nodes instead of writing output.
106    fn tree_merge_to_node(&mut self, a: Option<NodeRef>, b: Option<NodeRef>, parent: &NodeRef) {
107        // Create merge lists for each branch
108        let mlist_a = a.as_ref().map(|node| self.make_merge_list(node));
109        let mlist_b = b.as_ref().map(|node| self.make_merge_list(node));
110
111        // Generate merge pair list
112        let merged = match (&mlist_a, &mlist_b) {
113            (Some(ma), Some(mb)) => self.make_merge_pair_list(ma.clone(), mb.clone()),
114            (Some(ma), None) => self.merge_list_to_pair_list(ma, None),
115            (None, Some(mb)) => self.merge_list_to_pair_list(mb, None),
116            (None, None) => MergePairList::new(),
117        };
118
119        // Process each merge pair
120        for pair in merged.pairs() {
121            let merged_content = self.merge_node_content(pair);
122
123            if let Some(content) = merged_content {
124                match &content {
125                    XmlContent::Element(elem) if elem.qname() == "$ROOT$" => {
126                        // Skip synthetic root but process children
127                        let partners = self.get_recursion_partners(pair);
128                        self.tree_merge_to_node(partners.first, partners.second, parent);
129                    }
130                    _ => {
131                        // Create new node with merged content
132                        let child = new_node_ref(NodeInner::new_base(Some(content.clone())));
133
134                        // Add to parent
135                        NodeInner::add_child_to_ref(parent, child.clone());
136
137                        // Recurse for element children
138                        if let XmlContent::Element(_) = content {
139                            let partners = self.get_recursion_partners(pair);
140                            self.tree_merge_to_node(partners.first, partners.second, &child);
141                        }
142                    }
143                }
144            }
145        }
146    }
147
148    /// Creates a merge list from the children of a branch node.
149    fn make_merge_list(&mut self, parent: &NodeRef) -> MergeList {
150        MergeList::from_branch(parent, &mut self.edit_log)
151    }
152
153    /// Merges two merge lists into a merge pair list.
154    fn make_merge_pair_list(&mut self, mlist_a: MergeList, mlist_b: MergeList) -> MergePairList {
155        let mut mlist_a = mlist_a;
156        let mut mlist_b = mlist_b;
157
158        // Remove deleted or far-moved nodes
159        self.remove_deleted_or_moved(&mut mlist_a, &mut mlist_b);
160
161        // Checkpoint for potential rollback
162        self.edit_log.checkpoint();
163
164        // Lists should now have the same entries
165        if mlist_a.entry_count() != mlist_b.entry_count() {
166            // Fallback to simpler merge
167            self.edit_log.rewind();
168            let is_left = mlist_a
169                .entry_parent()
170                .map(|p| BranchNode::is_left_tree(&p))
171                .unwrap_or(true);
172            return if is_left {
173                self.merge_list_to_pair_list(&mlist_a, Some(&mlist_b))
174            } else {
175                self.merge_list_to_pair_list(&mlist_b, Some(&mlist_a))
176            };
177        }
178
179        let mut merged = MergePairList::new();
180        let mut pos_a = 0usize;
181        let mut pos_b = 0usize;
182
183        loop {
184            let ea = mlist_a.entry(pos_a);
185            let eb = mlist_b.entry(pos_b);
186
187            if ea.is_none() || eb.is_none() {
188                break;
189            }
190
191            let ea = ea.unwrap();
192            let eb = eb.unwrap();
193
194            // Add hangons from ea
195            for hangon in ea.hangons() {
196                let partner = hangon.borrow().first_partner(MatchType::FULL);
197                merged.append(Some(hangon.clone()), partner);
198                self.log_hangon_struct_ops(hangon);
199            }
200
201            // Add hangons from eb (if not equal to ea's hangons)
202            if !self.check_hangon_combine(ea, eb) {
203                for hangon in eb.hangons() {
204                    let partner = hangon.borrow().first_partner(MatchType::FULL);
205                    merged.append(Some(hangon.clone()), partner);
206                    self.log_hangon_struct_ops(hangon);
207                }
208            }
209
210            // Determine next positions
211            let next_a: Option<usize>;
212            let next_b: Option<usize>;
213
214            let ea_locked = ea.locked && mlist_a.entry(pos_a + 1).is_some_and(|e| e.locked);
215            let eb_locked = eb.locked && mlist_b.entry(pos_b + 1).is_some_and(|e| e.locked);
216
217            if !ea_locked && !eb_locked {
218                // No locking, both advance
219                next_a = Some(pos_a + 1);
220                next_b = Some(pos_b + 1);
221            } else if ea_locked && !eb_locked {
222                next_a = Some(pos_a + 1);
223                next_b = mlist_b.find_partner_index(mlist_a.entry(pos_a + 1));
224            } else if !ea_locked && eb_locked {
225                next_b = Some(pos_b + 1);
226                next_a = mlist_a.find_partner_index(mlist_b.entry(pos_b + 1));
227            } else {
228                // Both locked - check for conflict
229                let partner_b = mlist_b.find_partner_index(mlist_a.entry(pos_a + 1));
230                if partner_b != Some(pos_b + 1) {
231                    // Conflict - fallback
232                    self.conflict_log.add_list_conflict(
233                        ConflictType::Move,
234                        "Conflicting moves inside child list",
235                        ea.node().and_then(|n| BranchNode::base_match(&n)),
236                        ea.node(),
237                        eb.node(),
238                    );
239                    self.edit_log.rewind();
240                    let is_left = mlist_a
241                        .entry_parent()
242                        .map(|p| BranchNode::is_left_tree(&p))
243                        .unwrap_or(true);
244                    return if is_left {
245                        self.merge_list_to_pair_list(&mlist_a, Some(&mlist_b))
246                    } else {
247                        self.merge_list_to_pair_list(&mlist_b, Some(&mlist_a))
248                    };
249                }
250                next_a = Some(pos_a + 1);
251                next_b = partner_b;
252            }
253
254            pos_a = next_a.unwrap_or(pos_a + 1);
255            pos_b = next_b.unwrap_or(pos_b + 1);
256
257            // Check if done (at END marker)
258            if mlist_a.is_end(pos_a) || mlist_b.is_end(pos_b) {
259                break;
260            }
261
262            // Add the main entry pair
263            let ea = mlist_a.entry(pos_a);
264            let eb = mlist_b.entry(pos_b);
265            if let (Some(ea), Some(eb)) = (ea, eb) {
266                merged.append(ea.node(), eb.node());
267                self.log_entry_struct_ops(ea, eb);
268            }
269        }
270
271        self.edit_log.commit();
272        merged
273    }
274
275    /// Converts a single merge list to a pair list.
276    fn merge_list_to_pair_list(
277        &mut self,
278        mlist_a: &MergeList,
279        mlist_b: Option<&MergeList>,
280    ) -> MergePairList {
281        let mut merged = MergePairList::new();
282
283        for (i, entry) in mlist_a.entries().iter().enumerate() {
284            // Skip START marker but process its hangons
285            if i > 0 && !mlist_a.is_end(i) {
286                let node = entry.node();
287                let partner = node
288                    .as_ref()
289                    .and_then(|n| n.borrow().first_partner(MatchType::FULL));
290                merged.append(node, partner);
291                if let Some(n) = entry.node() {
292                    self.log_hangon_struct_ops(&n);
293                }
294            }
295
296            // Add hangons
297            for hangon in entry.hangons() {
298                let partner = hangon.borrow().first_partner(MatchType::FULL);
299                merged.append(Some(hangon.clone()), partner);
300                self.log_hangon_struct_ops(hangon);
301            }
302
303            // Add hangons from mlist_b if present
304            if let Some(mb) = mlist_b {
305                if let Some(partner_entry) = mb.find_partner(entry) {
306                    if !self.check_hangon_combine(entry, partner_entry) {
307                        for hangon in partner_entry.hangons() {
308                            let partner = hangon.borrow().first_partner(MatchType::FULL);
309                            merged.append(Some(hangon.clone()), partner);
310                            self.log_hangon_struct_ops(hangon);
311                        }
312                    }
313                }
314            }
315        }
316
317        merged
318    }
319
320    /// Removes deleted or far-moved nodes from merge lists.
321    fn remove_deleted_or_moved(&mut self, mlist_a: &mut MergeList, mlist_b: &mut MergeList) {
322        let base_parent = mlist_a
323            .entry_parent()
324            .and_then(|p| BranchNode::base_match(&p));
325
326        if base_parent.is_none() {
327            return;
328        }
329
330        let base_parent = base_parent.unwrap();
331        let child_count = base_parent.borrow().child_count();
332
333        for i in 0..child_count {
334            let bn = base_parent.borrow().child(i).cloned();
335            if bn.is_none() {
336                continue;
337            }
338            let bn = bn.unwrap();
339
340            let op1 = self.get_operation(&bn, mlist_a);
341            let op2 = self.get_operation(&bn, mlist_b);
342
343            // Handle based on which operation is smaller
344            // The table is symmetric in terms of which list we apply actions to
345            if op1 <= op2 {
346                self.handle_operation_pair(op1, op2, &bn, mlist_a, mlist_b);
347            } else {
348                self.handle_operation_pair(op2, op1, &bn, mlist_b, mlist_a);
349            }
350        }
351    }
352
353    /// Handles a pair of operations on a base node.
354    fn handle_operation_pair(
355        &mut self,
356        op1: Operation,
357        op2: Operation,
358        bn: &NodeRef,
359        ml1: &mut MergeList,
360        _ml2: &mut MergeList,
361    ) {
362        use Operation::*;
363
364        match (op1, op2) {
365            (Nop, Nop) | (Nop, MoveI) | (MoveI, MoveI) | (Delete, Delete) => {
366                // OK cases - no action needed
367            }
368            (Nop, MoveF) | (Nop, Delete) => {
369                // Delete the node from ml1
370                if let Some(ix) = ml1.match_in_list(bn) {
371                    if op2 == Delete {
372                        // Check for modifications in deleted subtree
373                        if let Some(entry) = ml1.entry(ix) {
374                            if let Some(node) = entry.node() {
375                                if self.is_deletia_modified(&node, ml1) {
376                                    self.conflict_log.add_list_warning(
377                                        ConflictType::Delete,
378                                        "Modifications in deleted subtree",
379                                        Some(bn.clone()),
380                                        entry.node(),
381                                        None,
382                                    );
383                                }
384                            }
385                        }
386                        self.edit_log.delete(Some(bn.clone()));
387                    }
388
389                    // Move hangons to predecessor
390                    ml1.move_hangons_to_predecessor(ix);
391                    ml1.remove_entry_at(ix);
392                }
393            }
394            (MoveI, MoveF) => {
395                // Conflict: node moved to different locations
396                if let Some(ix) = ml1.match_in_list(bn) {
397                    let node = ml1.entry(ix).and_then(|e| e.node());
398                    let partner = node
399                        .as_ref()
400                        .and_then(|n| n.borrow().first_partner(MatchType::FULL));
401                    self.conflict_log.add_list_conflict(
402                        ConflictType::Move,
403                        "Node moved to different locations",
404                        Some(bn.clone()),
405                        node,
406                        partner,
407                    );
408                    ml1.remove_entry_at(ix);
409                }
410            }
411            (MoveI, Delete) => {
412                // Conflict: node moved and deleted
413                if let Some(ix) = ml1.match_in_list(bn) {
414                    let node = ml1.entry(ix).and_then(|e| e.node());
415                    self.conflict_log.add_list_conflict(
416                        ConflictType::Move,
417                        "Node moved and deleted",
418                        Some(bn.clone()),
419                        node,
420                        None,
421                    );
422                    self.edit_log.delete(Some(bn.clone()));
423                    ml1.remove_entry_at(ix);
424                }
425            }
426            (MoveF, MoveF) => {
427                // Check for movef-movef conflict
428                if self.is_movef_movef_conflict(bn) {
429                    self.conflict_log.add_list_conflict(
430                        ConflictType::Move,
431                        "Node moved to different locations",
432                        Some(bn.clone()),
433                        BaseNode::left(bn).borrow().full_match(),
434                        BaseNode::right(bn).borrow().full_match(),
435                    );
436                }
437            }
438            (MoveF, Delete) => {
439                // Conflict: moved and deleted - keep the move
440                self.conflict_log.add_list_conflict(
441                    ConflictType::Move,
442                    "Node moved and deleted",
443                    Some(bn.clone()),
444                    BaseNode::left(bn).borrow().full_match(),
445                    BaseNode::right(bn).borrow().full_match(),
446                );
447            }
448            _ => {}
449        }
450    }
451
452    /// Gets the operation on a base node in a merge list.
453    fn get_operation(&self, bn: &NodeRef, ml: &MergeList) -> Operation {
454        if let Some(pos) = ml.match_in_list(bn) {
455            if ml.entry(pos).is_some_and(|e| e.moved) {
456                Operation::MoveI
457            } else {
458                Operation::Nop
459            }
460        } else {
461            // Check if moved far or deleted
462            let is_left = ml
463                .entry_parent()
464                .map(|p| BranchNode::is_left_tree(&p))
465                .unwrap_or(true);
466
467            let copies = if is_left {
468                BaseNode::left(bn)
469            } else {
470                BaseNode::right(bn)
471            };
472
473            if copies.borrow().match_count() == 0 {
474                Operation::Delete
475            } else {
476                Operation::MoveF
477            }
478        }
479    }
480
481    /// Checks if a deleted subtree has modifications.
482    fn is_deletia_modified(&self, n: &NodeRef, ml: &MergeList) -> bool {
483        let base_match = BranchNode::base_match(n);
484        if base_match.is_none() {
485            return true; // Inserted node
486        }
487
488        let base_match = base_match.unwrap();
489        if self.get_operation(&base_match, ml) != Operation::Nop {
490            return true; // Node was moved
491        }
492
493        if BranchNode::base_match_type(n) != MatchType::FULL {
494            return true; // Structural or content modification
495        }
496
497        // Check if deleted in other branch
498        if let Some(partners) = BranchNode::partners(n) {
499            if partners.borrow().match_count() == 0 {
500                if !self.matches(n, &base_match) {
501                    return true; // Updated
502                }
503                // Check children recursively
504                for i in 0..n.borrow().child_count() {
505                    if let Some(child) = n.borrow().child(i).cloned() {
506                        // Would need child's merge list - simplify for now
507                        if BranchNode::base_match_type(&child) != MatchType::FULL {
508                            return true;
509                        }
510                    }
511                }
512            }
513        }
514
515        false
516    }
517
518    /// Checks for movef-movef conflict.
519    ///
520    /// A MoveF-MoveF conflict occurs when both branches move the same base node
521    /// to different parent locations. Returns true if the parents differ (conflict).
522    fn is_movef_movef_conflict(&self, bn: &NodeRef) -> bool {
523        // Get left/right branch copies of base node
524        let left_node = BaseNode::left(bn).borrow().full_match();
525        let right_node = BaseNode::right(bn).borrow().full_match();
526
527        let (left_node, right_node) = match (left_node, right_node) {
528            (Some(l), Some(r)) => (l, r),
529            _ => return false,
530        };
531
532        // Get parents
533        let left_parent = BranchNode::parent(&left_node);
534        let right_parent = BranchNode::parent(&right_node);
535
536        match (left_parent, right_parent) {
537            (Some(lp), Some(rp)) => {
538                let lp_base = BranchNode::base_match(&lp);
539                let rp_base = BranchNode::base_match(&rp);
540                match (lp_base, rp_base) {
541                    (Some(lb), Some(rb)) => lb.borrow().id() != rb.borrow().id(),
542                    _ => true, // Inserted parent = conflict
543                }
544            }
545            (None, None) => false, // Both at root
546            _ => true,             // One at root, one not
547        }
548    }
549
550    /// Merges the content of nodes in a merge pair.
551    fn merge_node_content(&mut self, pair: &MergePair) -> Option<XmlContent> {
552        let n1 = pair.first.as_ref();
553        let n2 = pair.second.as_ref();
554
555        match (n1, n2) {
556            (None, None) => None,
557            (Some(n), None) | (None, Some(n)) => n.borrow().content().cloned(),
558            (Some(n1), Some(n2)) => {
559                let n1_content = n1.borrow().is_match(MatchType::CONTENT);
560                let n2_content = n2.borrow().is_match(MatchType::CONTENT);
561
562                if n1_content {
563                    if !n2_content {
564                        self.edit_log.update(Some(n2.clone()));
565                        n2.borrow().content().cloned()
566                    } else {
567                        self.content_merge(n1, n2)
568                    }
569                } else if n2_content {
570                    self.edit_log.update(Some(n1.clone()));
571                    n1.borrow().content().cloned()
572                } else {
573                    // Neither matches content - forced merge
574                    self.content_merge(n1, n2)
575                }
576            }
577        }
578    }
579
580    /// Merges content of two nodes.
581    fn content_merge(&mut self, a: &NodeRef, b: &NodeRef) -> Option<XmlContent> {
582        let base_a = BranchNode::base_match(a);
583        let base_b = BranchNode::base_match(b);
584
585        let a_updated = base_a.as_ref().is_none_or(|ba| !self.matches(a, ba));
586        let b_updated = base_b.as_ref().is_none_or(|bb| !self.matches(b, bb));
587
588        if a_updated && b_updated {
589            // Both updated - check if same update
590            if self.matches(a, b) {
591                self.conflict_log.add_node_warning(
592                    ConflictType::Update,
593                    "Node updated in both branches, but updates are equal",
594                    base_a.clone(),
595                    Some(a.clone()),
596                    Some(b.clone()),
597                );
598                self.edit_log.update(Some(a.clone()));
599                return a.borrow().content().cloned();
600            }
601
602            // Try element merge
603            if let Some(merged) = self.merge_elements(base_a.as_ref(), a, b) {
604                return Some(merged);
605            }
606
607            // Conflict - use first branch
608            self.conflict_log.add_node_conflict(
609                ConflictType::Update,
610                "Node updated in both branches, using branch 1",
611                base_a,
612                Some(a.clone()),
613                Some(b.clone()),
614            );
615            self.edit_log.update(Some(a.clone()));
616            a.borrow().content().cloned()
617        } else if b_updated {
618            // Check if a has namespace declaration changes that need merging
619            if self.has_namespace_changes(a, base_a.as_ref()) {
620                if let Some(merged) = self.merge_elements(base_a.as_ref(), a, b) {
621                    return Some(merged);
622                }
623                // Namespace conflict
624                self.conflict_log.add_node_conflict(
625                    ConflictType::Update,
626                    "Namespace declaration conflict between branches, using branch 2",
627                    base_a,
628                    Some(a.clone()),
629                    Some(b.clone()),
630                );
631            }
632            self.edit_log.update(Some(b.clone()));
633            b.borrow().content().cloned()
634        } else if a_updated {
635            // Check if b has namespace declaration changes that need merging
636            if self.has_namespace_changes(b, base_b.as_ref()) {
637                if let Some(merged) = self.merge_elements(base_a.as_ref(), a, b) {
638                    return Some(merged);
639                }
640                // Namespace conflict
641                self.conflict_log.add_node_conflict(
642                    ConflictType::Update,
643                    "Namespace declaration conflict between branches, using branch 1",
644                    base_a,
645                    Some(a.clone()),
646                    Some(b.clone()),
647                );
648            }
649            self.edit_log.update(Some(a.clone()));
650            a.borrow().content().cloned()
651        } else {
652            // Neither content updated, but check for namespace-only changes
653            if self.has_namespace_changes(a, base_a.as_ref())
654                || self.has_namespace_changes(b, base_b.as_ref())
655            {
656                if let Some(merged) = self.merge_elements(base_a.as_ref(), a, b) {
657                    return Some(merged);
658                }
659                // Namespace conflict
660                self.conflict_log.add_node_conflict(
661                    ConflictType::Update,
662                    "Namespace declaration conflict between branches, using branch 1",
663                    base_a.clone(),
664                    Some(a.clone()),
665                    Some(b.clone()),
666                );
667            }
668            a.borrow().content().cloned()
669        }
670    }
671
672    /// Attempts to merge two element nodes.
673    fn merge_elements(
674        &self,
675        base: Option<&NodeRef>,
676        a: &NodeRef,
677        b: &NodeRef,
678    ) -> Option<XmlContent> {
679        let base_content = base.and_then(|n| n.borrow().content().cloned());
680        let a_content = a.borrow().content().cloned();
681        let b_content = b.borrow().content().cloned();
682
683        match (&base_content, &a_content, &b_content) {
684            (
685                Some(XmlContent::Element(base_elem)),
686                Some(XmlContent::Element(a_elem)),
687                Some(XmlContent::Element(b_elem)),
688            ) => {
689                // Determine tag name
690                let tag_name = if base_elem.names_match(b_elem) {
691                    a_elem.qname().to_string()
692                } else if base_elem.names_match(a_elem) {
693                    b_elem.qname().to_string()
694                } else {
695                    return None; // Both changed tag name
696                };
697
698                // Merge attributes
699                self.merge_attributes(
700                    base_elem.attributes(),
701                    a_elem.attributes(),
702                    b_elem.attributes(),
703                )
704                .and_then(|merged_attrs| {
705                    let merged_ns = self.merge_namespace_decls(
706                        base_elem.namespace_decls(),
707                        a_elem.namespace_decls(),
708                        b_elem.namespace_decls(),
709                    )?;
710                    // Use the winning element's expanded name
711                    let expanded = if base_elem.names_match(b_elem) {
712                        a_elem.expanded_name().cloned()
713                    } else {
714                        b_elem.expanded_name().cloned()
715                    };
716                    Some(XmlContent::Element(XmlElement::new_with_namespace(
717                        tag_name,
718                        expanded,
719                        merged_ns,
720                        merged_attrs,
721                    )))
722                })
723            }
724            _ => None,
725        }
726    }
727
728    /// Merges attributes from base, a, and b.
729    fn merge_attributes(
730        &self,
731        base: &std::collections::HashMap<String, String>,
732        a: &std::collections::HashMap<String, String>,
733        b: &std::collections::HashMap<String, String>,
734    ) -> Option<std::collections::HashMap<String, String>> {
735        let mut merged = std::collections::HashMap::new();
736        let mut deletia = std::collections::HashSet::new();
737
738        // Check for deleted attributes with conflicts
739        for (key, base_val) in base {
740            let in_a = a.get(key);
741            let in_b = b.get(key);
742
743            match (in_a, in_b) {
744                (None, Some(b_val)) if base_val != b_val => return None, // Deleted and changed
745                (Some(a_val), None) if base_val != a_val => return None, // Deleted and changed
746                (None, _) | (_, None) => {
747                    deletia.insert(key.clone());
748                }
749                _ => {}
750            }
751        }
752
753        // Build merged attributes from a
754        for (key, a_val) in a {
755            if deletia.contains(key) {
756                continue;
757            }
758
759            if let Some(b_val) = b.get(key) {
760                let base_val = base.get(key);
761                if base_val == Some(b_val) {
762                    // A possibly updated
763                    merged.insert(key.clone(), a_val.clone());
764                } else if base_val == Some(a_val) {
765                    // B possibly updated
766                    merged.insert(key.clone(), b_val.clone());
767                } else if a_val != b_val {
768                    // Both changed differently
769                    return None;
770                } else {
771                    merged.insert(key.clone(), a_val.clone());
772                }
773            } else {
774                // Insert from a
775                merged.insert(key.clone(), a_val.clone());
776            }
777        }
778
779        // Add inserts from b
780        for (key, b_val) in b {
781            if !deletia.contains(key) && !a.contains_key(key) {
782                merged.insert(key.clone(), b_val.clone());
783            }
784        }
785
786        Some(merged)
787    }
788
789    /// Merges namespace declarations from base, a, and b using 3-way merge.
790    /// Returns None on conflict (deleted in one branch, changed in the other).
791    fn merge_namespace_decls(
792        &self,
793        base: &std::collections::HashMap<String, String>,
794        a: &std::collections::HashMap<String, String>,
795        b: &std::collections::HashMap<String, String>,
796    ) -> Option<std::collections::HashMap<String, String>> {
797        let mut merged = std::collections::HashMap::new();
798        let mut deletia = std::collections::HashSet::new();
799
800        // Detect deletions and deletion-vs-change conflicts
801        for (prefix, base_uri) in base {
802            let in_a = a.get(prefix);
803            let in_b = b.get(prefix);
804
805            match (in_a, in_b) {
806                (None, Some(b_uri)) if base_uri != b_uri => return None,
807                (Some(a_uri), None) if base_uri != a_uri => return None,
808                (None, _) | (_, None) => {
809                    deletia.insert(prefix.clone());
810                }
811                _ => {}
812            }
813        }
814
815        // Build merged from a
816        for (prefix, a_uri) in a {
817            if deletia.contains(prefix) {
818                continue;
819            }
820            if let Some(b_uri) = b.get(prefix) {
821                let base_uri = base.get(prefix);
822                if base_uri == Some(b_uri) {
823                    merged.insert(prefix.clone(), a_uri.clone());
824                } else if base_uri == Some(a_uri) {
825                    merged.insert(prefix.clone(), b_uri.clone());
826                } else if a_uri != b_uri {
827                    return None;
828                } else {
829                    merged.insert(prefix.clone(), a_uri.clone());
830                }
831            } else {
832                merged.insert(prefix.clone(), a_uri.clone());
833            }
834        }
835
836        // Add new declarations from b
837        for (prefix, b_uri) in b {
838            if !deletia.contains(prefix) && !a.contains_key(prefix) {
839                merged.insert(prefix.clone(), b_uri.clone());
840            }
841        }
842
843        Some(merged)
844    }
845
846    /// Gets partners for recursion.
847    fn get_recursion_partners(&self, pair: &MergePair) -> RecursionPartners {
848        let n1 = pair.first.as_ref();
849        let n2 = pair.second.as_ref();
850
851        match (n1, n2) {
852            (None, _) | (_, None) => RecursionPartners {
853                first: pair.first.clone(),
854                second: pair.second.clone(),
855            },
856            (Some(n1), Some(n2)) => {
857                let n1_children = n1.borrow().is_match(MatchType::CHILDREN);
858                let n2_children = n2.borrow().is_match(MatchType::CHILDREN);
859
860                if n1_children && n2_children {
861                    RecursionPartners {
862                        first: Some(n1.clone()),
863                        second: Some(n2.clone()),
864                    }
865                } else if n1_children && n2.borrow().is_match(MatchType::CONTENT) {
866                    RecursionPartners {
867                        first: None,
868                        second: Some(n2.clone()),
869                    }
870                } else if n1.borrow().is_match(MatchType::CONTENT) && n2_children {
871                    RecursionPartners {
872                        first: Some(n1.clone()),
873                        second: None,
874                    }
875                } else {
876                    RecursionPartners {
877                        first: Some(n1.clone()),
878                        second: Some(n2.clone()),
879                    }
880                }
881            }
882        }
883    }
884
885    /// Checks if a node has namespace declaration changes relative to its base.
886    fn has_namespace_changes(&self, node: &NodeRef, base: Option<&NodeRef>) -> bool {
887        let Some(base) = base else { return false };
888        let node_borrowed = node.borrow();
889        let base_borrowed = base.borrow();
890        match (node_borrowed.content(), base_borrowed.content()) {
891            (Some(XmlContent::Element(ne)), Some(XmlContent::Element(be))) => {
892                !ne.namespace_decls_equal(be)
893            }
894            _ => false,
895        }
896    }
897
898    /// Checks if two nodes have equal content.
899    fn matches(&self, a: &NodeRef, b: &NodeRef) -> bool {
900        let a_borrowed = a.borrow();
901        let b_borrowed = b.borrow();
902        let a_content = a_borrowed.content();
903        let b_content = b_borrowed.content();
904
905        match (a_content, b_content) {
906            (Some(ac), Some(bc)) => ac.content_equals(bc),
907            _ => false,
908        }
909    }
910
911    /// Logs structural operations for a hangon node.
912    fn log_hangon_struct_ops(&mut self, n: &NodeRef) {
913        if !BranchNode::has_base_match(n) {
914            self.edit_log.insert(Some(n.clone()));
915        } else {
916            let base = BranchNode::base_match(n).unwrap();
917            let is_left = BranchNode::is_left_tree(n);
918
919            let copies = if is_left {
920                BaseNode::left(&base)
921            } else {
922                BaseNode::right(&base)
923            };
924
925            if copies.borrow().match_count() > 1 {
926                self.edit_log.copy(Some(n.clone()));
927            } else {
928                self.edit_log.r#move(Some(n.clone()));
929            }
930        }
931    }
932
933    /// Logs structural operations for merge entries.
934    fn log_entry_struct_ops(&mut self, e1: &MergeEntry, e2: &MergeEntry) {
935        if e1.moved {
936            if let Some(n) = e1.node() {
937                self.edit_log.r#move(Some(n));
938            }
939        } else if e2.moved {
940            if let Some(n) = e2.node() {
941                self.edit_log.r#move(Some(n));
942            }
943        }
944    }
945
946    /// Checks if hangons from two entries are equal.
947    /// Returns true only if hangons match exactly (same count and content).
948    /// When false, the caller should add eb's hangons to the merge list.
949    fn check_hangon_combine(&mut self, ea: &MergeEntry, eb: &MergeEntry) -> bool {
950        // If ea has no hangons, return false so eb's hangons are added
951        if ea.hangons().is_empty() {
952            return false;
953        }
954
955        if ea.hangons().len() != eb.hangons().len() {
956            if !ea.hangons().is_empty() && !eb.hangons().is_empty() {
957                self.conflict_log.add_list_warning(
958                    ConflictType::Insert,
959                    "Insertions/copies in both branches",
960                    ea.node().and_then(|n| BranchNode::base_match(&n)),
961                    ea.node(),
962                    eb.node(),
963                );
964            }
965            return false;
966        }
967
968        // Check if hangons match exactly
969        let mut equal = true;
970        for (ha, hb) in ea.hangons().iter().zip(eb.hangons().iter()) {
971            if !self.tree_matches(ha, hb) {
972                equal = false;
973                break;
974            }
975        }
976
977        if equal {
978            self.conflict_log.add_list_warning(
979                ConflictType::Insert,
980                "Equal insertions/copies in both branches",
981                ea.node().and_then(|n| BranchNode::base_match(&n)),
982                ea.node(),
983                eb.node(),
984            );
985        } else if !ea.hangons().is_empty() {
986            self.conflict_log.add_list_warning(
987                ConflictType::Insert,
988                "Insertions/copies in both branches, sequencing",
989                ea.node().and_then(|n| BranchNode::base_match(&n)),
990                ea.node(),
991                eb.node(),
992            );
993        }
994
995        equal
996    }
997
998    /// Checks if entire subtrees match exactly.
999    fn tree_matches(&self, a: &NodeRef, b: &NodeRef) -> bool {
1000        if !self.matches(a, b) {
1001            return false;
1002        }
1003
1004        let a_count = a.borrow().child_count();
1005        let b_count = b.borrow().child_count();
1006
1007        if a_count != b_count {
1008            return false;
1009        }
1010
1011        for i in 0..a_count {
1012            let a_child = a.borrow().child(i).cloned();
1013            let b_child = b.borrow().child(i).cloned();
1014
1015            match (a_child, b_child) {
1016                (Some(ac), Some(bc)) => {
1017                    if !self.tree_matches(&ac, &bc) {
1018                        return false;
1019                    }
1020                }
1021                _ => return false,
1022            }
1023        }
1024
1025        true
1026    }
1027}
1028
1029/// Partners for recursion.
1030struct RecursionPartners {
1031    first: Option<NodeRef>,
1032    second: Option<NodeRef>,
1033}
1034
1035impl crate::node::NodeInner {
1036    /// Returns the first partner that matches the given type.
1037    pub fn first_partner(&self, type_flags: MatchType) -> Option<NodeRef> {
1038        if let crate::node::NodeKind::Branch {
1039            match_type,
1040            partners,
1041            ..
1042        } = self.kind()
1043        {
1044            if !match_type.intersects(type_flags) {
1045                return None;
1046            }
1047
1048            if let Some(partners) = partners {
1049                let borrowed = partners.borrow();
1050                for candidate in borrowed.matches() {
1051                    if BranchNode::base_match_type(candidate).intersects(type_flags) {
1052                        return Some(candidate.clone());
1053                    }
1054                }
1055            }
1056        }
1057        None
1058    }
1059
1060    /// Returns true if the match type includes the given flags.
1061    pub fn is_match(&self, type_flags: MatchType) -> bool {
1062        if let crate::node::NodeKind::Branch { match_type, .. } = self.kind() {
1063            match_type.intersects(type_flags)
1064        } else {
1065            false
1066        }
1067    }
1068}
1069
1070#[cfg(test)]
1071mod tests {
1072    use super::*;
1073    use crate::matching::TriMatching;
1074    use crate::xml::{BaseNodeFactory, BranchNodeFactory, XmlParser};
1075    use std::collections::HashMap;
1076
1077    /// Helper: run a 3-way merge from XML strings, return (output_xml, conflict_count).
1078    fn merge_xml(base: &str, a: &str, b: &str) -> (String, usize) {
1079        let base_tree = XmlParser::new(BaseNodeFactory)
1080            .parse_str(base)
1081            .expect("parse base");
1082        let a_tree = XmlParser::new(BranchNodeFactory)
1083            .parse_str(a)
1084            .expect("parse a");
1085        let b_tree = XmlParser::new(BranchNodeFactory)
1086            .parse_str(b)
1087            .expect("parse b");
1088        let matching = TriMatching::new(a_tree, base_tree, b_tree);
1089        let mut merge = Merge::new(matching);
1090        let mut output = Vec::new();
1091        merge.merge(&mut output).expect("merge");
1092        let conflicts = merge.conflict_log.conflicts().len();
1093        (String::from_utf8(output).expect("utf8"), conflicts)
1094    }
1095
1096    #[test]
1097    fn test_merge_namespace_decls_deletion() {
1098        // Branch B removes xmlns:foo, A unchanged
1099        let base = r#"<?xml version="1.0"?><root xmlns:foo="http://foo.com" xmlns:bar="http://bar.com"><child/></root>"#;
1100        let a = base;
1101        let b = r#"<?xml version="1.0"?><root xmlns:bar="http://bar.com"><child/></root>"#;
1102        let (output, conflicts) = merge_xml(base, a, b);
1103        assert!(!output.contains("xmlns:foo"), "xmlns:foo should be removed");
1104        assert!(output.contains("xmlns:bar"), "xmlns:bar should remain");
1105        assert_eq!(conflicts, 0);
1106    }
1107
1108    #[test]
1109    fn test_merge_namespace_decls_addition() {
1110        // A adds xmlns:b, B adds xmlns:c
1111        let base = r#"<?xml version="1.0"?><root xmlns:a="http://a.com"><child/></root>"#;
1112        let a = r#"<?xml version="1.0"?><root xmlns:a="http://a.com" xmlns:b="http://b.com"><child/></root>"#;
1113        let b = r#"<?xml version="1.0"?><root xmlns:a="http://a.com" xmlns:c="http://c.com"><child/></root>"#;
1114        let (output, conflicts) = merge_xml(base, a, b);
1115        assert!(output.contains("xmlns:b"), "xmlns:b should be present");
1116        assert!(output.contains("xmlns:c"), "xmlns:c should be present");
1117        assert_eq!(conflicts, 0);
1118    }
1119
1120    #[test]
1121    fn test_merge_namespace_decls_delete_vs_change_conflict() {
1122        // A deletes xmlns:foo, B changes its URI → conflict
1123        let base = r#"<?xml version="1.0"?><root xmlns:foo="http://foo.com" xmlns:bar="http://bar.com"><child/></root>"#;
1124        let a = r#"<?xml version="1.0"?><root xmlns:bar="http://bar.com"><child/></root>"#;
1125        let b = r#"<?xml version="1.0"?><root xmlns:foo="http://foo-v2.com" xmlns:bar="http://bar.com"><child/></root>"#;
1126        let (_output, conflicts) = merge_xml(base, a, b);
1127        assert_eq!(conflicts, 1, "delete-vs-change should produce a conflict");
1128    }
1129
1130    #[test]
1131    fn test_merge_namespace_decls_both_change_same_prefix_conflict() {
1132        // A and B both change xmlns:foo to different URIs → conflict
1133        let base = r#"<?xml version="1.0"?><root xmlns:foo="http://foo.com"><child/></root>"#;
1134        let a = r#"<?xml version="1.0"?><root xmlns:foo="http://foo-a.com"><child/></root>"#;
1135        let b = r#"<?xml version="1.0"?><root xmlns:foo="http://foo-b.com"><child/></root>"#;
1136        let (_output, conflicts) = merge_xml(base, a, b);
1137        assert_eq!(
1138            conflicts, 1,
1139            "both changing same prefix differently should conflict"
1140        );
1141    }
1142
1143    #[test]
1144    fn test_merge_namespace_decls_both_change_same_value() {
1145        // A and B both change xmlns:foo to the SAME URI → no conflict
1146        let base = r#"<?xml version="1.0"?><root xmlns:foo="http://foo.com"><child/></root>"#;
1147        let a = r#"<?xml version="1.0"?><root xmlns:foo="http://foo-v2.com"><child/></root>"#;
1148        let b = r#"<?xml version="1.0"?><root xmlns:foo="http://foo-v2.com"><child/></root>"#;
1149        let (output, conflicts) = merge_xml(base, a, b);
1150        assert!(
1151            output.contains("http://foo-v2.com"),
1152            "merged URI should be the new value"
1153        );
1154        assert_eq!(conflicts, 0);
1155    }
1156
1157    #[test]
1158    fn test_merge_namespace_decls_unit() {
1159        // Direct unit test of merge_namespace_decls
1160        let matching = {
1161            let base = XmlParser::new(BaseNodeFactory)
1162                .parse_str("<root/>")
1163                .unwrap();
1164            let a = XmlParser::new(BranchNodeFactory)
1165                .parse_str("<root/>")
1166                .unwrap();
1167            let b = XmlParser::new(BranchNodeFactory)
1168                .parse_str("<root/>")
1169                .unwrap();
1170            TriMatching::new(a, base, b)
1171        };
1172        let merge = Merge::new(matching);
1173
1174        let mut base = HashMap::new();
1175        base.insert("foo".into(), "http://foo.com".into());
1176        base.insert("bar".into(), "http://bar.com".into());
1177
1178        // Deletion: a removes foo
1179        let mut a = HashMap::new();
1180        a.insert("bar".into(), "http://bar.com".into());
1181        let b = base.clone();
1182
1183        let result = merge.merge_namespace_decls(&base, &a, &b).unwrap();
1184        assert!(!result.contains_key("foo"));
1185        assert!(result.contains_key("bar"));
1186
1187        // Conflict: a removes foo, b changes foo
1188        let mut b_changed = HashMap::new();
1189        b_changed.insert("foo".into(), "http://foo-v2.com".into());
1190        b_changed.insert("bar".into(), "http://bar.com".into());
1191
1192        let result = merge.merge_namespace_decls(&base, &a, &b_changed);
1193        assert!(result.is_none(), "delete-vs-change should return None");
1194    }
1195}