1use std::fmt;
2use serde::Serialize;
3
4#[derive(Debug, Clone)]
11pub struct MarkerFormat {
12 pub marker_length: usize,
13 pub enhanced: bool,
14}
15
16impl Default for MarkerFormat {
17 fn default() -> Self {
18 Self { marker_length: 7, enhanced: true }
19 }
20}
21
22impl MarkerFormat {
23 pub fn standard(marker_length: usize) -> Self {
24 Self { marker_length, enhanced: false }
25 }
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum ConflictKind {
31 BothModified,
33 ModifyDelete { modified_in_ours: bool },
35 BothAdded,
37 RenameRename {
39 base_name: String,
40 ours_name: String,
41 theirs_name: String,
42 },
43}
44
45impl fmt::Display for ConflictKind {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 match self {
48 ConflictKind::BothModified => write!(f, "both modified"),
49 ConflictKind::ModifyDelete {
50 modified_in_ours: true,
51 } => write!(f, "modified in ours, deleted in theirs"),
52 ConflictKind::ModifyDelete {
53 modified_in_ours: false,
54 } => write!(f, "deleted in ours, modified in theirs"),
55 ConflictKind::BothAdded => write!(f, "both added"),
56 ConflictKind::RenameRename { base_name, ours_name, theirs_name } => {
57 write!(f, "both renamed: '{}' → ours '{}', theirs '{}'", base_name, ours_name, theirs_name)
58 }
59 }
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
71pub enum ConflictComplexity {
72 Text,
74 Syntax,
76 Functional,
78 TextSyntax,
80 TextFunctional,
82 SyntaxFunctional,
84 TextSyntaxFunctional,
86 Unknown,
88}
89
90impl ConflictComplexity {
91 pub fn resolution_hint(&self) -> &'static str {
93 match self {
94 ConflictComplexity::Text =>
95 "Cosmetic change on both sides. Pick either version or combine formatting.",
96 ConflictComplexity::Syntax =>
97 "Structural change (rename/retype). Check callers of this entity.",
98 ConflictComplexity::Functional =>
99 "Logic changed on both sides. Requires understanding intent of each change.",
100 ConflictComplexity::TextSyntax =>
101 "Renamed and reformatted. Prefer the structural change, verify formatting.",
102 ConflictComplexity::TextFunctional =>
103 "Logic and cosmetic changes overlap. Resolve logic first, then reformat.",
104 ConflictComplexity::SyntaxFunctional =>
105 "Structural and logic conflict. Both design and behavior differ.",
106 ConflictComplexity::TextSyntaxFunctional =>
107 "All three dimensions conflict. Manual review required.",
108 ConflictComplexity::Unknown =>
109 "Could not classify. Compare both versions manually.",
110 }
111 }
112}
113
114impl fmt::Display for ConflictComplexity {
115 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 match self {
117 ConflictComplexity::Text => write!(f, "T"),
118 ConflictComplexity::Syntax => write!(f, "S"),
119 ConflictComplexity::Functional => write!(f, "F"),
120 ConflictComplexity::TextSyntax => write!(f, "T+S"),
121 ConflictComplexity::TextFunctional => write!(f, "T+F"),
122 ConflictComplexity::SyntaxFunctional => write!(f, "S+F"),
123 ConflictComplexity::TextSyntaxFunctional => write!(f, "T+S+F"),
124 ConflictComplexity::Unknown => write!(f, "?"),
125 }
126 }
127}
128
129pub fn classify_conflict(base: Option<&str>, ours: Option<&str>, theirs: Option<&str>) -> ConflictComplexity {
131 let base = base.unwrap_or("");
132 let ours = ours.unwrap_or("");
133 let theirs = theirs.unwrap_or("");
134
135 let ours_diff = classify_change(base, ours);
137 let theirs_diff = classify_change(base, theirs);
138
139 let has_text = ours_diff.text || theirs_diff.text;
141 let has_syntax = ours_diff.syntax || theirs_diff.syntax;
142 let has_functional = ours_diff.functional || theirs_diff.functional;
143
144 match (has_text, has_syntax, has_functional) {
145 (true, false, false) => ConflictComplexity::Text,
146 (false, true, false) => ConflictComplexity::Syntax,
147 (false, false, true) => ConflictComplexity::Functional,
148 (true, true, false) => ConflictComplexity::TextSyntax,
149 (true, false, true) => ConflictComplexity::TextFunctional,
150 (false, true, true) => ConflictComplexity::SyntaxFunctional,
151 (true, true, true) => ConflictComplexity::TextSyntaxFunctional,
152 (false, false, false) => ConflictComplexity::Unknown,
153 }
154}
155
156struct ChangeDimensions {
157 text: bool,
158 syntax: bool,
159 functional: bool,
160}
161
162fn find_signature_end(lines: &[&str]) -> usize {
166 if lines.is_empty() {
167 return 0;
168 }
169 let mut depth: i32 = 0;
170 for (i, line) in lines.iter().enumerate() {
171 for ch in line.chars() {
172 match ch {
173 '(' => depth += 1,
174 ')' => depth -= 1,
175 '{' | ':' if depth <= 0 && i > 0 => {
176 return i + 1;
178 }
179 _ => {}
180 }
181 }
182 if depth <= 0 && i > 0 {
185 let trimmed = line.trim();
187 if trimmed.ends_with('{') || trimmed.ends_with(':') || trimmed.ends_with("->") {
188 return i + 1;
189 }
190 }
191 }
192 1
194}
195
196fn classify_change(base: &str, modified: &str) -> ChangeDimensions {
197 if base == modified {
198 return ChangeDimensions {
199 text: false,
200 syntax: false,
201 functional: false,
202 };
203 }
204
205 let base_lines: Vec<&str> = base.lines().collect();
206 let modified_lines: Vec<&str> = modified.lines().collect();
207
208 let mut has_comment_change = false;
209 let mut has_signature_change = false;
210 let mut has_body_change = false;
211
212 let base_sig_end = find_signature_end(&base_lines);
214 let mod_sig_end = find_signature_end(&modified_lines);
215
216 let base_sig: Vec<&str> = base_lines.iter().take(base_sig_end).copied().collect();
218 let mod_sig: Vec<&str> = modified_lines.iter().take(mod_sig_end).copied().collect();
219 if base_sig != mod_sig {
220 let all_comments = base_sig.iter().all(|l| is_comment_line(l))
221 && mod_sig.iter().all(|l| is_comment_line(l));
222 if all_comments {
223 has_comment_change = true;
224 } else {
225 has_signature_change = true;
226 }
227 }
228
229 let base_body: Vec<&str> = base_lines.iter().skip(base_sig_end).copied().collect();
231 let mod_body: Vec<&str> = modified_lines.iter().skip(mod_sig_end).copied().collect();
232
233 if base_body != mod_body {
234 let base_no_comments: Vec<&str> = base_body
236 .iter()
237 .filter(|l| !is_comment_line(l))
238 .copied()
239 .collect();
240 let mod_no_comments: Vec<&str> = mod_body
241 .iter()
242 .filter(|l| !is_comment_line(l))
243 .copied()
244 .collect();
245
246 if base_no_comments == mod_no_comments {
247 has_comment_change = true;
248 } else {
249 has_body_change = true;
250 }
251 }
252
253 ChangeDimensions {
254 text: has_comment_change,
255 syntax: has_signature_change,
256 functional: has_body_change,
257 }
258}
259
260fn is_comment_line(line: &str) -> bool {
261 let trimmed = line.trim();
262 trimmed.starts_with("//")
263 || trimmed.starts_with("/*")
264 || trimmed.starts_with("*")
265 || trimmed.starts_with("#")
266 || trimmed.starts_with("\"\"\"")
267 || trimmed.starts_with("'''")
268}
269
270#[derive(Debug, Clone)]
272pub struct EntityConflict {
273 pub entity_name: String,
274 pub entity_type: String,
275 pub kind: ConflictKind,
276 pub complexity: ConflictComplexity,
277 pub ours_content: Option<String>,
278 pub theirs_content: Option<String>,
279 pub base_content: Option<String>,
280}
281
282pub fn narrow_conflict_lines<'a>(
285 ours_lines: &'a [&'a str],
286 theirs_lines: &'a [&'a str],
287) -> (usize, usize) {
288 let prefix_len = ours_lines.iter()
290 .zip(theirs_lines.iter())
291 .take_while(|(a, b)| a == b)
292 .count();
293
294 let ours_remaining = ours_lines.len() - prefix_len;
296 let theirs_remaining = theirs_lines.len() - prefix_len;
297 let max_suffix = ours_remaining.min(theirs_remaining);
298 let suffix_len = ours_lines.iter().rev()
299 .zip(theirs_lines.iter().rev())
300 .take(max_suffix)
301 .take_while(|(a, b)| a == b)
302 .count();
303
304 (prefix_len, suffix_len)
305}
306
307impl EntityConflict {
308 pub fn to_conflict_markers(&self, fmt: &MarkerFormat) -> String {
316 let ours = self.ours_content.as_deref().unwrap_or("");
317 let theirs = self.theirs_content.as_deref().unwrap_or("");
318 let open = "<".repeat(fmt.marker_length);
319 let sep = "=".repeat(fmt.marker_length);
320 let close = ">".repeat(fmt.marker_length);
321
322 let ours_lines: Vec<&str> = ours.lines().collect();
323 let theirs_lines: Vec<&str> = theirs.lines().collect();
324
325 let (prefix_len, suffix_len) = narrow_conflict_lines(&ours_lines, &theirs_lines);
326
327 let has_narrowing = prefix_len > 0 || suffix_len > 0;
329 let ours_mid = &ours_lines[prefix_len..ours_lines.len() - suffix_len];
330 let theirs_mid = &theirs_lines[prefix_len..theirs_lines.len() - suffix_len];
331
332 let mut out = String::new();
333
334 if has_narrowing {
336 for line in &ours_lines[..prefix_len] {
337 out.push_str(line);
338 out.push('\n');
339 }
340 }
341
342 if fmt.enhanced {
344 let confidence = match &self.complexity {
345 ConflictComplexity::Text => "high",
346 ConflictComplexity::Syntax => "medium",
347 ConflictComplexity::Functional => "medium",
348 ConflictComplexity::TextSyntax => "medium",
349 ConflictComplexity::TextFunctional => "medium",
350 ConflictComplexity::SyntaxFunctional => "low",
351 ConflictComplexity::TextSyntaxFunctional => "low",
352 ConflictComplexity::Unknown => "unknown",
353 };
354 let label = format!(
355 "{} `{}` ({}, confidence: {})",
356 self.entity_type, self.entity_name, self.complexity, confidence
357 );
358 let hint = self.complexity.resolution_hint();
359 out.push_str(&format!("{} ours \u{2014} {}\n", open, label));
360 out.push_str(&format!("// hint: {}\n", hint));
361 } else {
362 out.push_str(&format!("{} ours\n", open));
363 }
364
365 if has_narrowing {
367 for line in ours_mid {
368 out.push_str(line);
369 out.push('\n');
370 }
371 } else {
372 out.push_str(ours);
373 if !ours.is_empty() && !ours.ends_with('\n') {
374 out.push('\n');
375 }
376 }
377
378 if !fmt.enhanced {
380 let base_marker = "|".repeat(fmt.marker_length);
381 out.push_str(&format!("{} base\n", base_marker));
382 let base = self.base_content.as_deref().unwrap_or("");
383 if has_narrowing {
384 let base_lines: Vec<&str> = base.lines().collect();
385 let base_prefix = prefix_len.min(base_lines.len());
387 let base_suffix = suffix_len.min(base_lines.len().saturating_sub(base_prefix));
388 for line in &base_lines[base_prefix..base_lines.len() - base_suffix] {
389 out.push_str(line);
390 out.push('\n');
391 }
392 } else {
393 out.push_str(base);
394 if !base.is_empty() && !base.ends_with('\n') {
395 out.push('\n');
396 }
397 }
398 }
399
400 out.push_str(&format!("{}\n", sep));
401
402 if has_narrowing {
404 for line in theirs_mid {
405 out.push_str(line);
406 out.push('\n');
407 }
408 } else {
409 out.push_str(theirs);
410 if !theirs.is_empty() && !theirs.ends_with('\n') {
411 out.push('\n');
412 }
413 }
414
415 if fmt.enhanced {
417 let confidence = match &self.complexity {
418 ConflictComplexity::Text => "high",
419 ConflictComplexity::Syntax => "medium",
420 ConflictComplexity::Functional => "medium",
421 ConflictComplexity::TextSyntax => "medium",
422 ConflictComplexity::TextFunctional => "medium",
423 ConflictComplexity::SyntaxFunctional => "low",
424 ConflictComplexity::TextSyntaxFunctional => "low",
425 ConflictComplexity::Unknown => "unknown",
426 };
427 let label = format!(
428 "{} `{}` ({}, confidence: {})",
429 self.entity_type, self.entity_name, self.complexity, confidence
430 );
431 out.push_str(&format!("{} theirs \u{2014} {}\n", close, label));
432 } else {
433 out.push_str(&format!("{} theirs\n", close));
434 }
435
436 if has_narrowing {
438 for line in &ours_lines[ours_lines.len() - suffix_len..] {
439 out.push_str(line);
440 out.push('\n');
441 }
442 }
443
444 out
445 }
446}
447
448#[derive(Debug, Clone)]
450pub struct ParsedConflict {
451 pub entity_name: String,
452 pub entity_kind: String,
453 pub complexity: ConflictComplexity,
454 pub confidence: String,
455 pub hint: String,
456 pub ours_content: String,
457 pub theirs_content: String,
458}
459
460pub fn parse_weave_conflicts(content: &str) -> Vec<ParsedConflict> {
465 let mut conflicts = Vec::new();
466 let lines: Vec<&str> = content.lines().collect();
467 let mut i = 0;
468
469 while i < lines.len() {
470 if lines[i].starts_with("<<<<<<< ours") {
472 let header = lines[i];
473 let (entity_kind, entity_name, complexity, confidence) = parse_conflict_header(header);
474
475 i += 1;
476
477 let mut hint = String::new();
479 if i < lines.len() && lines[i].starts_with("// hint: ") {
480 hint = lines[i].trim_start_matches("// hint: ").to_string();
481 i += 1;
482 }
483
484 let mut ours_lines = Vec::new();
486 while i < lines.len() && lines[i] != "=======" {
487 ours_lines.push(lines[i]);
488 i += 1;
489 }
490 i += 1; let mut theirs_lines = Vec::new();
494 while i < lines.len() && !lines[i].starts_with(">>>>>>> theirs") {
495 theirs_lines.push(lines[i]);
496 i += 1;
497 }
498 i += 1; let ours_content = if ours_lines.is_empty() {
501 String::new()
502 } else {
503 ours_lines.join("\n") + "\n"
504 };
505 let theirs_content = if theirs_lines.is_empty() {
506 String::new()
507 } else {
508 theirs_lines.join("\n") + "\n"
509 };
510
511 conflicts.push(ParsedConflict {
512 entity_name,
513 entity_kind,
514 complexity,
515 confidence,
516 hint,
517 ours_content,
518 theirs_content,
519 });
520 } else {
521 i += 1;
522 }
523 }
524
525 conflicts
526}
527
528fn parse_conflict_header(header: &str) -> (String, String, ConflictComplexity, String) {
529 let after_dash = header
531 .split('\u{2014}')
532 .nth(1)
533 .unwrap_or(header)
534 .trim();
535
536 let entity_kind = after_dash
538 .split('`')
539 .next()
540 .unwrap_or("")
541 .trim()
542 .to_string();
543
544 let entity_name = after_dash
546 .split('`')
547 .nth(1)
548 .unwrap_or("")
549 .to_string();
550
551 let paren_content = after_dash
553 .rsplit('(')
554 .next()
555 .unwrap_or("")
556 .trim_end_matches(')');
557
558 let parts: Vec<&str> = paren_content.split(',').map(|s| s.trim()).collect();
559 let complexity = match parts.first().copied().unwrap_or("") {
560 "T" => ConflictComplexity::Text,
561 "S" => ConflictComplexity::Syntax,
562 "F" => ConflictComplexity::Functional,
563 "T+S" => ConflictComplexity::TextSyntax,
564 "T+F" => ConflictComplexity::TextFunctional,
565 "S+F" => ConflictComplexity::SyntaxFunctional,
566 "T+S+F" => ConflictComplexity::TextSyntaxFunctional,
567 _ => ConflictComplexity::Unknown,
568 };
569
570 let confidence = parts
571 .iter()
572 .find(|p| p.starts_with("confidence:"))
573 .map(|p| p.trim_start_matches("confidence:").trim().to_string())
574 .unwrap_or_else(|| "unknown".to_string());
575
576 (entity_kind, entity_name, complexity, confidence)
577}
578
579#[derive(Debug, Clone, Default, Serialize)]
581pub struct MergeStats {
582 pub entities_unchanged: usize,
583 pub entities_ours_only: usize,
584 pub entities_theirs_only: usize,
585 pub entities_both_changed_merged: usize,
586 pub entities_conflicted: usize,
587 pub entities_added_ours: usize,
588 pub entities_added_theirs: usize,
589 pub entities_deleted: usize,
590 pub used_fallback: bool,
591 pub semantic_warnings: usize,
593 pub resolved_via_diffy: usize,
595 pub resolved_via_inner_merge: usize,
597}
598
599impl MergeStats {
600 pub fn has_conflicts(&self) -> bool {
601 self.entities_conflicted > 0
602 }
603
604 pub fn confidence(&self) -> &'static str {
607 if self.entities_conflicted > 0 {
608 "conflict"
609 } else if self.resolved_via_inner_merge > 0 || self.used_fallback {
610 "medium"
611 } else if self.resolved_via_diffy > 0 {
612 "high"
613 } else {
614 "very_high"
615 }
616 }
617}
618
619#[cfg(test)]
620mod tests {
621 use super::*;
622
623 #[test]
624 fn test_classify_functional_conflict() {
625 let base = "function foo() {\n return 1;\n}\n";
626 let ours = "function foo() {\n return 2;\n}\n";
627 let theirs = "function foo() {\n return 3;\n}\n";
628 assert_eq!(
629 classify_conflict(Some(base), Some(ours), Some(theirs)),
630 ConflictComplexity::Functional
631 );
632 }
633
634 #[test]
635 fn test_classify_syntax_conflict() {
636 let base = "function foo(a: number) {\n return a;\n}\n";
638 let ours = "function foo(a: string) {\n return a;\n}\n";
639 let theirs = "function foo(a: boolean) {\n return a;\n}\n";
640 assert_eq!(
641 classify_conflict(Some(base), Some(ours), Some(theirs)),
642 ConflictComplexity::Syntax
643 );
644 }
645
646 #[test]
647 fn test_classify_text_conflict() {
648 let base = "// old comment\n return 1;\n";
650 let ours = "// ours comment\n return 1;\n";
651 let theirs = "// theirs comment\n return 1;\n";
652 assert_eq!(
653 classify_conflict(Some(base), Some(ours), Some(theirs)),
654 ConflictComplexity::Text
655 );
656 }
657
658 #[test]
659 fn test_classify_syntax_functional_conflict() {
660 let base = "function foo(a: number) {\n return a;\n}\n";
662 let ours = "function foo(a: string) {\n return a + 1;\n}\n";
663 let theirs = "function foo(a: boolean) {\n return a + 2;\n}\n";
664 assert_eq!(
665 classify_conflict(Some(base), Some(ours), Some(theirs)),
666 ConflictComplexity::SyntaxFunctional
667 );
668 }
669
670 #[test]
671 fn test_classify_unknown_when_identical() {
672 let content = "function foo() {\n return 1;\n}\n";
673 assert_eq!(
674 classify_conflict(Some(content), Some(content), Some(content)),
675 ConflictComplexity::Unknown
676 );
677 }
678
679 #[test]
680 fn test_classify_modify_delete() {
681 let base = "function foo() {\n return 1;\n}\n";
684 let ours = "function foo() {\n return 2;\n}\n";
685 assert_eq!(
686 classify_conflict(Some(base), Some(ours), None),
687 ConflictComplexity::SyntaxFunctional
688 );
689 }
690
691 #[test]
692 fn test_classify_both_added() {
693 let ours = "function foo() {\n return 1;\n}\n";
696 let theirs = "function foo() {\n return 2;\n}\n";
697 assert_eq!(
698 classify_conflict(None, Some(ours), Some(theirs)),
699 ConflictComplexity::SyntaxFunctional
700 );
701 }
702
703 #[test]
704 fn test_conflict_markers_include_complexity_and_hint() {
705 let conflict = EntityConflict {
706 entity_name: "foo".to_string(),
707 entity_type: "function".to_string(),
708 kind: ConflictKind::BothModified,
709 complexity: ConflictComplexity::Functional,
710 ours_content: Some("return 1;".to_string()),
711 theirs_content: Some("return 2;".to_string()),
712 base_content: Some("return 0;".to_string()),
713 };
714 let markers = conflict.to_conflict_markers(&MarkerFormat::default());
715 assert!(markers.contains("confidence: medium"), "Markers should contain confidence: {}", markers);
716 assert!(markers.contains("// hint: Logic changed on both sides"), "Markers should contain hint: {}", markers);
717 }
718
719 #[test]
720 fn test_resolution_hints() {
721 assert!(ConflictComplexity::Text.resolution_hint().contains("Cosmetic"));
722 assert!(ConflictComplexity::Syntax.resolution_hint().contains("Structural"));
723 assert!(ConflictComplexity::Functional.resolution_hint().contains("Logic"));
724 assert!(ConflictComplexity::TextSyntax.resolution_hint().contains("Renamed"));
725 assert!(ConflictComplexity::TextFunctional.resolution_hint().contains("Logic and cosmetic"));
726 assert!(ConflictComplexity::SyntaxFunctional.resolution_hint().contains("Structural and logic"));
727 assert!(ConflictComplexity::TextSyntaxFunctional.resolution_hint().contains("All three"));
728 assert!(ConflictComplexity::Unknown.resolution_hint().contains("Could not classify"));
729 }
730
731 #[test]
732 fn test_parse_weave_conflicts() {
733 let conflict = EntityConflict {
734 entity_name: "process".to_string(),
735 entity_type: "function".to_string(),
736 kind: ConflictKind::BothModified,
737 complexity: ConflictComplexity::Functional,
738 ours_content: Some("fn process() { return 1; }".to_string()),
739 theirs_content: Some("fn process() { return 2; }".to_string()),
740 base_content: Some("fn process() { return 0; }".to_string()),
741 };
742 let markers = conflict.to_conflict_markers(&MarkerFormat::default());
743
744 let parsed = parse_weave_conflicts(&markers);
745 assert_eq!(parsed.len(), 1);
746 assert_eq!(parsed[0].entity_name, "process");
747 assert_eq!(parsed[0].entity_kind, "function");
748 assert_eq!(parsed[0].complexity, ConflictComplexity::Functional);
749 assert_eq!(parsed[0].confidence, "medium");
750 assert!(parsed[0].hint.contains("Logic changed"));
751 assert!(parsed[0].ours_content.contains("return 1"));
752 assert!(parsed[0].theirs_content.contains("return 2"));
753 }
754
755 #[test]
756 fn test_parse_weave_conflicts_multiple() {
757 let c1 = EntityConflict {
758 entity_name: "foo".to_string(),
759 entity_type: "function".to_string(),
760 kind: ConflictKind::BothModified,
761 complexity: ConflictComplexity::Text,
762 ours_content: Some("// a".to_string()),
763 theirs_content: Some("// b".to_string()),
764 base_content: None,
765 };
766 let c2 = EntityConflict {
767 entity_name: "Bar".to_string(),
768 entity_type: "class".to_string(),
769 kind: ConflictKind::BothModified,
770 complexity: ConflictComplexity::SyntaxFunctional,
771 ours_content: Some("class Bar { x() {} }".to_string()),
772 theirs_content: Some("class Bar { y() {} }".to_string()),
773 base_content: None,
774 };
775 let content = format!("some code\n{}\nmore code\n{}\nend", c1.to_conflict_markers(&MarkerFormat::default()), c2.to_conflict_markers(&MarkerFormat::default()));
776 let parsed = parse_weave_conflicts(&content);
777 assert_eq!(parsed.len(), 2);
778 assert_eq!(parsed[0].entity_name, "foo");
779 assert_eq!(parsed[0].complexity, ConflictComplexity::Text);
780 assert_eq!(parsed[1].entity_name, "Bar");
781 assert_eq!(parsed[1].complexity, ConflictComplexity::SyntaxFunctional);
782 }
783
784 #[test]
785 fn test_standard_markers_no_metadata() {
786 let conflict = EntityConflict {
787 entity_name: "foo".to_string(),
788 entity_type: "function".to_string(),
789 kind: ConflictKind::BothModified,
790 complexity: ConflictComplexity::Functional,
791 ours_content: Some("return 1;".to_string()),
792 theirs_content: Some("return 2;".to_string()),
793 base_content: Some("return 0;".to_string()),
794 };
795 let markers = conflict.to_conflict_markers(&MarkerFormat::standard(7));
796 assert_eq!(markers, "<<<<<<< ours\nreturn 1;\n||||||| base\nreturn 0;\n=======\nreturn 2;\n>>>>>>> theirs\n");
797 assert!(!markers.contains('\u{2014}'));
799 assert!(!markers.contains("hint"));
800 assert!(!markers.contains("confidence"));
801 }
802
803 #[test]
804 fn test_standard_markers_custom_length() {
805 let conflict = EntityConflict {
806 entity_name: "foo".to_string(),
807 entity_type: "function".to_string(),
808 kind: ConflictKind::BothModified,
809 complexity: ConflictComplexity::Functional,
810 ours_content: Some("a".to_string()),
811 theirs_content: Some("b".to_string()),
812 base_content: None,
813 };
814 let markers = conflict.to_conflict_markers(&MarkerFormat::standard(11));
815 assert!(markers.starts_with("<<<<<<<<<<<")); assert!(markers.contains("===========")); assert!(markers.contains(">>>>>>>>>>>")); }
819}
820
821impl fmt::Display for MergeStats {
822 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
823 write!(f, "unchanged: {}", self.entities_unchanged)?;
824 if self.entities_ours_only > 0 {
825 write!(f, ", ours-only: {}", self.entities_ours_only)?;
826 }
827 if self.entities_theirs_only > 0 {
828 write!(f, ", theirs-only: {}", self.entities_theirs_only)?;
829 }
830 if self.entities_both_changed_merged > 0 {
831 write!(f, ", auto-merged: {}", self.entities_both_changed_merged)?;
832 }
833 if self.entities_added_ours > 0 {
834 write!(f, ", added-ours: {}", self.entities_added_ours)?;
835 }
836 if self.entities_added_theirs > 0 {
837 write!(f, ", added-theirs: {}", self.entities_added_theirs)?;
838 }
839 if self.entities_deleted > 0 {
840 write!(f, ", deleted: {}", self.entities_deleted)?;
841 }
842 if self.entities_conflicted > 0 {
843 write!(f, ", CONFLICTS: {}", self.entities_conflicted)?;
844 }
845 if self.semantic_warnings > 0 {
846 write!(f, ", semantic-warnings: {}", self.semantic_warnings)?;
847 }
848 if self.used_fallback {
849 write!(f, " (line-level fallback)")?;
850 }
851 Ok(())
852 }
853}