Skip to main content

xml_3dm/merge/
conflict_log.rs

1//! Conflict logging for the merge algorithm.
2//!
3//! This module provides structures for tracking conflicts and warnings
4//! that occur during the 3-way merge process.
5
6use std::io::Write;
7
8use crate::node::NodeRef;
9
10/// Types of conflicts that can occur during merge.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ConflictType {
13    /// Content update conflict.
14    Update,
15    /// Node deletion conflict.
16    Delete,
17    /// Node insertion conflict.
18    Insert,
19    /// Node move conflict.
20    Move,
21}
22
23impl ConflictType {
24    /// Returns the XML tag name for this conflict type.
25    pub fn tag_name(&self) -> &'static str {
26        match self {
27            ConflictType::Update => "update",
28            ConflictType::Delete => "delete",
29            ConflictType::Insert => "insert",
30            ConflictType::Move => "move",
31        }
32    }
33}
34
35/// A single conflict or warning entry.
36#[derive(Debug, Clone)]
37pub struct ConflictEntry {
38    /// The type of conflict.
39    pub conflict_type: ConflictType,
40    /// Description of the conflict.
41    pub text: String,
42    /// The base node involved (if any).
43    pub base: Option<NodeRef>,
44    /// The branch 1 (left) node involved (if any).
45    pub branch1: Option<NodeRef>,
46    /// The branch 2 (right) node involved (if any).
47    pub branch2: Option<NodeRef>,
48    /// Path in the merged tree.
49    pub merge_path: String,
50    /// Whether this is a list-level conflict (vs node-level).
51    pub is_list: bool,
52}
53
54/// Log of conflicts and warnings encountered during merge.
55#[derive(Debug, Default)]
56pub struct ConflictLog {
57    /// List of conflicts (merge failures).
58    conflicts: Vec<ConflictEntry>,
59    /// List of warnings (potential issues but merge succeeded).
60    warnings: Vec<ConflictEntry>,
61    /// Current path in the merge tree.
62    current_path: Vec<String>,
63}
64
65impl ConflictLog {
66    /// Creates a new empty conflict log.
67    pub fn new() -> Self {
68        ConflictLog {
69            conflicts: Vec::new(),
70            warnings: Vec::new(),
71            current_path: Vec::new(),
72        }
73    }
74
75    /// Pushes a path component onto the current path.
76    pub fn push_path(&mut self, component: &str) {
77        self.current_path.push(component.to_string());
78    }
79
80    /// Pops a path component from the current path.
81    pub fn pop_path(&mut self) {
82        self.current_path.pop();
83    }
84
85    /// Returns the current path as a string.
86    pub fn path_string(&self) -> String {
87        if self.current_path.is_empty() {
88            "/".to_string()
89        } else {
90            format!("/{}", self.current_path.join("/"))
91        }
92    }
93
94    /// Adds a list-level conflict.
95    pub fn add_list_conflict(
96        &mut self,
97        conflict_type: ConflictType,
98        text: &str,
99        base: Option<NodeRef>,
100        branch_a: Option<NodeRef>,
101        branch_b: Option<NodeRef>,
102    ) {
103        self.add(true, false, conflict_type, text, base, branch_a, branch_b);
104    }
105
106    /// Adds a list-level warning.
107    pub fn add_list_warning(
108        &mut self,
109        conflict_type: ConflictType,
110        text: &str,
111        base: Option<NodeRef>,
112        branch_a: Option<NodeRef>,
113        branch_b: Option<NodeRef>,
114    ) {
115        self.add(true, true, conflict_type, text, base, branch_a, branch_b);
116    }
117
118    /// Adds a node-level conflict.
119    pub fn add_node_conflict(
120        &mut self,
121        conflict_type: ConflictType,
122        text: &str,
123        base: Option<NodeRef>,
124        branch_a: Option<NodeRef>,
125        branch_b: Option<NodeRef>,
126    ) {
127        self.add(false, false, conflict_type, text, base, branch_a, branch_b);
128    }
129
130    /// Adds a node-level warning.
131    pub fn add_node_warning(
132        &mut self,
133        conflict_type: ConflictType,
134        text: &str,
135        base: Option<NodeRef>,
136        branch_a: Option<NodeRef>,
137        branch_b: Option<NodeRef>,
138    ) {
139        self.add(false, true, conflict_type, text, base, branch_a, branch_b);
140    }
141
142    /// Internal method to add an entry.
143    #[allow(clippy::too_many_arguments)]
144    fn add(
145        &mut self,
146        is_list: bool,
147        is_warning: bool,
148        conflict_type: ConflictType,
149        text: &str,
150        base: Option<NodeRef>,
151        branch_a: Option<NodeRef>,
152        branch_b: Option<NodeRef>,
153    ) {
154        // Normalize so branch1 is left tree, branch2 is right tree
155        let (branch1, branch2) = Self::normalize_branches(branch_a, branch_b);
156
157        let entry = ConflictEntry {
158            conflict_type,
159            text: text.to_string(),
160            base,
161            branch1,
162            branch2,
163            merge_path: self.path_string(),
164            is_list,
165        };
166
167        if is_warning {
168            self.warnings.push(entry);
169        } else {
170            self.conflicts.push(entry);
171        }
172    }
173
174    /// Normalizes branch references so branch1 is left and branch2 is right.
175    fn normalize_branches(
176        branch_a: Option<NodeRef>,
177        branch_b: Option<NodeRef>,
178    ) -> (Option<NodeRef>, Option<NodeRef>) {
179        use crate::node::BranchNode;
180
181        let (ba, bb) = match (branch_a, branch_b) {
182            (None, b) => (b, None),
183            (a, b) => (a, b),
184        };
185
186        match &ba {
187            Some(node) if BranchNode::is_left_tree(node) => (ba, bb),
188            Some(_) => (bb, ba),
189            None => (None, None),
190        }
191    }
192
193    /// Returns true if there are any conflicts.
194    pub fn has_conflicts(&self) -> bool {
195        !self.conflicts.is_empty()
196    }
197
198    /// Returns the number of conflicts.
199    pub fn conflict_count(&self) -> usize {
200        self.conflicts.len()
201    }
202
203    /// Returns the number of warnings.
204    pub fn warning_count(&self) -> usize {
205        self.warnings.len()
206    }
207
208    /// Returns a reference to the conflicts.
209    pub fn conflicts(&self) -> &[ConflictEntry] {
210        &self.conflicts
211    }
212
213    /// Returns a reference to the warnings.
214    pub fn warnings(&self) -> &[ConflictEntry] {
215        &self.warnings
216    }
217
218    /// Writes the conflict log as XML.
219    pub fn write_xml<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
220        writeln!(writer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
221        writeln!(writer, "<conflictlist>")?;
222
223        if !self.conflicts.is_empty() {
224            writeln!(writer, "  <conflicts>")?;
225            for entry in &self.conflicts {
226                self.write_entry(writer, entry, "    ")?;
227            }
228            writeln!(writer, "  </conflicts>")?;
229        }
230
231        if !self.warnings.is_empty() {
232            writeln!(writer, "  <warnings>")?;
233            for entry in &self.warnings {
234                self.write_entry(writer, entry, "    ")?;
235            }
236            writeln!(writer, "  </warnings>")?;
237        }
238
239        writeln!(writer, "</conflictlist>")?;
240
241        // Print summary to stderr like the Java version
242        if !self.conflicts.is_empty() {
243            eprintln!("MERGE FAILED: {} conflicts.", self.conflicts.len());
244        }
245        if !self.warnings.is_empty() {
246            eprintln!("Warning: {} conflict warnings.", self.warnings.len());
247        }
248
249        Ok(())
250    }
251
252    /// Writes a single conflict entry as XML.
253    fn write_entry<W: Write>(
254        &self,
255        writer: &mut W,
256        entry: &ConflictEntry,
257        indent: &str,
258    ) -> std::io::Result<()> {
259        let tag = entry.conflict_type.tag_name();
260
261        writeln!(writer, "{}<{}>", indent, tag)?;
262        writeln!(writer, "{}  {}", indent, escape_xml(&entry.text))?;
263
264        // Merged tree node
265        writeln!(
266            writer,
267            "{}  <node tree=\"merged\" path=\"{}\" />",
268            indent,
269            escape_xml(&entry.merge_path)
270        )?;
271
272        // Base node
273        if let Some(base) = &entry.base {
274            let path = get_node_path(base);
275            writeln!(
276                writer,
277                "{}  <node tree=\"base\" path=\"{}\" />",
278                indent,
279                escape_xml(&path)
280            )?;
281        }
282
283        // Branch 1 node
284        if let Some(b1) = &entry.branch1 {
285            let path = get_node_path(b1);
286            writeln!(
287                writer,
288                "{}  <node tree=\"branch1\" path=\"{}\" />",
289                indent,
290                escape_xml(&path)
291            )?;
292        }
293
294        // Branch 2 node
295        if let Some(b2) = &entry.branch2 {
296            let path = get_node_path(b2);
297            writeln!(
298                writer,
299                "{}  <node tree=\"branch2\" path=\"{}\" />",
300                indent,
301                escape_xml(&path)
302            )?;
303        }
304
305        writeln!(writer, "{}</{}>", indent, tag)?;
306        Ok(())
307    }
308}
309
310/// Escapes special characters in XML content.
311fn escape_xml(s: &str) -> String {
312    s.replace('&', "&amp;")
313        .replace('<', "&lt;")
314        .replace('>', "&gt;")
315        .replace('"', "&quot;")
316}
317
318/// Gets a path string for a node (for error reporting).
319fn get_node_path(node: &NodeRef) -> String {
320    use crate::node::XmlContent;
321
322    let mut parts = Vec::new();
323    let mut current = Some(node.clone());
324
325    while let Some(n) = current {
326        let borrowed = n.borrow();
327        let name = match borrowed.content() {
328            Some(XmlContent::Element(e)) => e.qname().to_string(),
329            Some(XmlContent::Text(_)) => "#text".to_string(),
330            Some(XmlContent::Comment(_)) => "#comment".to_string(),
331            Some(XmlContent::ProcessingInstruction(pi)) => format!("#pi-{}", pi.target()),
332            None => "#node".to_string(),
333        };
334
335        let pos = borrowed.child_pos();
336        if pos >= 0 {
337            parts.push(format!("{}[{}]", name, pos));
338        } else {
339            parts.push(name);
340        }
341
342        current = borrowed.parent().upgrade();
343    }
344
345    parts.reverse();
346    if parts.is_empty() {
347        "/".to_string()
348    } else {
349        format!("/{}", parts.join("/"))
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    #[test]
358    fn test_conflict_type_tag_names() {
359        assert_eq!(ConflictType::Update.tag_name(), "update");
360        assert_eq!(ConflictType::Delete.tag_name(), "delete");
361        assert_eq!(ConflictType::Insert.tag_name(), "insert");
362        assert_eq!(ConflictType::Move.tag_name(), "move");
363    }
364
365    #[test]
366    fn test_conflict_log_empty() {
367        let log = ConflictLog::new();
368        assert!(!log.has_conflicts());
369        assert_eq!(log.conflict_count(), 0);
370        assert_eq!(log.warning_count(), 0);
371    }
372
373    #[test]
374    fn test_conflict_log_path() {
375        let mut log = ConflictLog::new();
376        assert_eq!(log.path_string(), "/");
377
378        log.push_path("root");
379        assert_eq!(log.path_string(), "/root");
380
381        log.push_path("child");
382        assert_eq!(log.path_string(), "/root/child");
383
384        log.pop_path();
385        assert_eq!(log.path_string(), "/root");
386    }
387
388    #[test]
389    fn test_add_conflict() {
390        let mut log = ConflictLog::new();
391        log.add_list_conflict(ConflictType::Move, "Test conflict", None, None, None);
392
393        assert!(log.has_conflicts());
394        assert_eq!(log.conflict_count(), 1);
395        assert_eq!(log.warning_count(), 0);
396
397        let entry = &log.conflicts()[0];
398        assert_eq!(entry.conflict_type, ConflictType::Move);
399        assert_eq!(entry.text, "Test conflict");
400    }
401
402    #[test]
403    fn test_add_warning() {
404        let mut log = ConflictLog::new();
405        log.add_list_warning(ConflictType::Insert, "Test warning", None, None, None);
406
407        assert!(!log.has_conflicts());
408        assert_eq!(log.conflict_count(), 0);
409        assert_eq!(log.warning_count(), 1);
410
411        let entry = &log.warnings()[0];
412        assert_eq!(entry.conflict_type, ConflictType::Insert);
413    }
414}