1use std::io::Write;
7
8use crate::node::NodeRef;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ConflictType {
13 Update,
15 Delete,
17 Insert,
19 Move,
21}
22
23impl ConflictType {
24 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#[derive(Debug, Clone)]
37pub struct ConflictEntry {
38 pub conflict_type: ConflictType,
40 pub text: String,
42 pub base: Option<NodeRef>,
44 pub branch1: Option<NodeRef>,
46 pub branch2: Option<NodeRef>,
48 pub merge_path: String,
50 pub is_list: bool,
52}
53
54#[derive(Debug, Default)]
56pub struct ConflictLog {
57 conflicts: Vec<ConflictEntry>,
59 warnings: Vec<ConflictEntry>,
61 current_path: Vec<String>,
63}
64
65impl ConflictLog {
66 pub fn new() -> Self {
68 ConflictLog {
69 conflicts: Vec::new(),
70 warnings: Vec::new(),
71 current_path: Vec::new(),
72 }
73 }
74
75 pub fn push_path(&mut self, component: &str) {
77 self.current_path.push(component.to_string());
78 }
79
80 pub fn pop_path(&mut self) {
82 self.current_path.pop();
83 }
84
85 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 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 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 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 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 #[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 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 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 pub fn has_conflicts(&self) -> bool {
195 !self.conflicts.is_empty()
196 }
197
198 pub fn conflict_count(&self) -> usize {
200 self.conflicts.len()
201 }
202
203 pub fn warning_count(&self) -> usize {
205 self.warnings.len()
206 }
207
208 pub fn conflicts(&self) -> &[ConflictEntry] {
210 &self.conflicts
211 }
212
213 pub fn warnings(&self) -> &[ConflictEntry] {
215 &self.warnings
216 }
217
218 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 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 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 writeln!(
266 writer,
267 "{} <node tree=\"merged\" path=\"{}\" />",
268 indent,
269 escape_xml(&entry.merge_path)
270 )?;
271
272 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 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 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
310fn escape_xml(s: &str) -> String {
312 s.replace('&', "&")
313 .replace('<', "<")
314 .replace('>', ">")
315 .replace('"', """)
316}
317
318fn 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 None => "#node".to_string(),
332 };
333
334 let pos = borrowed.child_pos();
335 if pos >= 0 {
336 parts.push(format!("{}[{}]", name, pos));
337 } else {
338 parts.push(name);
339 }
340
341 current = borrowed.parent().upgrade();
342 }
343
344 parts.reverse();
345 if parts.is_empty() {
346 "/".to_string()
347 } else {
348 format!("/{}", parts.join("/"))
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn test_conflict_type_tag_names() {
358 assert_eq!(ConflictType::Update.tag_name(), "update");
359 assert_eq!(ConflictType::Delete.tag_name(), "delete");
360 assert_eq!(ConflictType::Insert.tag_name(), "insert");
361 assert_eq!(ConflictType::Move.tag_name(), "move");
362 }
363
364 #[test]
365 fn test_conflict_log_empty() {
366 let log = ConflictLog::new();
367 assert!(!log.has_conflicts());
368 assert_eq!(log.conflict_count(), 0);
369 assert_eq!(log.warning_count(), 0);
370 }
371
372 #[test]
373 fn test_conflict_log_path() {
374 let mut log = ConflictLog::new();
375 assert_eq!(log.path_string(), "/");
376
377 log.push_path("root");
378 assert_eq!(log.path_string(), "/root");
379
380 log.push_path("child");
381 assert_eq!(log.path_string(), "/root/child");
382
383 log.pop_path();
384 assert_eq!(log.path_string(), "/root");
385 }
386
387 #[test]
388 fn test_add_conflict() {
389 let mut log = ConflictLog::new();
390 log.add_list_conflict(ConflictType::Move, "Test conflict", None, None, None);
391
392 assert!(log.has_conflicts());
393 assert_eq!(log.conflict_count(), 1);
394 assert_eq!(log.warning_count(), 0);
395
396 let entry = &log.conflicts()[0];
397 assert_eq!(entry.conflict_type, ConflictType::Move);
398 assert_eq!(entry.text, "Test conflict");
399 }
400
401 #[test]
402 fn test_add_warning() {
403 let mut log = ConflictLog::new();
404 log.add_list_warning(ConflictType::Insert, "Test warning", None, None, None);
405
406 assert!(!log.has_conflicts());
407 assert_eq!(log.conflict_count(), 0);
408 assert_eq!(log.warning_count(), 1);
409
410 let entry = &log.warnings()[0];
411 assert_eq!(entry.conflict_type, ConflictType::Insert);
412 }
413}