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::{MatchType, NodeRef, XmlContent, XmlElement};
31
32/// Operation codes for merge list processing.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
34pub enum Operation {
35    /// No operation - node unchanged.
36    Nop,
37    /// Move inside child list.
38    MoveI,
39    /// Move far (to different parent).
40    MoveF,
41    /// Node deleted.
42    Delete,
43}
44
45/// The main merge struct that orchestrates the 3-way merge.
46pub struct Merge {
47    /// The tri-matching with base, left, and right trees.
48    matching: TriMatching,
49    /// Log of conflicts encountered during merge.
50    pub conflict_log: ConflictLog,
51    /// Log of edit operations performed.
52    pub edit_log: EditLog,
53    /// Current indentation level for formatted output.
54    indent: usize,
55}
56
57impl Merge {
58    /// Creates a new Merge from a TriMatching.
59    pub fn new(matching: TriMatching) -> Self {
60        Merge {
61            matching,
62            conflict_log: ConflictLog::new(),
63            edit_log: EditLog::new(),
64            indent: 0,
65        }
66    }
67
68    /// Returns a reference to the tri-matching.
69    pub fn matching(&self) -> &TriMatching {
70        &self.matching
71    }
72
73    /// Runs the 3-way merge and writes the result.
74    ///
75    /// The output is written as XML to the provided writer.
76    pub fn merge<W: Write>(&mut self, writer: &mut W) -> std::io::Result<()> {
77        // Start document
78        writeln!(writer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
79
80        // Run the tree merge
81        self.tree_merge(
82            Some(self.matching.left_root().clone()),
83            Some(self.matching.right_root().clone()),
84            writer,
85        )?;
86
87        Ok(())
88    }
89
90    /// Main recursive merge function.
91    ///
92    /// Merges the child lists of a and b, then recurses for each child.
93    fn tree_merge<W: Write>(
94        &mut self,
95        a: Option<NodeRef>,
96        b: Option<NodeRef>,
97        writer: &mut W,
98    ) -> std::io::Result<()> {
99        // Create merge lists for each branch
100        let mlist_a = a.as_ref().map(|node| self.make_merge_list(node));
101        let mlist_b = b.as_ref().map(|node| self.make_merge_list(node));
102
103        // Generate merge pair list
104        let merged = match (&mlist_a, &mlist_b) {
105            (Some(ma), Some(mb)) => self.make_merge_pair_list(ma.clone(), mb.clone()),
106            (Some(ma), None) => self.merge_list_to_pair_list(ma, None),
107            (None, Some(mb)) => self.merge_list_to_pair_list(mb, None),
108            (None, None) => MergePairList::new(),
109        };
110
111        // Process each merge pair
112        for pair in merged.pairs() {
113            let merged_node = self.merge_node_content(pair);
114
115            if let Some(content) = &merged_node {
116                match content {
117                    XmlContent::Text(text) => {
118                        // Output text content (with indentation only if on new line)
119                        let text_str: String = text.text().iter().collect();
120                        write!(
121                            writer,
122                            "{}{}",
123                            Self::indent_str_for(self.indent),
124                            escape_xml_text(&text_str)
125                        )?;
126                    }
127                    XmlContent::Comment(comment) => {
128                        // Output comment
129                        let comment_text: String = comment.text().iter().collect();
130                        writeln!(
131                            writer,
132                            "{}<!-- {} -->",
133                            Self::indent_str_for(self.indent),
134                            comment_text
135                        )?;
136                    }
137                    XmlContent::Element(elem) => {
138                        // Skip synthetic root
139                        if elem.qname() == "$ROOT$" {
140                            let partners = self.get_recursion_partners(pair);
141                            self.tree_merge(partners.first, partners.second, writer)?;
142                        } else {
143                            // Output element start tag
144                            self.write_start_element(writer, elem, self.indent)?;
145
146                            // Increment indent for children
147                            self.indent += 1;
148
149                            // Get recursion partners and recurse
150                            let partners = self.get_recursion_partners(pair);
151                            self.tree_merge(partners.first, partners.second, writer)?;
152
153                            // Decrement indent
154                            self.indent -= 1;
155
156                            // Output end tag
157                            writeln!(
158                                writer,
159                                "{}</{}>",
160                                Self::indent_str_for(self.indent),
161                                elem.qname()
162                            )?;
163                        }
164                    }
165                }
166            }
167        }
168
169        Ok(())
170    }
171
172    /// Creates a merge list from the children of a branch node.
173    fn make_merge_list(&mut self, parent: &NodeRef) -> MergeList {
174        MergeList::from_branch(parent, &mut self.edit_log)
175    }
176
177    /// Merges two merge lists into a merge pair list.
178    fn make_merge_pair_list(&mut self, mlist_a: MergeList, mlist_b: MergeList) -> MergePairList {
179        let mut mlist_a = mlist_a;
180        let mut mlist_b = mlist_b;
181
182        // Remove deleted or far-moved nodes
183        self.remove_deleted_or_moved(&mut mlist_a, &mut mlist_b);
184
185        // Checkpoint for potential rollback
186        self.edit_log.checkpoint();
187
188        // Lists should now have the same entries
189        if mlist_a.entry_count() != mlist_b.entry_count() {
190            // Fallback to simpler merge
191            self.edit_log.rewind();
192            let is_left = mlist_a
193                .entry_parent()
194                .map(|p| BranchNode::is_left_tree(&p))
195                .unwrap_or(true);
196            return if is_left {
197                self.merge_list_to_pair_list(&mlist_a, Some(&mlist_b))
198            } else {
199                self.merge_list_to_pair_list(&mlist_b, Some(&mlist_a))
200            };
201        }
202
203        let mut merged = MergePairList::new();
204        let mut pos_a = 0usize;
205        let mut pos_b = 0usize;
206
207        loop {
208            let ea = mlist_a.entry(pos_a);
209            let eb = mlist_b.entry(pos_b);
210
211            if ea.is_none() || eb.is_none() {
212                break;
213            }
214
215            let ea = ea.unwrap();
216            let eb = eb.unwrap();
217
218            // Add hangons from ea
219            for hangon in ea.hangons() {
220                let partner = hangon.borrow().first_partner(MatchType::FULL);
221                merged.append(Some(hangon.clone()), partner);
222                self.log_hangon_struct_ops(hangon);
223            }
224
225            // Add hangons from eb (if not equal to ea's hangons)
226            if !self.check_hangon_combine(ea, eb) {
227                for hangon in eb.hangons() {
228                    let partner = hangon.borrow().first_partner(MatchType::FULL);
229                    merged.append(Some(hangon.clone()), partner);
230                    self.log_hangon_struct_ops(hangon);
231                }
232            }
233
234            // Determine next positions
235            let next_a: Option<usize>;
236            let next_b: Option<usize>;
237
238            let ea_locked = ea.locked && mlist_a.entry(pos_a + 1).is_some_and(|e| e.locked);
239            let eb_locked = eb.locked && mlist_b.entry(pos_b + 1).is_some_and(|e| e.locked);
240
241            if !ea_locked && !eb_locked {
242                // No locking, both advance
243                next_a = Some(pos_a + 1);
244                next_b = Some(pos_b + 1);
245            } else if ea_locked && !eb_locked {
246                next_a = Some(pos_a + 1);
247                next_b = mlist_b.find_partner_index(mlist_a.entry(pos_a + 1));
248            } else if !ea_locked && eb_locked {
249                next_b = Some(pos_b + 1);
250                next_a = mlist_a.find_partner_index(mlist_b.entry(pos_b + 1));
251            } else {
252                // Both locked - check for conflict
253                let partner_b = mlist_b.find_partner_index(mlist_a.entry(pos_a + 1));
254                if partner_b != Some(pos_b + 1) {
255                    // Conflict - fallback
256                    self.conflict_log.add_list_conflict(
257                        ConflictType::Move,
258                        "Conflicting moves inside child list",
259                        ea.node().and_then(|n| BranchNode::base_match(&n)),
260                        ea.node(),
261                        eb.node(),
262                    );
263                    self.edit_log.rewind();
264                    let is_left = mlist_a
265                        .entry_parent()
266                        .map(|p| BranchNode::is_left_tree(&p))
267                        .unwrap_or(true);
268                    return if is_left {
269                        self.merge_list_to_pair_list(&mlist_a, Some(&mlist_b))
270                    } else {
271                        self.merge_list_to_pair_list(&mlist_b, Some(&mlist_a))
272                    };
273                }
274                next_a = Some(pos_a + 1);
275                next_b = partner_b;
276            }
277
278            pos_a = next_a.unwrap_or(pos_a + 1);
279            pos_b = next_b.unwrap_or(pos_b + 1);
280
281            // Check if done (at END marker)
282            if mlist_a.is_end(pos_a) || mlist_b.is_end(pos_b) {
283                break;
284            }
285
286            // Add the main entry pair
287            let ea = mlist_a.entry(pos_a);
288            let eb = mlist_b.entry(pos_b);
289            if let (Some(ea), Some(eb)) = (ea, eb) {
290                merged.append(ea.node(), eb.node());
291                self.log_entry_struct_ops(ea, eb);
292            }
293        }
294
295        self.edit_log.commit();
296        merged
297    }
298
299    /// Converts a single merge list to a pair list.
300    fn merge_list_to_pair_list(
301        &mut self,
302        mlist_a: &MergeList,
303        mlist_b: Option<&MergeList>,
304    ) -> MergePairList {
305        let mut merged = MergePairList::new();
306
307        for (i, entry) in mlist_a.entries().iter().enumerate() {
308            // Skip START marker but process its hangons
309            if i > 0 && !mlist_a.is_end(i) {
310                let node = entry.node();
311                let partner = node
312                    .as_ref()
313                    .and_then(|n| n.borrow().first_partner(MatchType::FULL));
314                merged.append(node, partner);
315                if let Some(n) = entry.node() {
316                    self.log_hangon_struct_ops(&n);
317                }
318            }
319
320            // Add hangons
321            for hangon in entry.hangons() {
322                let partner = hangon.borrow().first_partner(MatchType::FULL);
323                merged.append(Some(hangon.clone()), partner);
324                self.log_hangon_struct_ops(hangon);
325            }
326
327            // Add hangons from mlist_b if present
328            if let Some(mb) = mlist_b {
329                if let Some(partner_entry) = mb.find_partner(entry) {
330                    if !self.check_hangon_combine(entry, partner_entry) {
331                        for hangon in partner_entry.hangons() {
332                            let partner = hangon.borrow().first_partner(MatchType::FULL);
333                            merged.append(Some(hangon.clone()), partner);
334                            self.log_hangon_struct_ops(hangon);
335                        }
336                    }
337                }
338            }
339        }
340
341        merged
342    }
343
344    /// Removes deleted or far-moved nodes from merge lists.
345    fn remove_deleted_or_moved(&mut self, mlist_a: &mut MergeList, mlist_b: &mut MergeList) {
346        let base_parent = mlist_a
347            .entry_parent()
348            .and_then(|p| BranchNode::base_match(&p));
349
350        if base_parent.is_none() {
351            return;
352        }
353
354        let base_parent = base_parent.unwrap();
355        let child_count = base_parent.borrow().child_count();
356
357        for i in 0..child_count {
358            let bn = base_parent.borrow().child(i).cloned();
359            if bn.is_none() {
360                continue;
361            }
362            let bn = bn.unwrap();
363
364            let op1 = self.get_operation(&bn, mlist_a);
365            let op2 = self.get_operation(&bn, mlist_b);
366
367            // Handle based on which operation is smaller
368            // The table is symmetric in terms of which list we apply actions to
369            if op1 <= op2 {
370                self.handle_operation_pair(op1, op2, &bn, mlist_a, mlist_b);
371            } else {
372                self.handle_operation_pair(op2, op1, &bn, mlist_b, mlist_a);
373            }
374        }
375    }
376
377    /// Handles a pair of operations on a base node.
378    fn handle_operation_pair(
379        &mut self,
380        op1: Operation,
381        op2: Operation,
382        bn: &NodeRef,
383        ml1: &mut MergeList,
384        _ml2: &mut MergeList,
385    ) {
386        use Operation::*;
387
388        match (op1, op2) {
389            (Nop, Nop) | (Nop, MoveI) | (MoveI, MoveI) | (Delete, Delete) => {
390                // OK cases - no action needed
391            }
392            (Nop, MoveF) | (Nop, Delete) => {
393                // Delete the node from ml1
394                if let Some(ix) = ml1.match_in_list(bn) {
395                    if op2 == Delete {
396                        // Check for modifications in deleted subtree
397                        if let Some(entry) = ml1.entry(ix) {
398                            if let Some(node) = entry.node() {
399                                if self.is_deletia_modified(&node, ml1) {
400                                    self.conflict_log.add_list_warning(
401                                        ConflictType::Delete,
402                                        "Modifications in deleted subtree",
403                                        Some(bn.clone()),
404                                        entry.node(),
405                                        None,
406                                    );
407                                }
408                            }
409                        }
410                        self.edit_log.delete(Some(bn.clone()));
411                    }
412
413                    // Move hangons to predecessor
414                    ml1.move_hangons_to_predecessor(ix);
415                    ml1.remove_entry_at(ix);
416                }
417            }
418            (MoveI, MoveF) => {
419                // Conflict: node moved to different locations
420                if let Some(ix) = ml1.match_in_list(bn) {
421                    let node = ml1.entry(ix).and_then(|e| e.node());
422                    let partner = node
423                        .as_ref()
424                        .and_then(|n| n.borrow().first_partner(MatchType::FULL));
425                    self.conflict_log.add_list_conflict(
426                        ConflictType::Move,
427                        "Node moved to different locations",
428                        Some(bn.clone()),
429                        node,
430                        partner,
431                    );
432                    ml1.remove_entry_at(ix);
433                }
434            }
435            (MoveI, Delete) => {
436                // Conflict: node moved and deleted
437                if let Some(ix) = ml1.match_in_list(bn) {
438                    let node = ml1.entry(ix).and_then(|e| e.node());
439                    self.conflict_log.add_list_conflict(
440                        ConflictType::Move,
441                        "Node moved and deleted",
442                        Some(bn.clone()),
443                        node,
444                        None,
445                    );
446                    self.edit_log.delete(Some(bn.clone()));
447                    ml1.remove_entry_at(ix);
448                }
449            }
450            (MoveF, MoveF) => {
451                // Check for movef-movef conflict
452                if self.is_movef_movef_conflict(bn) {
453                    self.conflict_log.add_list_conflict(
454                        ConflictType::Move,
455                        "Node moved to different locations",
456                        Some(bn.clone()),
457                        BaseNode::left(bn).borrow().full_match(),
458                        BaseNode::right(bn).borrow().full_match(),
459                    );
460                }
461            }
462            (MoveF, Delete) => {
463                // Conflict: moved and deleted - keep the move
464                self.conflict_log.add_list_conflict(
465                    ConflictType::Move,
466                    "Node moved and deleted",
467                    Some(bn.clone()),
468                    BaseNode::left(bn).borrow().full_match(),
469                    BaseNode::right(bn).borrow().full_match(),
470                );
471            }
472            _ => {}
473        }
474    }
475
476    /// Gets the operation on a base node in a merge list.
477    fn get_operation(&self, bn: &NodeRef, ml: &MergeList) -> Operation {
478        if let Some(pos) = ml.match_in_list(bn) {
479            if ml.entry(pos).is_some_and(|e| e.moved) {
480                Operation::MoveI
481            } else {
482                Operation::Nop
483            }
484        } else {
485            // Check if moved far or deleted
486            let is_left = ml
487                .entry_parent()
488                .map(|p| BranchNode::is_left_tree(&p))
489                .unwrap_or(true);
490
491            let copies = if is_left {
492                BaseNode::left(bn)
493            } else {
494                BaseNode::right(bn)
495            };
496
497            if copies.borrow().match_count() == 0 {
498                Operation::Delete
499            } else {
500                Operation::MoveF
501            }
502        }
503    }
504
505    /// Checks if a deleted subtree has modifications.
506    fn is_deletia_modified(&self, n: &NodeRef, ml: &MergeList) -> bool {
507        let base_match = BranchNode::base_match(n);
508        if base_match.is_none() {
509            return true; // Inserted node
510        }
511
512        let base_match = base_match.unwrap();
513        if self.get_operation(&base_match, ml) != Operation::Nop {
514            return true; // Node was moved
515        }
516
517        if BranchNode::base_match_type(n) != MatchType::FULL {
518            return true; // Structural or content modification
519        }
520
521        // Check if deleted in other branch
522        if let Some(partners) = BranchNode::partners(n) {
523            if partners.borrow().match_count() == 0 {
524                if !self.matches(n, &base_match) {
525                    return true; // Updated
526                }
527                // Check children recursively
528                for i in 0..n.borrow().child_count() {
529                    if let Some(child) = n.borrow().child(i).cloned() {
530                        // Would need child's merge list - simplify for now
531                        if BranchNode::base_match_type(&child) != MatchType::FULL {
532                            return true;
533                        }
534                    }
535                }
536            }
537        }
538
539        false
540    }
541
542    /// Checks for movef-movef conflict.
543    ///
544    /// A MoveF-MoveF conflict occurs when both branches move the same base node
545    /// to different parent locations. Returns true if the parents differ (conflict).
546    fn is_movef_movef_conflict(&self, bn: &NodeRef) -> bool {
547        // Get left/right branch copies of base node
548        let left_node = BaseNode::left(bn).borrow().full_match();
549        let right_node = BaseNode::right(bn).borrow().full_match();
550
551        let (left_node, right_node) = match (left_node, right_node) {
552            (Some(l), Some(r)) => (l, r),
553            _ => return false,
554        };
555
556        // Get parents
557        let left_parent = BranchNode::parent(&left_node);
558        let right_parent = BranchNode::parent(&right_node);
559
560        match (left_parent, right_parent) {
561            (Some(lp), Some(rp)) => {
562                let lp_base = BranchNode::base_match(&lp);
563                let rp_base = BranchNode::base_match(&rp);
564                match (lp_base, rp_base) {
565                    (Some(lb), Some(rb)) => lb.borrow().id() != rb.borrow().id(),
566                    _ => true, // Inserted parent = conflict
567                }
568            }
569            (None, None) => false, // Both at root
570            _ => true,             // One at root, one not
571        }
572    }
573
574    /// Merges the content of nodes in a merge pair.
575    fn merge_node_content(&mut self, pair: &MergePair) -> Option<XmlContent> {
576        let n1 = pair.first.as_ref();
577        let n2 = pair.second.as_ref();
578
579        match (n1, n2) {
580            (None, None) => None,
581            (Some(n), None) | (None, Some(n)) => n.borrow().content().cloned(),
582            (Some(n1), Some(n2)) => {
583                let n1_content = n1.borrow().is_match(MatchType::CONTENT);
584                let n2_content = n2.borrow().is_match(MatchType::CONTENT);
585
586                if n1_content {
587                    if !n2_content {
588                        self.edit_log.update(Some(n2.clone()));
589                        n2.borrow().content().cloned()
590                    } else {
591                        self.content_merge(n1, n2)
592                    }
593                } else if n2_content {
594                    self.edit_log.update(Some(n1.clone()));
595                    n1.borrow().content().cloned()
596                } else {
597                    // Neither matches content - forced merge
598                    self.content_merge(n1, n2)
599                }
600            }
601        }
602    }
603
604    /// Merges content of two nodes.
605    fn content_merge(&mut self, a: &NodeRef, b: &NodeRef) -> Option<XmlContent> {
606        let base_a = BranchNode::base_match(a);
607        let base_b = BranchNode::base_match(b);
608
609        let a_updated = base_a.as_ref().is_none_or(|ba| !self.matches(a, ba));
610        let b_updated = base_b.as_ref().is_none_or(|bb| !self.matches(b, bb));
611
612        if a_updated && b_updated {
613            // Both updated - check if same update
614            if self.matches(a, b) {
615                self.conflict_log.add_node_warning(
616                    ConflictType::Update,
617                    "Node updated in both branches, but updates are equal",
618                    base_a.clone(),
619                    Some(a.clone()),
620                    Some(b.clone()),
621                );
622                self.edit_log.update(Some(a.clone()));
623                return a.borrow().content().cloned();
624            }
625
626            // Try element merge
627            if let Some(merged) = self.merge_elements(base_a.as_ref(), a, b) {
628                return Some(merged);
629            }
630
631            // Conflict - use first branch
632            self.conflict_log.add_node_conflict(
633                ConflictType::Update,
634                "Node updated in both branches, using branch 1",
635                base_a,
636                Some(a.clone()),
637                Some(b.clone()),
638            );
639            self.edit_log.update(Some(a.clone()));
640            a.borrow().content().cloned()
641        } else if b_updated {
642            self.edit_log.update(Some(b.clone()));
643            b.borrow().content().cloned()
644        } else if a_updated {
645            self.edit_log.update(Some(a.clone()));
646            a.borrow().content().cloned()
647        } else {
648            a.borrow().content().cloned()
649        }
650    }
651
652    /// Attempts to merge two element nodes.
653    fn merge_elements(
654        &self,
655        base: Option<&NodeRef>,
656        a: &NodeRef,
657        b: &NodeRef,
658    ) -> Option<XmlContent> {
659        let base_content = base.and_then(|n| n.borrow().content().cloned());
660        let a_content = a.borrow().content().cloned();
661        let b_content = b.borrow().content().cloned();
662
663        match (&base_content, &a_content, &b_content) {
664            (
665                Some(XmlContent::Element(base_elem)),
666                Some(XmlContent::Element(a_elem)),
667                Some(XmlContent::Element(b_elem)),
668            ) => {
669                // Determine tag name
670                let tag_name = if base_elem.qname() == b_elem.qname() {
671                    a_elem.qname().to_string()
672                } else if base_elem.qname() == a_elem.qname() {
673                    b_elem.qname().to_string()
674                } else {
675                    return None; // Both changed tag name
676                };
677
678                // Merge attributes
679                self.merge_attributes(
680                    base_elem.attributes(),
681                    a_elem.attributes(),
682                    b_elem.attributes(),
683                )
684                .map(|merged_attrs| XmlContent::Element(XmlElement::new(tag_name, merged_attrs)))
685            }
686            _ => None,
687        }
688    }
689
690    /// Merges attributes from base, a, and b.
691    fn merge_attributes(
692        &self,
693        base: &std::collections::HashMap<String, String>,
694        a: &std::collections::HashMap<String, String>,
695        b: &std::collections::HashMap<String, String>,
696    ) -> Option<std::collections::HashMap<String, String>> {
697        let mut merged = std::collections::HashMap::new();
698        let mut deletia = std::collections::HashSet::new();
699
700        // Check for deleted attributes with conflicts
701        for (key, base_val) in base {
702            let in_a = a.get(key);
703            let in_b = b.get(key);
704
705            match (in_a, in_b) {
706                (None, Some(b_val)) if base_val != b_val => return None, // Deleted and changed
707                (Some(a_val), None) if base_val != a_val => return None, // Deleted and changed
708                (None, _) | (_, None) => {
709                    deletia.insert(key.clone());
710                }
711                _ => {}
712            }
713        }
714
715        // Build merged attributes from a
716        for (key, a_val) in a {
717            if deletia.contains(key) {
718                continue;
719            }
720
721            if let Some(b_val) = b.get(key) {
722                let base_val = base.get(key);
723                if base_val == Some(b_val) {
724                    // A possibly updated
725                    merged.insert(key.clone(), a_val.clone());
726                } else if base_val == Some(a_val) {
727                    // B possibly updated
728                    merged.insert(key.clone(), b_val.clone());
729                } else if a_val != b_val {
730                    // Both changed differently
731                    return None;
732                } else {
733                    merged.insert(key.clone(), a_val.clone());
734                }
735            } else {
736                // Insert from a
737                merged.insert(key.clone(), a_val.clone());
738            }
739        }
740
741        // Add inserts from b
742        for (key, b_val) in b {
743            if !deletia.contains(key) && !a.contains_key(key) {
744                merged.insert(key.clone(), b_val.clone());
745            }
746        }
747
748        Some(merged)
749    }
750
751    /// Gets partners for recursion.
752    fn get_recursion_partners(&self, pair: &MergePair) -> RecursionPartners {
753        let n1 = pair.first.as_ref();
754        let n2 = pair.second.as_ref();
755
756        match (n1, n2) {
757            (None, _) | (_, None) => RecursionPartners {
758                first: pair.first.clone(),
759                second: pair.second.clone(),
760            },
761            (Some(n1), Some(n2)) => {
762                let n1_children = n1.borrow().is_match(MatchType::CHILDREN);
763                let n2_children = n2.borrow().is_match(MatchType::CHILDREN);
764
765                if n1_children && n2_children {
766                    RecursionPartners {
767                        first: Some(n1.clone()),
768                        second: Some(n2.clone()),
769                    }
770                } else if n1_children && n2.borrow().is_match(MatchType::CONTENT) {
771                    RecursionPartners {
772                        first: None,
773                        second: Some(n2.clone()),
774                    }
775                } else if n1.borrow().is_match(MatchType::CONTENT) && n2_children {
776                    RecursionPartners {
777                        first: Some(n1.clone()),
778                        second: None,
779                    }
780                } else {
781                    RecursionPartners {
782                        first: Some(n1.clone()),
783                        second: Some(n2.clone()),
784                    }
785                }
786            }
787        }
788    }
789
790    /// Checks if two nodes have equal content.
791    fn matches(&self, a: &NodeRef, b: &NodeRef) -> bool {
792        let a_borrowed = a.borrow();
793        let b_borrowed = b.borrow();
794        let a_content = a_borrowed.content();
795        let b_content = b_borrowed.content();
796
797        match (a_content, b_content) {
798            (Some(ac), Some(bc)) => ac.content_equals(bc),
799            _ => false,
800        }
801    }
802
803    /// Logs structural operations for a hangon node.
804    fn log_hangon_struct_ops(&mut self, n: &NodeRef) {
805        if !BranchNode::has_base_match(n) {
806            self.edit_log.insert(Some(n.clone()));
807        } else {
808            let base = BranchNode::base_match(n).unwrap();
809            let is_left = BranchNode::is_left_tree(n);
810
811            let copies = if is_left {
812                BaseNode::left(&base)
813            } else {
814                BaseNode::right(&base)
815            };
816
817            if copies.borrow().match_count() > 1 {
818                self.edit_log.copy(Some(n.clone()));
819            } else {
820                self.edit_log.r#move(Some(n.clone()));
821            }
822        }
823    }
824
825    /// Logs structural operations for merge entries.
826    fn log_entry_struct_ops(&mut self, e1: &MergeEntry, e2: &MergeEntry) {
827        if e1.moved {
828            if let Some(n) = e1.node() {
829                self.edit_log.r#move(Some(n));
830            }
831        } else if e2.moved {
832            if let Some(n) = e2.node() {
833                self.edit_log.r#move(Some(n));
834            }
835        }
836    }
837
838    /// Checks if hangons from two entries are equal.
839    /// Returns true only if hangons match exactly (same count and content).
840    /// When false, the caller should add eb's hangons to the merge list.
841    fn check_hangon_combine(&mut self, ea: &MergeEntry, eb: &MergeEntry) -> bool {
842        // If ea has no hangons, return false so eb's hangons are added
843        if ea.hangons().is_empty() {
844            return false;
845        }
846
847        if ea.hangons().len() != eb.hangons().len() {
848            if !ea.hangons().is_empty() && !eb.hangons().is_empty() {
849                self.conflict_log.add_list_warning(
850                    ConflictType::Insert,
851                    "Insertions/copies in both branches",
852                    ea.node().and_then(|n| BranchNode::base_match(&n)),
853                    ea.node(),
854                    eb.node(),
855                );
856            }
857            return false;
858        }
859
860        // Check if hangons match exactly
861        let mut equal = true;
862        for (ha, hb) in ea.hangons().iter().zip(eb.hangons().iter()) {
863            if !self.tree_matches(ha, hb) {
864                equal = false;
865                break;
866            }
867        }
868
869        if equal {
870            self.conflict_log.add_list_warning(
871                ConflictType::Insert,
872                "Equal insertions/copies in both branches",
873                ea.node().and_then(|n| BranchNode::base_match(&n)),
874                ea.node(),
875                eb.node(),
876            );
877        } else if !ea.hangons().is_empty() {
878            self.conflict_log.add_list_warning(
879                ConflictType::Insert,
880                "Insertions/copies in both branches, sequencing",
881                ea.node().and_then(|n| BranchNode::base_match(&n)),
882                ea.node(),
883                eb.node(),
884            );
885        }
886
887        equal
888    }
889
890    /// Checks if entire subtrees match exactly.
891    fn tree_matches(&self, a: &NodeRef, b: &NodeRef) -> bool {
892        if !self.matches(a, b) {
893            return false;
894        }
895
896        let a_count = a.borrow().child_count();
897        let b_count = b.borrow().child_count();
898
899        if a_count != b_count {
900            return false;
901        }
902
903        for i in 0..a_count {
904            let a_child = a.borrow().child(i).cloned();
905            let b_child = b.borrow().child(i).cloned();
906
907            match (a_child, b_child) {
908                (Some(ac), Some(bc)) => {
909                    if !self.tree_matches(&ac, &bc) {
910                        return false;
911                    }
912                }
913                _ => return false,
914            }
915        }
916
917        true
918    }
919
920    /// Writes an element start tag.
921    fn write_start_element<W: Write>(
922        &self,
923        writer: &mut W,
924        elem: &XmlElement,
925        indent: usize,
926    ) -> std::io::Result<()> {
927        write!(writer, "{}<{}", Self::indent_str_for(indent), elem.qname())?;
928
929        // Write attributes (sorted for deterministic output)
930        let mut attrs: Vec<(&String, &String)> = elem.attributes().iter().collect();
931        attrs.sort_by(|a, b| a.0.cmp(b.0));
932
933        for (name, value) in attrs {
934            write!(writer, " {}=\"{}\"", name, escape_xml_attr(value))?;
935        }
936
937        writeln!(writer, ">")?;
938        Ok(())
939    }
940
941    /// Returns the indentation string for a given level.
942    fn indent_str_for(level: usize) -> String {
943        "  ".repeat(level)
944    }
945}
946
947/// Partners for recursion.
948struct RecursionPartners {
949    first: Option<NodeRef>,
950    second: Option<NodeRef>,
951}
952
953/// Escapes special characters in XML text content.
954fn escape_xml_text(s: &str) -> String {
955    s.replace('&', "&amp;")
956        .replace('<', "&lt;")
957        .replace('>', "&gt;")
958}
959
960/// Escapes special characters in XML attribute values.
961fn escape_xml_attr(s: &str) -> String {
962    s.replace('&', "&amp;")
963        .replace('<', "&lt;")
964        .replace('>', "&gt;")
965        .replace('"', "&quot;")
966        .replace('\'', "&apos;")
967}
968
969impl crate::node::NodeInner {
970    /// Returns the first partner that matches the given type.
971    pub fn first_partner(&self, type_flags: MatchType) -> Option<NodeRef> {
972        if let crate::node::NodeKind::Branch {
973            match_type,
974            partners,
975            ..
976        } = self.kind()
977        {
978            if !match_type.intersects(type_flags) {
979                return None;
980            }
981
982            if let Some(partners) = partners {
983                let borrowed = partners.borrow();
984                for candidate in borrowed.matches() {
985                    if BranchNode::base_match_type(candidate).intersects(type_flags) {
986                        return Some(candidate.clone());
987                    }
988                }
989            }
990        }
991        None
992    }
993
994    /// Returns true if the match type includes the given flags.
995    pub fn is_match(&self, type_flags: MatchType) -> bool {
996        if let crate::node::NodeKind::Branch { match_type, .. } = self.kind() {
997            match_type.intersects(type_flags)
998        } else {
999            false
1000        }
1001    }
1002}