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                        // Check if the deleted entry was locked (involved in
389                        // a move or copy) — thesis section 7.1.2
390                        if let Some(entry) = ml1.entry(ix) {
391                            if entry.locked {
392                                self.conflict_log.add_list_conflict(
393                                    ConflictType::Delete,
394                                    "Moved or copied node deleted",
395                                    Some(bn.clone()),
396                                    entry.node(),
397                                    None,
398                                );
399                            }
400                        }
401                    }
402
403                    // Move hangons to predecessor
404                    ml1.move_hangons_to_predecessor(ix);
405                    ml1.remove_entry_at(ix);
406                }
407            }
408            (MoveI, MoveF) => {
409                // Conflict: node moved to different locations
410                if let Some(ix) = ml1.match_in_list(bn) {
411                    let node = ml1.entry(ix).and_then(|e| e.node());
412                    let partner = node
413                        .as_ref()
414                        .and_then(|n| n.borrow().first_partner(MatchType::FULL));
415                    self.conflict_log.add_list_conflict(
416                        ConflictType::Move,
417                        "Node moved to different locations",
418                        Some(bn.clone()),
419                        node,
420                        partner,
421                    );
422                    ml1.remove_entry_at(ix);
423                }
424            }
425            (MoveI, Delete) => {
426                // Conflict: node moved and deleted
427                if let Some(ix) = ml1.match_in_list(bn) {
428                    let node = ml1.entry(ix).and_then(|e| e.node());
429                    self.conflict_log.add_list_conflict(
430                        ConflictType::Move,
431                        "Node moved and deleted",
432                        Some(bn.clone()),
433                        node,
434                        None,
435                    );
436                    self.edit_log.delete(Some(bn.clone()));
437                    ml1.remove_entry_at(ix);
438                }
439            }
440            (MoveF, MoveF) => {
441                // Check for movef-movef conflict
442                if self.is_movef_movef_conflict(bn) {
443                    self.conflict_log.add_list_conflict(
444                        ConflictType::Move,
445                        "Node moved to different locations",
446                        Some(bn.clone()),
447                        BaseNode::left(bn).borrow().full_match(),
448                        BaseNode::right(bn).borrow().full_match(),
449                    );
450                }
451            }
452            (MoveF, Delete) => {
453                // Conflict: moved and deleted - keep the move
454                self.conflict_log.add_list_conflict(
455                    ConflictType::Move,
456                    "Node moved and deleted",
457                    Some(bn.clone()),
458                    BaseNode::left(bn).borrow().full_match(),
459                    BaseNode::right(bn).borrow().full_match(),
460                );
461            }
462            _ => {}
463        }
464    }
465
466    /// Gets the operation on a base node in a merge list.
467    fn get_operation(&self, bn: &NodeRef, ml: &MergeList) -> Operation {
468        if let Some(pos) = ml.match_in_list(bn) {
469            if ml.entry(pos).is_some_and(|e| e.moved) {
470                Operation::MoveI
471            } else {
472                Operation::Nop
473            }
474        } else {
475            // Check if moved far or deleted
476            let is_left = ml
477                .entry_parent()
478                .map(|p| BranchNode::is_left_tree(&p))
479                .unwrap_or(true);
480
481            let copies = if is_left {
482                BaseNode::left(bn)
483            } else {
484                BaseNode::right(bn)
485            };
486
487            if copies.borrow().match_count() == 0 {
488                Operation::Delete
489            } else {
490                Operation::MoveF
491            }
492        }
493    }
494
495    /// Checks if a deleted subtree has modifications.
496    ///
497    /// Recursively checks children with per-level merge lists, matching the
498    /// thesis algorithm (Section 7.1.1, Listing 5).
499    fn is_deletia_modified(&mut self, n: &NodeRef, ml: &MergeList) -> bool {
500        let base_match = BranchNode::base_match(n);
501        if base_match.is_none() {
502            return true; // Inserted node
503        }
504
505        let base_match = base_match.unwrap();
506        if self.get_operation(&base_match, ml) != Operation::Nop {
507            return true; // Node was moved
508        }
509
510        if BranchNode::base_match_type(n) != MatchType::FULL {
511            return true; // Structural or content modification
512        }
513
514        // Check if deleted in other branch
515        if let Some(partners) = BranchNode::partners(n) {
516            if partners.borrow().match_count() == 0 {
517                if !self.matches(n, &base_match) {
518                    return true; // Updated
519                }
520                // Recursively check children with a child-level merge list
521                let child_ml = self.make_merge_list(n);
522                for i in 0..n.borrow().child_count() {
523                    if let Some(child) = n.borrow().child(i).cloned() {
524                        if self.is_deletia_modified(&child, &child_ml) {
525                            return true;
526                        }
527                    }
528                }
529            }
530        }
531
532        false
533    }
534
535    /// Checks for movef-movef conflict.
536    ///
537    /// A MoveF-MoveF conflict occurs when both branches move the same base node
538    /// to different parent locations. Returns true if the parents differ (conflict).
539    fn is_movef_movef_conflict(&self, bn: &NodeRef) -> bool {
540        // Get left/right branch copies of base node
541        let left_node = BaseNode::left(bn).borrow().full_match();
542        let right_node = BaseNode::right(bn).borrow().full_match();
543
544        let (left_node, right_node) = match (left_node, right_node) {
545            (Some(l), Some(r)) => (l, r),
546            _ => return false,
547        };
548
549        // Get parents
550        let left_parent = BranchNode::parent(&left_node);
551        let right_parent = BranchNode::parent(&right_node);
552
553        match (left_parent, right_parent) {
554            (Some(lp), Some(rp)) => {
555                let lp_base = BranchNode::base_match(&lp);
556                let rp_base = BranchNode::base_match(&rp);
557                match (lp_base, rp_base) {
558                    (Some(lb), Some(rb)) => lb.borrow().id() != rb.borrow().id(),
559                    _ => true, // Inserted parent = conflict
560                }
561            }
562            (None, None) => false, // Both at root
563            _ => true,             // One at root, one not
564        }
565    }
566
567    /// Merges the content of nodes in a merge pair.
568    fn merge_node_content(&mut self, pair: &MergePair) -> Option<XmlContent> {
569        let n1 = pair.first.as_ref();
570        let n2 = pair.second.as_ref();
571
572        match (n1, n2) {
573            (None, None) => None,
574            (Some(n), None) | (None, Some(n)) => n.borrow().content().cloned(),
575            (Some(n1), Some(n2)) => {
576                let n1_content = n1.borrow().is_match(MatchType::CONTENT);
577                let n2_content = n2.borrow().is_match(MatchType::CONTENT);
578
579                if n1_content {
580                    if !n2_content {
581                        self.edit_log.update(Some(n2.clone()));
582                        n2.borrow().content().cloned()
583                    } else {
584                        self.content_merge(n1, n2)
585                    }
586                } else if n2_content {
587                    self.edit_log.update(Some(n1.clone()));
588                    n1.borrow().content().cloned()
589                } else {
590                    // Neither matches content - forced merge
591                    self.content_merge(n1, n2)
592                }
593            }
594        }
595    }
596
597    /// Merges content of two nodes.
598    fn content_merge(&mut self, a: &NodeRef, b: &NodeRef) -> Option<XmlContent> {
599        let base_a = BranchNode::base_match(a);
600        let base_b = BranchNode::base_match(b);
601
602        let a_updated = base_a.as_ref().is_none_or(|ba| !self.matches(a, ba));
603        let b_updated = base_b.as_ref().is_none_or(|bb| !self.matches(b, bb));
604
605        if a_updated && b_updated {
606            // Both updated - check if same update
607            if self.matches(a, b) {
608                self.conflict_log.add_node_warning(
609                    ConflictType::Update,
610                    "Node updated in both branches, but updates are equal",
611                    base_a.clone(),
612                    Some(a.clone()),
613                    Some(b.clone()),
614                );
615                self.edit_log.update(Some(a.clone()));
616                return a.borrow().content().cloned();
617            }
618
619            // Try element merge
620            if let Some(merged) = self.merge_elements(base_a.as_ref(), a, b) {
621                return Some(merged);
622            }
623
624            // Conflict - use first branch
625            self.conflict_log.add_node_conflict(
626                ConflictType::Update,
627                "Node updated in both branches, using branch 1",
628                base_a,
629                Some(a.clone()),
630                Some(b.clone()),
631            );
632            self.edit_log.update(Some(a.clone()));
633            a.borrow().content().cloned()
634        } else if b_updated {
635            // Check if a has namespace declaration changes that need merging
636            if self.has_namespace_changes(a, base_a.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 2",
644                    base_a,
645                    Some(a.clone()),
646                    Some(b.clone()),
647                );
648            }
649            self.edit_log.update(Some(b.clone()));
650            b.borrow().content().cloned()
651        } else if a_updated {
652            // Check if b has namespace declaration changes that need merging
653            if self.has_namespace_changes(b, base_b.as_ref()) {
654                if let Some(merged) = self.merge_elements(base_a.as_ref(), a, b) {
655                    return Some(merged);
656                }
657                // Namespace conflict
658                self.conflict_log.add_node_conflict(
659                    ConflictType::Update,
660                    "Namespace declaration conflict between branches, using branch 1",
661                    base_a,
662                    Some(a.clone()),
663                    Some(b.clone()),
664                );
665            }
666            self.edit_log.update(Some(a.clone()));
667            a.borrow().content().cloned()
668        } else {
669            // Neither content updated, but check for namespace-only changes
670            if self.has_namespace_changes(a, base_a.as_ref())
671                || self.has_namespace_changes(b, base_b.as_ref())
672            {
673                if let Some(merged) = self.merge_elements(base_a.as_ref(), a, b) {
674                    return Some(merged);
675                }
676                // Namespace conflict
677                self.conflict_log.add_node_conflict(
678                    ConflictType::Update,
679                    "Namespace declaration conflict between branches, using branch 1",
680                    base_a.clone(),
681                    Some(a.clone()),
682                    Some(b.clone()),
683                );
684            }
685            a.borrow().content().cloned()
686        }
687    }
688
689    /// Attempts to merge two element nodes.
690    fn merge_elements(
691        &self,
692        base: Option<&NodeRef>,
693        a: &NodeRef,
694        b: &NodeRef,
695    ) -> Option<XmlContent> {
696        let base_content = base.and_then(|n| n.borrow().content().cloned());
697        let a_content = a.borrow().content().cloned();
698        let b_content = b.borrow().content().cloned();
699
700        match (&base_content, &a_content, &b_content) {
701            (
702                Some(XmlContent::Element(base_elem)),
703                Some(XmlContent::Element(a_elem)),
704                Some(XmlContent::Element(b_elem)),
705            ) => {
706                // Determine tag name
707                let tag_name = if base_elem.names_match(b_elem) {
708                    a_elem.qname().to_string()
709                } else if base_elem.names_match(a_elem) {
710                    b_elem.qname().to_string()
711                } else {
712                    return None; // Both changed tag name
713                };
714
715                // Merge attributes
716                self.merge_attributes(
717                    base_elem.attributes(),
718                    a_elem.attributes(),
719                    b_elem.attributes(),
720                )
721                .and_then(|merged_attrs| {
722                    let merged_ns = self.merge_namespace_decls(
723                        base_elem.namespace_decls(),
724                        a_elem.namespace_decls(),
725                        b_elem.namespace_decls(),
726                    )?;
727                    // Use the winning element's expanded name
728                    let expanded = if base_elem.names_match(b_elem) {
729                        a_elem.expanded_name().cloned()
730                    } else {
731                        b_elem.expanded_name().cloned()
732                    };
733                    Some(XmlContent::Element(XmlElement::new_with_namespace(
734                        tag_name,
735                        expanded,
736                        merged_ns,
737                        merged_attrs,
738                    )))
739                })
740            }
741            _ => None,
742        }
743    }
744
745    /// Merges attributes from base, a, and b.
746    fn merge_attributes(
747        &self,
748        base: &std::collections::HashMap<String, String>,
749        a: &std::collections::HashMap<String, String>,
750        b: &std::collections::HashMap<String, String>,
751    ) -> Option<std::collections::HashMap<String, String>> {
752        let mut merged = std::collections::HashMap::new();
753        let mut deletia = std::collections::HashSet::new();
754
755        // Check for deleted attributes with conflicts
756        for (key, base_val) in base {
757            let in_a = a.get(key);
758            let in_b = b.get(key);
759
760            match (in_a, in_b) {
761                (None, Some(b_val)) if base_val != b_val => return None, // Deleted and changed
762                (Some(a_val), None) if base_val != a_val => return None, // Deleted and changed
763                (None, _) | (_, None) => {
764                    deletia.insert(key.clone());
765                }
766                _ => {}
767            }
768        }
769
770        // Build merged attributes from a
771        for (key, a_val) in a {
772            if deletia.contains(key) {
773                continue;
774            }
775
776            if let Some(b_val) = b.get(key) {
777                let base_val = base.get(key);
778                if base_val == Some(b_val) {
779                    // A possibly updated
780                    merged.insert(key.clone(), a_val.clone());
781                } else if base_val == Some(a_val) {
782                    // B possibly updated
783                    merged.insert(key.clone(), b_val.clone());
784                } else if a_val != b_val {
785                    // Both changed differently
786                    return None;
787                } else {
788                    merged.insert(key.clone(), a_val.clone());
789                }
790            } else {
791                // Insert from a
792                merged.insert(key.clone(), a_val.clone());
793            }
794        }
795
796        // Add inserts from b
797        for (key, b_val) in b {
798            if !deletia.contains(key) && !a.contains_key(key) {
799                merged.insert(key.clone(), b_val.clone());
800            }
801        }
802
803        Some(merged)
804    }
805
806    /// Merges namespace declarations from base, a, and b using 3-way merge.
807    /// Returns None on conflict (deleted in one branch, changed in the other).
808    fn merge_namespace_decls(
809        &self,
810        base: &std::collections::HashMap<String, String>,
811        a: &std::collections::HashMap<String, String>,
812        b: &std::collections::HashMap<String, String>,
813    ) -> Option<std::collections::HashMap<String, String>> {
814        let mut merged = std::collections::HashMap::new();
815        let mut deletia = std::collections::HashSet::new();
816
817        // Detect deletions and deletion-vs-change conflicts
818        for (prefix, base_uri) in base {
819            let in_a = a.get(prefix);
820            let in_b = b.get(prefix);
821
822            match (in_a, in_b) {
823                (None, Some(b_uri)) if base_uri != b_uri => return None,
824                (Some(a_uri), None) if base_uri != a_uri => return None,
825                (None, _) | (_, None) => {
826                    deletia.insert(prefix.clone());
827                }
828                _ => {}
829            }
830        }
831
832        // Build merged from a
833        for (prefix, a_uri) in a {
834            if deletia.contains(prefix) {
835                continue;
836            }
837            if let Some(b_uri) = b.get(prefix) {
838                let base_uri = base.get(prefix);
839                if base_uri == Some(b_uri) {
840                    merged.insert(prefix.clone(), a_uri.clone());
841                } else if base_uri == Some(a_uri) {
842                    merged.insert(prefix.clone(), b_uri.clone());
843                } else if a_uri != b_uri {
844                    return None;
845                } else {
846                    merged.insert(prefix.clone(), a_uri.clone());
847                }
848            } else {
849                merged.insert(prefix.clone(), a_uri.clone());
850            }
851        }
852
853        // Add new declarations from b
854        for (prefix, b_uri) in b {
855            if !deletia.contains(prefix) && !a.contains_key(prefix) {
856                merged.insert(prefix.clone(), b_uri.clone());
857            }
858        }
859
860        Some(merged)
861    }
862
863    /// Gets partners for recursion.
864    fn get_recursion_partners(&self, pair: &MergePair) -> RecursionPartners {
865        let n1 = pair.first.as_ref();
866        let n2 = pair.second.as_ref();
867
868        match (n1, n2) {
869            (None, _) | (_, None) => RecursionPartners {
870                first: pair.first.clone(),
871                second: pair.second.clone(),
872            },
873            (Some(n1), Some(n2)) => {
874                let n1_children = n1.borrow().is_match(MatchType::CHILDREN);
875                let n2_children = n2.borrow().is_match(MatchType::CHILDREN);
876
877                if n1_children && n2_children {
878                    RecursionPartners {
879                        first: Some(n1.clone()),
880                        second: Some(n2.clone()),
881                    }
882                } else if n1_children && n2.borrow().is_match(MatchType::CONTENT) {
883                    RecursionPartners {
884                        first: None,
885                        second: Some(n2.clone()),
886                    }
887                } else if n1.borrow().is_match(MatchType::CONTENT) && n2_children {
888                    RecursionPartners {
889                        first: Some(n1.clone()),
890                        second: None,
891                    }
892                } else {
893                    RecursionPartners {
894                        first: Some(n1.clone()),
895                        second: Some(n2.clone()),
896                    }
897                }
898            }
899        }
900    }
901
902    /// Checks if a node has namespace declaration changes relative to its base.
903    fn has_namespace_changes(&self, node: &NodeRef, base: Option<&NodeRef>) -> bool {
904        let Some(base) = base else { return false };
905        let node_borrowed = node.borrow();
906        let base_borrowed = base.borrow();
907        match (node_borrowed.content(), base_borrowed.content()) {
908            (Some(XmlContent::Element(ne)), Some(XmlContent::Element(be))) => {
909                !ne.namespace_decls_equal(be)
910            }
911            _ => false,
912        }
913    }
914
915    /// Checks if two nodes have equal content.
916    fn matches(&self, a: &NodeRef, b: &NodeRef) -> bool {
917        let a_borrowed = a.borrow();
918        let b_borrowed = b.borrow();
919        let a_content = a_borrowed.content();
920        let b_content = b_borrowed.content();
921
922        match (a_content, b_content) {
923            (Some(ac), Some(bc)) => ac.content_equals(bc),
924            _ => false,
925        }
926    }
927
928    /// Logs structural operations for a hangon node.
929    fn log_hangon_struct_ops(&mut self, n: &NodeRef) {
930        if !BranchNode::has_base_match(n) {
931            self.edit_log.insert(Some(n.clone()));
932        } else {
933            let base = BranchNode::base_match(n).unwrap();
934            let is_left = BranchNode::is_left_tree(n);
935
936            let copies = if is_left {
937                BaseNode::left(&base)
938            } else {
939                BaseNode::right(&base)
940            };
941
942            if copies.borrow().match_count() > 1 {
943                self.edit_log.copy(Some(n.clone()));
944            } else {
945                self.edit_log.r#move(Some(n.clone()));
946            }
947        }
948    }
949
950    /// Logs structural operations for merge entries.
951    fn log_entry_struct_ops(&mut self, e1: &MergeEntry, e2: &MergeEntry) {
952        if e1.moved {
953            if let Some(n) = e1.node() {
954                self.edit_log.r#move(Some(n));
955            }
956        } else if e2.moved {
957            if let Some(n) = e2.node() {
958                self.edit_log.r#move(Some(n));
959            }
960        }
961    }
962
963    /// Checks if hangons from two entries are equal.
964    /// Returns true only if hangons match exactly (same count and content).
965    /// When false, the caller should add eb's hangons to the merge list.
966    fn check_hangon_combine(&mut self, ea: &MergeEntry, eb: &MergeEntry) -> bool {
967        // If ea has no hangons, return false so eb's hangons are added
968        if ea.hangons().is_empty() {
969            return false;
970        }
971
972        if ea.hangons().len() != eb.hangons().len() {
973            if !ea.hangons().is_empty() && !eb.hangons().is_empty() {
974                self.conflict_log.add_list_warning(
975                    ConflictType::Insert,
976                    "Insertions/copies in both branches",
977                    ea.node().and_then(|n| BranchNode::base_match(&n)),
978                    ea.node(),
979                    eb.node(),
980                );
981            }
982            return false;
983        }
984
985        // Check if hangons match exactly
986        let mut equal = true;
987        for (ha, hb) in ea.hangons().iter().zip(eb.hangons().iter()) {
988            if !self.tree_matches(ha, hb) {
989                equal = false;
990                break;
991            }
992        }
993
994        if equal {
995            self.conflict_log.add_list_warning(
996                ConflictType::Insert,
997                "Equal insertions/copies in both branches",
998                ea.node().and_then(|n| BranchNode::base_match(&n)),
999                ea.node(),
1000                eb.node(),
1001            );
1002        } else if !ea.hangons().is_empty() {
1003            self.conflict_log.add_list_warning(
1004                ConflictType::Insert,
1005                "Insertions/copies in both branches, sequencing",
1006                ea.node().and_then(|n| BranchNode::base_match(&n)),
1007                ea.node(),
1008                eb.node(),
1009            );
1010        }
1011
1012        equal
1013    }
1014
1015    /// Checks if entire subtrees match exactly.
1016    fn tree_matches(&self, a: &NodeRef, b: &NodeRef) -> bool {
1017        if !self.matches(a, b) {
1018            return false;
1019        }
1020
1021        let a_count = a.borrow().child_count();
1022        let b_count = b.borrow().child_count();
1023
1024        if a_count != b_count {
1025            return false;
1026        }
1027
1028        for i in 0..a_count {
1029            let a_child = a.borrow().child(i).cloned();
1030            let b_child = b.borrow().child(i).cloned();
1031
1032            match (a_child, b_child) {
1033                (Some(ac), Some(bc)) => {
1034                    if !self.tree_matches(&ac, &bc) {
1035                        return false;
1036                    }
1037                }
1038                _ => return false,
1039            }
1040        }
1041
1042        true
1043    }
1044}
1045
1046/// Partners for recursion.
1047struct RecursionPartners {
1048    first: Option<NodeRef>,
1049    second: Option<NodeRef>,
1050}
1051
1052impl crate::node::NodeInner {
1053    /// Returns the first partner that matches the given type.
1054    pub fn first_partner(&self, type_flags: MatchType) -> Option<NodeRef> {
1055        if let crate::node::NodeKind::Branch {
1056            match_type,
1057            partners,
1058            ..
1059        } = self.kind()
1060        {
1061            if !match_type.intersects(type_flags) {
1062                return None;
1063            }
1064
1065            if let Some(partners) = partners {
1066                let borrowed = partners.borrow();
1067                for candidate in borrowed.matches() {
1068                    if BranchNode::base_match_type(candidate).intersects(type_flags) {
1069                        return Some(candidate.clone());
1070                    }
1071                }
1072            }
1073        }
1074        None
1075    }
1076
1077    /// Returns true if the match type includes the given flags.
1078    pub fn is_match(&self, type_flags: MatchType) -> bool {
1079        if let crate::node::NodeKind::Branch { match_type, .. } = self.kind() {
1080            match_type.intersects(type_flags)
1081        } else {
1082            false
1083        }
1084    }
1085}
1086
1087#[cfg(test)]
1088mod tests {
1089    use super::*;
1090    use crate::matching::TriMatching;
1091    use crate::xml::{BaseNodeFactory, BranchNodeFactory, XmlParser};
1092    use std::collections::HashMap;
1093
1094    /// Helper: run a 3-way merge from XML strings, return (output_xml, conflict_count).
1095    fn merge_xml(base: &str, a: &str, b: &str) -> (String, usize) {
1096        let base_tree = XmlParser::new(BaseNodeFactory)
1097            .parse_str(base)
1098            .expect("parse base");
1099        let a_tree = XmlParser::new(BranchNodeFactory)
1100            .parse_str(a)
1101            .expect("parse a");
1102        let b_tree = XmlParser::new(BranchNodeFactory)
1103            .parse_str(b)
1104            .expect("parse b");
1105        let matching = TriMatching::new(a_tree, base_tree, b_tree);
1106        let mut merge = Merge::new(matching);
1107        let mut output = Vec::new();
1108        merge.merge(&mut output).expect("merge");
1109        let conflicts = merge.conflict_log.conflicts().len();
1110        (String::from_utf8(output).expect("utf8"), conflicts)
1111    }
1112
1113    #[test]
1114    fn test_merge_namespace_decls_deletion() {
1115        // Branch B removes xmlns:foo, A unchanged
1116        let base = r#"<?xml version="1.0"?><root xmlns:foo="http://foo.com" xmlns:bar="http://bar.com"><child/></root>"#;
1117        let a = base;
1118        let b = r#"<?xml version="1.0"?><root xmlns:bar="http://bar.com"><child/></root>"#;
1119        let (output, conflicts) = merge_xml(base, a, b);
1120        assert!(!output.contains("xmlns:foo"), "xmlns:foo should be removed");
1121        assert!(output.contains("xmlns:bar"), "xmlns:bar should remain");
1122        assert_eq!(conflicts, 0);
1123    }
1124
1125    #[test]
1126    fn test_merge_namespace_decls_addition() {
1127        // A adds xmlns:b, B adds xmlns:c
1128        let base = r#"<?xml version="1.0"?><root xmlns:a="http://a.com"><child/></root>"#;
1129        let a = r#"<?xml version="1.0"?><root xmlns:a="http://a.com" xmlns:b="http://b.com"><child/></root>"#;
1130        let b = r#"<?xml version="1.0"?><root xmlns:a="http://a.com" xmlns:c="http://c.com"><child/></root>"#;
1131        let (output, conflicts) = merge_xml(base, a, b);
1132        assert!(output.contains("xmlns:b"), "xmlns:b should be present");
1133        assert!(output.contains("xmlns:c"), "xmlns:c should be present");
1134        assert_eq!(conflicts, 0);
1135    }
1136
1137    #[test]
1138    fn test_merge_namespace_decls_delete_vs_change_conflict() {
1139        // A deletes xmlns:foo, B changes its URI → conflict
1140        let base = r#"<?xml version="1.0"?><root xmlns:foo="http://foo.com" xmlns:bar="http://bar.com"><child/></root>"#;
1141        let a = r#"<?xml version="1.0"?><root xmlns:bar="http://bar.com"><child/></root>"#;
1142        let b = r#"<?xml version="1.0"?><root xmlns:foo="http://foo-v2.com" xmlns:bar="http://bar.com"><child/></root>"#;
1143        let (_output, conflicts) = merge_xml(base, a, b);
1144        assert_eq!(conflicts, 1, "delete-vs-change should produce a conflict");
1145    }
1146
1147    #[test]
1148    fn test_merge_namespace_decls_both_change_same_prefix_conflict() {
1149        // A and B both change xmlns:foo to different URIs → conflict
1150        let base = r#"<?xml version="1.0"?><root xmlns:foo="http://foo.com"><child/></root>"#;
1151        let a = r#"<?xml version="1.0"?><root xmlns:foo="http://foo-a.com"><child/></root>"#;
1152        let b = r#"<?xml version="1.0"?><root xmlns:foo="http://foo-b.com"><child/></root>"#;
1153        let (_output, conflicts) = merge_xml(base, a, b);
1154        assert_eq!(
1155            conflicts, 1,
1156            "both changing same prefix differently should conflict"
1157        );
1158    }
1159
1160    #[test]
1161    fn test_merge_namespace_decls_both_change_same_value() {
1162        // A and B both change xmlns:foo to the SAME URI → no conflict
1163        let base = r#"<?xml version="1.0"?><root xmlns:foo="http://foo.com"><child/></root>"#;
1164        let a = r#"<?xml version="1.0"?><root xmlns:foo="http://foo-v2.com"><child/></root>"#;
1165        let b = r#"<?xml version="1.0"?><root xmlns:foo="http://foo-v2.com"><child/></root>"#;
1166        let (output, conflicts) = merge_xml(base, a, b);
1167        assert!(
1168            output.contains("http://foo-v2.com"),
1169            "merged URI should be the new value"
1170        );
1171        assert_eq!(conflicts, 0);
1172    }
1173
1174    #[test]
1175    fn test_merge_namespace_decls_unit() {
1176        // Direct unit test of merge_namespace_decls
1177        let matching = {
1178            let base = XmlParser::new(BaseNodeFactory)
1179                .parse_str("<root/>")
1180                .unwrap();
1181            let a = XmlParser::new(BranchNodeFactory)
1182                .parse_str("<root/>")
1183                .unwrap();
1184            let b = XmlParser::new(BranchNodeFactory)
1185                .parse_str("<root/>")
1186                .unwrap();
1187            TriMatching::new(a, base, b)
1188        };
1189        let merge = Merge::new(matching);
1190
1191        let mut base = HashMap::new();
1192        base.insert("foo".into(), "http://foo.com".into());
1193        base.insert("bar".into(), "http://bar.com".into());
1194
1195        // Deletion: a removes foo
1196        let mut a = HashMap::new();
1197        a.insert("bar".into(), "http://bar.com".into());
1198        let b = base.clone();
1199
1200        let result = merge.merge_namespace_decls(&base, &a, &b).unwrap();
1201        assert!(!result.contains_key("foo"));
1202        assert!(result.contains_key("bar"));
1203
1204        // Conflict: a removes foo, b changes foo
1205        let mut b_changed = HashMap::new();
1206        b_changed.insert("foo".into(), "http://foo-v2.com".into());
1207        b_changed.insert("bar".into(), "http://bar.com".into());
1208
1209        let result = merge.merge_namespace_decls(&base, &a, &b_changed);
1210        assert!(result.is_none(), "delete-vs-change should return None");
1211    }
1212}