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 out.push_str(&format!("{}\n", sep));
379
380 if has_narrowing {
382 for line in theirs_mid {
383 out.push_str(line);
384 out.push('\n');
385 }
386 } else {
387 out.push_str(theirs);
388 if !theirs.is_empty() && !theirs.ends_with('\n') {
389 out.push('\n');
390 }
391 }
392
393 if fmt.enhanced {
395 let confidence = match &self.complexity {
396 ConflictComplexity::Text => "high",
397 ConflictComplexity::Syntax => "medium",
398 ConflictComplexity::Functional => "medium",
399 ConflictComplexity::TextSyntax => "medium",
400 ConflictComplexity::TextFunctional => "medium",
401 ConflictComplexity::SyntaxFunctional => "low",
402 ConflictComplexity::TextSyntaxFunctional => "low",
403 ConflictComplexity::Unknown => "unknown",
404 };
405 let label = format!(
406 "{} `{}` ({}, confidence: {})",
407 self.entity_type, self.entity_name, self.complexity, confidence
408 );
409 out.push_str(&format!("{} theirs \u{2014} {}\n", close, label));
410 } else {
411 out.push_str(&format!("{} theirs\n", close));
412 }
413
414 if has_narrowing {
416 for line in &ours_lines[ours_lines.len() - suffix_len..] {
417 out.push_str(line);
418 out.push('\n');
419 }
420 }
421
422 out
423 }
424}
425
426#[derive(Debug, Clone)]
428pub struct ParsedConflict {
429 pub entity_name: String,
430 pub entity_kind: String,
431 pub complexity: ConflictComplexity,
432 pub confidence: String,
433 pub hint: String,
434 pub ours_content: String,
435 pub theirs_content: String,
436}
437
438pub fn parse_weave_conflicts(content: &str) -> Vec<ParsedConflict> {
443 let mut conflicts = Vec::new();
444 let lines: Vec<&str> = content.lines().collect();
445 let mut i = 0;
446
447 while i < lines.len() {
448 if lines[i].starts_with("<<<<<<< ours") {
450 let header = lines[i];
451 let (entity_kind, entity_name, complexity, confidence) = parse_conflict_header(header);
452
453 i += 1;
454
455 let mut hint = String::new();
457 if i < lines.len() && lines[i].starts_with("// hint: ") {
458 hint = lines[i].trim_start_matches("// hint: ").to_string();
459 i += 1;
460 }
461
462 let mut ours_lines = Vec::new();
464 while i < lines.len() && lines[i] != "=======" {
465 ours_lines.push(lines[i]);
466 i += 1;
467 }
468 i += 1; let mut theirs_lines = Vec::new();
472 while i < lines.len() && !lines[i].starts_with(">>>>>>> theirs") {
473 theirs_lines.push(lines[i]);
474 i += 1;
475 }
476 i += 1; let ours_content = if ours_lines.is_empty() {
479 String::new()
480 } else {
481 ours_lines.join("\n") + "\n"
482 };
483 let theirs_content = if theirs_lines.is_empty() {
484 String::new()
485 } else {
486 theirs_lines.join("\n") + "\n"
487 };
488
489 conflicts.push(ParsedConflict {
490 entity_name,
491 entity_kind,
492 complexity,
493 confidence,
494 hint,
495 ours_content,
496 theirs_content,
497 });
498 } else {
499 i += 1;
500 }
501 }
502
503 conflicts
504}
505
506fn parse_conflict_header(header: &str) -> (String, String, ConflictComplexity, String) {
507 let after_dash = header
509 .split('\u{2014}')
510 .nth(1)
511 .unwrap_or(header)
512 .trim();
513
514 let entity_kind = after_dash
516 .split('`')
517 .next()
518 .unwrap_or("")
519 .trim()
520 .to_string();
521
522 let entity_name = after_dash
524 .split('`')
525 .nth(1)
526 .unwrap_or("")
527 .to_string();
528
529 let paren_content = after_dash
531 .rsplit('(')
532 .next()
533 .unwrap_or("")
534 .trim_end_matches(')');
535
536 let parts: Vec<&str> = paren_content.split(',').map(|s| s.trim()).collect();
537 let complexity = match parts.first().copied().unwrap_or("") {
538 "T" => ConflictComplexity::Text,
539 "S" => ConflictComplexity::Syntax,
540 "F" => ConflictComplexity::Functional,
541 "T+S" => ConflictComplexity::TextSyntax,
542 "T+F" => ConflictComplexity::TextFunctional,
543 "S+F" => ConflictComplexity::SyntaxFunctional,
544 "T+S+F" => ConflictComplexity::TextSyntaxFunctional,
545 _ => ConflictComplexity::Unknown,
546 };
547
548 let confidence = parts
549 .iter()
550 .find(|p| p.starts_with("confidence:"))
551 .map(|p| p.trim_start_matches("confidence:").trim().to_string())
552 .unwrap_or_else(|| "unknown".to_string());
553
554 (entity_kind, entity_name, complexity, confidence)
555}
556
557#[derive(Debug, Clone, Default, Serialize)]
559pub struct MergeStats {
560 pub entities_unchanged: usize,
561 pub entities_ours_only: usize,
562 pub entities_theirs_only: usize,
563 pub entities_both_changed_merged: usize,
564 pub entities_conflicted: usize,
565 pub entities_added_ours: usize,
566 pub entities_added_theirs: usize,
567 pub entities_deleted: usize,
568 pub used_fallback: bool,
569 pub semantic_warnings: usize,
571 pub resolved_via_diffy: usize,
573 pub resolved_via_inner_merge: usize,
575}
576
577impl MergeStats {
578 pub fn has_conflicts(&self) -> bool {
579 self.entities_conflicted > 0
580 }
581
582 pub fn confidence(&self) -> &'static str {
585 if self.entities_conflicted > 0 {
586 "conflict"
587 } else if self.resolved_via_inner_merge > 0 || self.used_fallback {
588 "medium"
589 } else if self.resolved_via_diffy > 0 {
590 "high"
591 } else {
592 "very_high"
593 }
594 }
595}
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600
601 #[test]
602 fn test_classify_functional_conflict() {
603 let base = "function foo() {\n return 1;\n}\n";
604 let ours = "function foo() {\n return 2;\n}\n";
605 let theirs = "function foo() {\n return 3;\n}\n";
606 assert_eq!(
607 classify_conflict(Some(base), Some(ours), Some(theirs)),
608 ConflictComplexity::Functional
609 );
610 }
611
612 #[test]
613 fn test_classify_syntax_conflict() {
614 let base = "function foo(a: number) {\n return a;\n}\n";
616 let ours = "function foo(a: string) {\n return a;\n}\n";
617 let theirs = "function foo(a: boolean) {\n return a;\n}\n";
618 assert_eq!(
619 classify_conflict(Some(base), Some(ours), Some(theirs)),
620 ConflictComplexity::Syntax
621 );
622 }
623
624 #[test]
625 fn test_classify_text_conflict() {
626 let base = "// old comment\n return 1;\n";
628 let ours = "// ours comment\n return 1;\n";
629 let theirs = "// theirs comment\n return 1;\n";
630 assert_eq!(
631 classify_conflict(Some(base), Some(ours), Some(theirs)),
632 ConflictComplexity::Text
633 );
634 }
635
636 #[test]
637 fn test_classify_syntax_functional_conflict() {
638 let base = "function foo(a: number) {\n return a;\n}\n";
640 let ours = "function foo(a: string) {\n return a + 1;\n}\n";
641 let theirs = "function foo(a: boolean) {\n return a + 2;\n}\n";
642 assert_eq!(
643 classify_conflict(Some(base), Some(ours), Some(theirs)),
644 ConflictComplexity::SyntaxFunctional
645 );
646 }
647
648 #[test]
649 fn test_classify_unknown_when_identical() {
650 let content = "function foo() {\n return 1;\n}\n";
651 assert_eq!(
652 classify_conflict(Some(content), Some(content), Some(content)),
653 ConflictComplexity::Unknown
654 );
655 }
656
657 #[test]
658 fn test_classify_modify_delete() {
659 let base = "function foo() {\n return 1;\n}\n";
662 let ours = "function foo() {\n return 2;\n}\n";
663 assert_eq!(
664 classify_conflict(Some(base), Some(ours), None),
665 ConflictComplexity::SyntaxFunctional
666 );
667 }
668
669 #[test]
670 fn test_classify_both_added() {
671 let ours = "function foo() {\n return 1;\n}\n";
674 let theirs = "function foo() {\n return 2;\n}\n";
675 assert_eq!(
676 classify_conflict(None, Some(ours), Some(theirs)),
677 ConflictComplexity::SyntaxFunctional
678 );
679 }
680
681 #[test]
682 fn test_conflict_markers_include_complexity_and_hint() {
683 let conflict = EntityConflict {
684 entity_name: "foo".to_string(),
685 entity_type: "function".to_string(),
686 kind: ConflictKind::BothModified,
687 complexity: ConflictComplexity::Functional,
688 ours_content: Some("return 1;".to_string()),
689 theirs_content: Some("return 2;".to_string()),
690 base_content: Some("return 0;".to_string()),
691 };
692 let markers = conflict.to_conflict_markers(&MarkerFormat::default());
693 assert!(markers.contains("confidence: medium"), "Markers should contain confidence: {}", markers);
694 assert!(markers.contains("// hint: Logic changed on both sides"), "Markers should contain hint: {}", markers);
695 }
696
697 #[test]
698 fn test_resolution_hints() {
699 assert!(ConflictComplexity::Text.resolution_hint().contains("Cosmetic"));
700 assert!(ConflictComplexity::Syntax.resolution_hint().contains("Structural"));
701 assert!(ConflictComplexity::Functional.resolution_hint().contains("Logic"));
702 assert!(ConflictComplexity::TextSyntax.resolution_hint().contains("Renamed"));
703 assert!(ConflictComplexity::TextFunctional.resolution_hint().contains("Logic and cosmetic"));
704 assert!(ConflictComplexity::SyntaxFunctional.resolution_hint().contains("Structural and logic"));
705 assert!(ConflictComplexity::TextSyntaxFunctional.resolution_hint().contains("All three"));
706 assert!(ConflictComplexity::Unknown.resolution_hint().contains("Could not classify"));
707 }
708
709 #[test]
710 fn test_parse_weave_conflicts() {
711 let conflict = EntityConflict {
712 entity_name: "process".to_string(),
713 entity_type: "function".to_string(),
714 kind: ConflictKind::BothModified,
715 complexity: ConflictComplexity::Functional,
716 ours_content: Some("fn process() { return 1; }".to_string()),
717 theirs_content: Some("fn process() { return 2; }".to_string()),
718 base_content: Some("fn process() { return 0; }".to_string()),
719 };
720 let markers = conflict.to_conflict_markers(&MarkerFormat::default());
721
722 let parsed = parse_weave_conflicts(&markers);
723 assert_eq!(parsed.len(), 1);
724 assert_eq!(parsed[0].entity_name, "process");
725 assert_eq!(parsed[0].entity_kind, "function");
726 assert_eq!(parsed[0].complexity, ConflictComplexity::Functional);
727 assert_eq!(parsed[0].confidence, "medium");
728 assert!(parsed[0].hint.contains("Logic changed"));
729 assert!(parsed[0].ours_content.contains("return 1"));
730 assert!(parsed[0].theirs_content.contains("return 2"));
731 }
732
733 #[test]
734 fn test_parse_weave_conflicts_multiple() {
735 let c1 = EntityConflict {
736 entity_name: "foo".to_string(),
737 entity_type: "function".to_string(),
738 kind: ConflictKind::BothModified,
739 complexity: ConflictComplexity::Text,
740 ours_content: Some("// a".to_string()),
741 theirs_content: Some("// b".to_string()),
742 base_content: None,
743 };
744 let c2 = EntityConflict {
745 entity_name: "Bar".to_string(),
746 entity_type: "class".to_string(),
747 kind: ConflictKind::BothModified,
748 complexity: ConflictComplexity::SyntaxFunctional,
749 ours_content: Some("class Bar { x() {} }".to_string()),
750 theirs_content: Some("class Bar { y() {} }".to_string()),
751 base_content: None,
752 };
753 let content = format!("some code\n{}\nmore code\n{}\nend", c1.to_conflict_markers(&MarkerFormat::default()), c2.to_conflict_markers(&MarkerFormat::default()));
754 let parsed = parse_weave_conflicts(&content);
755 assert_eq!(parsed.len(), 2);
756 assert_eq!(parsed[0].entity_name, "foo");
757 assert_eq!(parsed[0].complexity, ConflictComplexity::Text);
758 assert_eq!(parsed[1].entity_name, "Bar");
759 assert_eq!(parsed[1].complexity, ConflictComplexity::SyntaxFunctional);
760 }
761
762 #[test]
763 fn test_standard_markers_no_metadata() {
764 let conflict = EntityConflict {
765 entity_name: "foo".to_string(),
766 entity_type: "function".to_string(),
767 kind: ConflictKind::BothModified,
768 complexity: ConflictComplexity::Functional,
769 ours_content: Some("return 1;".to_string()),
770 theirs_content: Some("return 2;".to_string()),
771 base_content: Some("return 0;".to_string()),
772 };
773 let markers = conflict.to_conflict_markers(&MarkerFormat::standard(7));
774 assert_eq!(markers, "<<<<<<< ours\nreturn 1;\n=======\nreturn 2;\n>>>>>>> theirs\n");
775 assert!(!markers.contains('\u{2014}'));
777 assert!(!markers.contains("hint"));
778 assert!(!markers.contains("confidence"));
779 }
780
781 #[test]
782 fn test_standard_markers_custom_length() {
783 let conflict = EntityConflict {
784 entity_name: "foo".to_string(),
785 entity_type: "function".to_string(),
786 kind: ConflictKind::BothModified,
787 complexity: ConflictComplexity::Functional,
788 ours_content: Some("a".to_string()),
789 theirs_content: Some("b".to_string()),
790 base_content: None,
791 };
792 let markers = conflict.to_conflict_markers(&MarkerFormat::standard(11));
793 assert!(markers.starts_with("<<<<<<<<<<<")); assert!(markers.contains("===========")); assert!(markers.contains(">>>>>>>>>>>")); }
797}
798
799impl fmt::Display for MergeStats {
800 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
801 write!(f, "unchanged: {}", self.entities_unchanged)?;
802 if self.entities_ours_only > 0 {
803 write!(f, ", ours-only: {}", self.entities_ours_only)?;
804 }
805 if self.entities_theirs_only > 0 {
806 write!(f, ", theirs-only: {}", self.entities_theirs_only)?;
807 }
808 if self.entities_both_changed_merged > 0 {
809 write!(f, ", auto-merged: {}", self.entities_both_changed_merged)?;
810 }
811 if self.entities_added_ours > 0 {
812 write!(f, ", added-ours: {}", self.entities_added_ours)?;
813 }
814 if self.entities_added_theirs > 0 {
815 write!(f, ", added-theirs: {}", self.entities_added_theirs)?;
816 }
817 if self.entities_deleted > 0 {
818 write!(f, ", deleted: {}", self.entities_deleted)?;
819 }
820 if self.entities_conflicted > 0 {
821 write!(f, ", CONFLICTS: {}", self.entities_conflicted)?;
822 }
823 if self.semantic_warnings > 0 {
824 write!(f, ", semantic-warnings: {}", self.semantic_warnings)?;
825 }
826 if self.used_fallback {
827 write!(f, " (line-level fallback)")?;
828 }
829 Ok(())
830 }
831}