1use std::ops::Range;
12use std::time::Duration;
13
14use similar::{Algorithm, DiffTag, TextDiff};
15
16const DIFF_TIMEOUT: Duration = Duration::from_millis(500);
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ChunkKind {
23 Stable,
25 Ours,
27 Theirs,
29 Agree,
31 Conflict,
33}
34
35#[derive(Debug, Clone)]
37pub struct MergeChunk {
38 pub id: usize,
40 pub kind: ChunkKind,
42 pub base: Vec<String>,
44 pub ours: Vec<String>,
47 pub theirs: Vec<String>,
50 pub base_start: usize,
52 pub ours_start: usize,
54 pub theirs_start: usize,
56}
57
58impl MergeChunk {
59 pub fn ours_lines(&self) -> &[String] {
62 if self.kind == ChunkKind::Stable {
63 &self.base
64 } else {
65 &self.ours
66 }
67 }
68
69 pub fn theirs_lines(&self) -> &[String] {
71 if self.kind == ChunkKind::Stable {
72 &self.base
73 } else {
74 &self.theirs
75 }
76 }
77}
78
79#[derive(Debug, Clone)]
81pub struct MergeResult {
82 pub chunks: Vec<MergeChunk>,
84 pub conflicts: usize,
86 pub ours_changes: usize,
88 pub theirs_changes: usize,
90 pub agree: usize,
92 pub ours_label: Option<String>,
94 pub theirs_label: Option<String>,
96}
97
98#[derive(Debug, Clone)]
100struct Hunk {
101 base_range: Range<usize>,
103 lines: Vec<String>,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109enum Side {
110 Ours,
111 Theirs,
112}
113
114#[derive(Debug, Clone)]
116struct SidedHunk {
117 side: Side,
118 hunk: Hunk,
119}
120
121pub fn merge_text(base: &str, ours: &str, theirs: &str) -> MergeResult {
123 let ours_diff = TextDiff::configure()
124 .algorithm(Algorithm::Myers)
125 .timeout(DIFF_TIMEOUT)
126 .diff_lines(base, ours);
127 let theirs_diff = TextDiff::configure()
128 .algorithm(Algorithm::Myers)
129 .timeout(DIFF_TIMEOUT)
130 .diff_lines(base, theirs);
131
132 let base_lines: Vec<String> = (0..ours_diff.old_len())
134 .filter_map(|i| ours_diff.old_slice(i))
135 .map(clean_line)
136 .collect();
137
138 let groups = group_hunks(extract_hunks(&ours_diff), extract_hunks(&theirs_diff));
139 build_chunks(&base_lines, groups)
140}
141
142fn clean_line(line: &str) -> String {
144 line.trim_end_matches(['\r', '\n']).to_owned()
145}
146
147fn extract_hunks(diff: &TextDiff<'_, '_, str>) -> Vec<Hunk> {
149 diff.ops()
150 .iter()
151 .filter(|op| op.tag() != DiffTag::Equal)
152 .map(|op| Hunk {
153 base_range: op.old_range(),
154 lines: op
156 .new_range()
157 .filter_map(|i| diff.new_slice(i))
158 .map(clean_line)
159 .collect(),
160 })
161 .collect()
162}
163
164fn collides(a: &Range<usize>, b: &Range<usize>) -> bool {
170 match (a.is_empty(), b.is_empty()) {
171 (true, true) => a.start == b.start,
172 (true, false) => b.start <= a.start && a.start <= b.end,
173 (false, true) => a.start <= b.start && b.start <= a.end,
174 (false, false) => a.start.max(b.start) < a.end.min(b.end),
175 }
176}
177
178fn group_hunks(ours: Vec<Hunk>, theirs: Vec<Hunk>) -> Vec<Vec<SidedHunk>> {
180 let mut all: Vec<SidedHunk> = ours
181 .into_iter()
182 .map(|hunk| SidedHunk {
183 side: Side::Ours,
184 hunk,
185 })
186 .chain(theirs.into_iter().map(|hunk| SidedHunk {
187 side: Side::Theirs,
188 hunk,
189 }))
190 .collect();
191 all.sort_by_key(|s| {
193 (
194 s.hunk.base_range.start,
195 s.hunk.base_range.end,
196 s.side == Side::Theirs,
197 )
198 });
199
200 let mut groups: Vec<(Range<usize>, Vec<SidedHunk>)> = Vec::new();
202 for sided in all {
203 let range = sided.hunk.base_range.clone();
204 match groups.last_mut() {
205 Some((merged, members)) if collides(merged, &range) => {
206 merged.start = merged.start.min(range.start);
207 merged.end = merged.end.max(range.end);
208 members.push(sided);
209 }
210 _ => groups.push((range, vec![sided])),
211 }
212 }
213 groups.into_iter().map(|(_, members)| members).collect()
214}
215
216fn side_content(
219 base_lines: &[String],
220 lo: usize,
221 hi: usize,
222 group: &[SidedHunk],
223 side: Side,
224) -> Vec<String> {
225 let mut out = Vec::new();
226 let mut pos = lo;
227 for sided in group.iter().filter(|s| s.side == side) {
228 out.extend_from_slice(&base_lines[pos..sided.hunk.base_range.start]);
229 out.extend(sided.hunk.lines.iter().cloned());
230 pos = sided.hunk.base_range.end;
231 }
232 out.extend_from_slice(&base_lines[pos..hi]);
233 out
234}
235
236fn build_chunks(base_lines: &[String], groups: Vec<Vec<SidedHunk>>) -> MergeResult {
238 let mut chunks: Vec<MergeChunk> = Vec::new();
239 let mut conflicts = 0usize;
240 let mut ours_changes = 0usize;
241 let mut theirs_changes = 0usize;
242 let mut agree = 0usize;
243
244 let mut base_pos = 0usize;
246 let mut base_no = 1usize;
247 let mut ours_no = 1usize;
248 let mut theirs_no = 1usize;
249
250 let push_stable = |chunks: &mut Vec<MergeChunk>,
252 lines: &[String],
253 base_no: &mut usize,
254 ours_no: &mut usize,
255 theirs_no: &mut usize| {
256 chunks.push(MergeChunk {
257 id: chunks.len(),
258 kind: ChunkKind::Stable,
259 base: lines.to_vec(),
260 ours: Vec::new(),
261 theirs: Vec::new(),
262 base_start: *base_no,
263 ours_start: *ours_no,
264 theirs_start: *theirs_no,
265 });
266 *base_no += lines.len();
267 *ours_no += lines.len();
268 *theirs_no += lines.len();
269 };
270
271 for group in groups {
272 let lo = group
274 .iter()
275 .map(|s| s.hunk.base_range.start)
276 .min()
277 .unwrap_or(0);
278 let hi = group
279 .iter()
280 .map(|s| s.hunk.base_range.end)
281 .max()
282 .unwrap_or(lo);
283
284 if lo > base_pos {
286 push_stable(
287 &mut chunks,
288 &base_lines[base_pos..lo],
289 &mut base_no,
290 &mut ours_no,
291 &mut theirs_no,
292 );
293 }
294
295 let ours_lines = side_content(base_lines, lo, hi, &group, Side::Ours);
296 let theirs_lines = side_content(base_lines, lo, hi, &group, Side::Theirs);
297 let has_ours = group.iter().any(|s| s.side == Side::Ours);
298 let has_theirs = group.iter().any(|s| s.side == Side::Theirs);
299 let kind = match (has_ours, has_theirs) {
300 (true, false) => ChunkKind::Ours,
301 (false, true) => ChunkKind::Theirs,
302 _ if ours_lines == theirs_lines => ChunkKind::Agree,
303 _ => ChunkKind::Conflict,
304 };
305 match kind {
306 ChunkKind::Ours => ours_changes += 1,
307 ChunkKind::Theirs => theirs_changes += 1,
308 ChunkKind::Agree => agree += 1,
309 ChunkKind::Conflict => conflicts += 1,
310 ChunkKind::Stable => {}
311 }
312
313 let (ours_len, theirs_len) = (ours_lines.len(), theirs_lines.len());
314 chunks.push(MergeChunk {
315 id: chunks.len(),
316 kind,
317 base: base_lines[lo..hi].to_vec(),
318 ours: ours_lines,
319 theirs: theirs_lines,
320 base_start: base_no,
321 ours_start: ours_no,
322 theirs_start: theirs_no,
323 });
324 base_no += hi - lo;
325 ours_no += ours_len;
326 theirs_no += theirs_len;
327 base_pos = hi;
328 }
329
330 if base_pos < base_lines.len() {
332 push_stable(
333 &mut chunks,
334 &base_lines[base_pos..],
335 &mut base_no,
336 &mut ours_no,
337 &mut theirs_no,
338 );
339 }
340
341 MergeResult {
342 chunks,
343 conflicts,
344 ours_changes,
345 theirs_changes,
346 agree,
347 ours_label: None,
348 theirs_label: None,
349 }
350}
351
352#[derive(Debug, thiserror::Error)]
354pub enum ConflictParseError {
355 #[error("未检测到 git 冲突标记(<<<<<<< / ======= / >>>>>>>)")]
357 NoMarkers,
358 #[error("第 {0} 行附近的冲突标记不完整或顺序错误")]
360 Malformed(usize),
361}
362
363enum Section {
365 Common,
367 Ours,
369 Base,
371 Theirs,
373}
374
375pub fn parse_conflict_file(text: &str) -> Result<MergeResult, ConflictParseError> {
381 let mut chunks: Vec<MergeChunk> = Vec::new();
382 let mut conflicts = 0usize;
383 let mut ours_label: Option<String> = None;
384 let mut theirs_label: Option<String> = None;
385
386 let mut base_no = 1usize;
388 let mut ours_no = 1usize;
389 let mut theirs_no = 1usize;
390
391 let mut common: Vec<String> = Vec::new();
393 let mut ours: Vec<String> = Vec::new();
394 let mut base: Vec<String> = Vec::new();
395 let mut theirs: Vec<String> = Vec::new();
396 let mut section = Section::Common;
397 let mut conflict_start = 0usize;
398
399 let flush_common = |chunks: &mut Vec<MergeChunk>,
401 common: &mut Vec<String>,
402 base_no: &mut usize,
403 ours_no: &mut usize,
404 theirs_no: &mut usize| {
405 if common.is_empty() {
406 return;
407 }
408 let lines = std::mem::take(common);
409 let len = lines.len();
410 chunks.push(MergeChunk {
411 id: chunks.len(),
412 kind: ChunkKind::Stable,
413 base: lines,
414 ours: Vec::new(),
415 theirs: Vec::new(),
416 base_start: *base_no,
417 ours_start: *ours_no,
418 theirs_start: *theirs_no,
419 });
420 *base_no += len;
421 *ours_no += len;
422 *theirs_no += len;
423 };
424
425 for (idx, raw) in text.lines().enumerate() {
426 let line = raw.trim_end_matches('\r');
427 let line_no = idx + 1;
428
429 if let Some(rest) = line.strip_prefix("<<<<<<<") {
430 if !matches!(section, Section::Common) {
432 return Err(ConflictParseError::Malformed(line_no));
433 }
434 flush_common(
435 &mut chunks,
436 &mut common,
437 &mut base_no,
438 &mut ours_no,
439 &mut theirs_no,
440 );
441 conflict_start = line_no;
442 let label = rest.trim();
443 if ours_label.is_none() && !label.is_empty() {
444 ours_label = Some(label.to_owned());
445 }
446 section = Section::Ours;
447 } else if matches!(section, Section::Ours) && line.starts_with("|||||||") {
448 section = Section::Base;
449 } else if matches!(section, Section::Ours | Section::Base)
450 && line.len() >= 7
451 && line.bytes().all(|b| b == b'=')
452 {
453 section = Section::Theirs;
454 } else if let Some(rest) = line.strip_prefix(">>>>>>>") {
455 if !matches!(section, Section::Theirs) {
456 return Err(ConflictParseError::Malformed(line_no));
457 }
458 let label = rest.trim();
459 if theirs_label.is_none() && !label.is_empty() {
460 theirs_label = Some(label.to_owned());
461 }
462 let (base_lines, ours_lines, theirs_lines) = (
463 std::mem::take(&mut base),
464 std::mem::take(&mut ours),
465 std::mem::take(&mut theirs),
466 );
467 let lens = (base_lines.len(), ours_lines.len(), theirs_lines.len());
468 chunks.push(MergeChunk {
469 id: chunks.len(),
470 kind: ChunkKind::Conflict,
471 base_start: base_no,
472 ours_start: ours_no,
473 theirs_start: theirs_no,
474 base: base_lines,
475 ours: ours_lines,
476 theirs: theirs_lines,
477 });
478 base_no += lens.0;
479 ours_no += lens.1;
480 theirs_no += lens.2;
481 conflicts += 1;
482 section = Section::Common;
483 } else {
484 let owned = line.to_owned();
485 match section {
486 Section::Common => common.push(owned),
487 Section::Ours => ours.push(owned),
488 Section::Base => base.push(owned),
489 Section::Theirs => theirs.push(owned),
490 }
491 }
492 }
493
494 if !matches!(section, Section::Common) {
496 return Err(ConflictParseError::Malformed(conflict_start));
497 }
498 if conflicts == 0 {
499 return Err(ConflictParseError::NoMarkers);
500 }
501 flush_common(
502 &mut chunks,
503 &mut common,
504 &mut base_no,
505 &mut ours_no,
506 &mut theirs_no,
507 );
508
509 Ok(MergeResult {
510 chunks,
511 conflicts,
512 ours_changes: 0,
513 theirs_changes: 0,
514 agree: 0,
515 ours_label,
516 theirs_label,
517 })
518}
519
520#[cfg(test)]
521mod tests {
522 use super::*;
523
524 fn kinds(result: &MergeResult) -> Vec<ChunkKind> {
526 result.chunks.iter().map(|c| c.kind).collect()
527 }
528
529 #[test]
530 fn identical_inputs_are_stable() {
531 let result = merge_text("a\nb\n", "a\nb\n", "a\nb\n");
532 assert_eq!(kinds(&result), vec![ChunkKind::Stable]);
533 assert_eq!(result.conflicts, 0);
534 }
535
536 #[test]
537 fn non_overlapping_changes_merge() {
538 let result = merge_text("a\nb\nc\nd\n", "A\nb\nc\nd\n", "a\nb\nc\nD\n");
539 assert_eq!(
540 kinds(&result),
541 vec![ChunkKind::Ours, ChunkKind::Stable, ChunkKind::Theirs]
542 );
543 assert_eq!(result.chunks[0].ours, vec!["A"]);
544 assert_eq!(result.chunks[0].base, vec!["a"]);
545 assert_eq!(result.chunks[2].theirs, vec!["D"]);
546 assert_eq!(result.conflicts, 0);
547 assert_eq!(result.ours_changes, 1);
548 assert_eq!(result.theirs_changes, 1);
549 }
550
551 #[test]
552 fn identical_changes_are_agree() {
553 let result = merge_text("a\nb\nc\n", "a\nB\nc\n", "a\nB\nc\n");
554 assert_eq!(
555 kinds(&result),
556 vec![ChunkKind::Stable, ChunkKind::Agree, ChunkKind::Stable]
557 );
558 assert_eq!(result.agree, 1);
559 assert_eq!(result.chunks[1].ours, result.chunks[1].theirs);
560 }
561
562 #[test]
563 fn conflicting_change_detected() {
564 let result = merge_text("a\nb\nc\n", "a\nX\nc\n", "a\nY\nc\n");
565 assert_eq!(result.conflicts, 1);
566 let conflict = &result.chunks[1];
567 assert_eq!(conflict.kind, ChunkKind::Conflict);
568 assert_eq!(conflict.base, vec!["b"]);
569 assert_eq!(conflict.ours, vec!["X"]);
570 assert_eq!(conflict.theirs, vec!["Y"]);
571 }
572
573 #[test]
574 fn insertions_at_same_point_conflict() {
575 let result = merge_text("a\nb\n", "a\nx\nb\n", "a\ny\nb\n");
576 assert_eq!(result.conflicts, 1);
577 let conflict = &result.chunks[1];
578 assert!(conflict.base.is_empty());
579 assert_eq!(conflict.ours, vec!["x"]);
580 assert_eq!(conflict.theirs, vec!["y"]);
581 }
582
583 #[test]
584 fn delete_vs_edit_conflicts() {
585 let result = merge_text("a\nb\nc\n", "a\nc\n", "a\nB\nc\n");
586 assert_eq!(result.conflicts, 1);
587 let conflict = &result.chunks[1];
588 assert!(conflict.ours.is_empty());
589 assert_eq!(conflict.theirs, vec!["B"]);
590 }
591
592 #[test]
593 fn adjacent_changes_stay_separate() {
594 let result = merge_text("a\nb\nc\nd\n", "A\nB\nc\nd\n", "a\nb\nC\nD\n");
595 assert_eq!(kinds(&result), vec![ChunkKind::Ours, ChunkKind::Theirs]);
596 assert_eq!(result.conflicts, 0);
597 }
598
599 #[test]
600 fn empty_base_same_addition_agrees() {
601 let result = merge_text("", "x\n", "x\n");
602 assert_eq!(kinds(&result), vec![ChunkKind::Agree]);
603 }
604
605 #[test]
606 fn empty_base_different_additions_conflict() {
607 let result = merge_text("", "x\n", "y\n");
608 assert_eq!(kinds(&result), vec![ChunkKind::Conflict]);
609 }
610
611 #[test]
612 fn one_side_unchanged_keeps_other() {
613 let result = merge_text("a\nb\n", "a\nB\n", "a\nb\n");
614 assert_eq!(kinds(&result), vec![ChunkKind::Stable, ChunkKind::Ours]);
615 assert_eq!(result.theirs_changes, 0);
616 }
617
618 #[test]
619 fn line_numbers_track_each_side() {
620 let result = merge_text("a\nb\nc\n", "a\nx\ny\nb\nc\n", "a\nb\nC\n");
622 assert_eq!(
623 kinds(&result),
624 vec![
625 ChunkKind::Stable,
626 ChunkKind::Ours,
627 ChunkKind::Stable,
628 ChunkKind::Theirs
629 ]
630 );
631 let theirs_chunk = &result.chunks[3];
632 assert_eq!(theirs_chunk.base_start, 3);
633 assert_eq!(theirs_chunk.ours_start, 5);
634 assert_eq!(theirs_chunk.theirs_start, 3);
635 }
636
637 #[test]
638 fn crlf_lines_are_clean() {
639 let result = merge_text("a\r\nb\r\n", "a\r\nB\r\n", "a\r\nb\r\n");
640 let all_lines = result
641 .chunks
642 .iter()
643 .flat_map(|c| c.base.iter().chain(c.ours.iter()).chain(c.theirs.iter()));
644 for line in all_lines {
645 assert!(!line.contains('\r'));
646 }
647 }
648
649 #[test]
652 fn parses_standard_conflict_markers() {
653 let text = "a\n<<<<<<< HEAD\nx\n=======\ny\n>>>>>>> feature/demo\nb\n";
654 let result = parse_conflict_file(text).unwrap();
655 assert_eq!(
656 kinds(&result),
657 vec![ChunkKind::Stable, ChunkKind::Conflict, ChunkKind::Stable]
658 );
659 assert_eq!(result.conflicts, 1);
660 assert_eq!(result.ours_label.as_deref(), Some("HEAD"));
661 assert_eq!(result.theirs_label.as_deref(), Some("feature/demo"));
662
663 let conflict = &result.chunks[1];
664 assert_eq!(conflict.ours, vec!["x"]);
665 assert_eq!(conflict.theirs, vec!["y"]);
666 assert!(conflict.base.is_empty());
667 assert_eq!(conflict.ours_start, 2);
668 assert_eq!(conflict.theirs_start, 2);
669 }
670
671 #[test]
672 fn parses_diff3_style_base_section() {
673 let text =
674 "<<<<<<< HEAD\nx\n||||||| merged common ancestors\no\n=======\ny\n>>>>>>> main\n";
675 let result = parse_conflict_file(text).unwrap();
676 let conflict = &result.chunks[0];
677 assert_eq!(conflict.kind, ChunkKind::Conflict);
678 assert_eq!(conflict.base, vec!["o"]);
679 assert_eq!(conflict.ours, vec!["x"]);
680 assert_eq!(conflict.theirs, vec!["y"]);
681 }
682
683 #[test]
684 fn parses_multiple_conflicts() {
685 let text = "a\n<<<<<<<\nx\n=======\ny\n>>>>>>>\nb\n<<<<<<<\np\n=======\nq\n>>>>>>>\n";
686 let result = parse_conflict_file(text).unwrap();
687 assert_eq!(result.conflicts, 2);
688 assert!(result.ours_label.is_none());
689 let second = &result.chunks[3];
691 assert_eq!(second.ours_start, 4); assert_eq!(second.theirs_start, 4); }
694
695 #[test]
696 fn conflict_file_without_markers_errors() {
697 assert!(matches!(
698 parse_conflict_file("hello\nworld\n"),
699 Err(ConflictParseError::NoMarkers)
700 ));
701 }
702
703 #[test]
704 fn unterminated_conflict_errors() {
705 assert!(matches!(
706 parse_conflict_file("a\n<<<<<<< HEAD\nx\n=======\ny\n"),
707 Err(ConflictParseError::Malformed(2))
708 ));
709 }
710
711 #[test]
712 fn nested_marker_errors() {
713 assert!(matches!(
714 parse_conflict_file("<<<<<<< HEAD\n<<<<<<< again\n"),
715 Err(ConflictParseError::Malformed(2))
716 ));
717 }
718
719 #[test]
720 fn separator_outside_conflict_is_content() {
721 let text = "title\n=======\n<<<<<<<\nx\n=======\ny\n>>>>>>>\n";
723 let result = parse_conflict_file(text).unwrap();
724 assert_eq!(result.chunks[0].base, vec!["title", "======="]);
725 assert_eq!(result.conflicts, 1);
726 }
727
728 #[test]
729 fn conflict_file_with_crlf_is_clean() {
730 let text = "a\r\n<<<<<<< HEAD\r\nx\r\n=======\r\ny\r\n>>>>>>> main\r\n";
731 let result = parse_conflict_file(text).unwrap();
732 let conflict = &result.chunks[1];
733 assert_eq!(conflict.ours, vec!["x"]);
734 assert_eq!(conflict.theirs, vec!["y"]);
735 }
736}