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