1use crate::error::Result;
20use similar::{Algorithm, DiffOp, DiffTag};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24pub enum MergeFavor {
25 #[default]
27 None,
28 Ours,
30 Theirs,
32 Union,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
38pub enum ConflictStyle {
39 #[default]
41 Merge,
42 Diff3,
44 ZealousDiff3,
46}
47
48pub struct MergeInput<'a> {
50 pub base: &'a [u8],
52 pub ours: &'a [u8],
54 pub theirs: &'a [u8],
56 pub label_ours: &'a str,
58 pub label_base: &'a str,
60 pub label_theirs: &'a str,
62 pub favor: MergeFavor,
64 pub style: ConflictStyle,
66 pub marker_size: usize,
68 pub diff_algorithm: Option<String>,
70 pub ignore_all_space: bool,
72 pub ignore_space_change: bool,
74 pub ignore_space_at_eol: bool,
76 pub ignore_cr_at_eol: bool,
78}
79
80pub struct MergeOutput {
82 pub content: Vec<u8>,
84 pub conflicts: usize,
86}
87
88pub fn merge(input: &MergeInput<'_>) -> Result<MergeOutput> {
94 let base_lines = split_lines(input.base);
95 let ours_lines = split_lines(input.ours);
96 let theirs_lines = split_lines(input.theirs);
97 let ws_mode = WhitespaceMode {
98 ignore_all_space: input.ignore_all_space,
99 ignore_space_change: input.ignore_space_change,
100 ignore_space_at_eol: input.ignore_space_at_eol,
101 ignore_cr_at_eol: input.ignore_cr_at_eol,
102 };
103 let base_compare_lines = lines_for_merge_diff_compare(&base_lines, &ws_mode);
106 let ours_compare_lines = lines_for_merge_diff_compare(&ours_lines, &ws_mode);
107 let theirs_compare_lines = lines_for_merge_diff_compare(&theirs_lines, &ws_mode);
108
109 let algo = input
110 .diff_algorithm
111 .as_deref()
112 .map(|name| match name.to_lowercase().as_str() {
113 "histogram" | "patience" => similar::Algorithm::Patience,
114 _ => similar::Algorithm::Myers,
115 })
116 .unwrap_or(similar::Algorithm::Myers);
117 let ours_ops = similar::capture_diff_slices(algo, &base_compare_lines, &ours_compare_lines);
118 let theirs_ops = similar::capture_diff_slices(algo, &base_compare_lines, &theirs_compare_lines);
119
120 let mut hunks = compute_hunks(
121 &base_lines,
122 &ours_lines,
123 &theirs_lines,
124 &ours_ops,
125 &theirs_ops,
126 &ws_mode,
127 );
128 if !matches!(input.style, ConflictStyle::ZealousDiff3) {
130 hunks = merge_adjacent_replace_and_trailing_insert_conflicts(hunks);
131 hunks = merge_adjacent_one_sided_line_changes_to_conflict(hunks);
132 }
133 if !matches!(
135 input.style,
136 ConflictStyle::Diff3 | ConflictStyle::ZealousDiff3
137 ) {
138 hunks = refine_conflicts_by_subdiff(hunks, algo, &ws_mode);
139 hunks = simplify_non_conflicts_between_conflicts(hunks, true);
140 }
141 hunks = coalesce_nearby_conflicts(hunks, 3, false);
144 if matches!(input.style, ConflictStyle::ZealousDiff3) {
145 hunks = adjust_zealous_hunks(hunks);
146 }
147
148 let marker = if input.marker_size == 0 {
149 7
150 } else {
151 input.marker_size
152 };
153
154 let mut content: Vec<u8> = Vec::new();
155 let mut conflicts = 0usize;
156 let mut ours_line_pos = 0usize;
157 let mut theirs_line_pos = 0usize;
158
159 for (idx, hunk) in hunks.iter().enumerate() {
160 match hunk {
161 Hunk::Unchanged(lines) => {
162 append_lines(&mut content, lines);
163 ours_line_pos += lines.len();
164 theirs_line_pos += lines.len();
165 }
166 Hunk::OnlyOurs { ours, .. } => {
167 append_lines(&mut content, ours);
168 ours_line_pos += ours.len();
169 }
170 Hunk::OnlyTheirs { theirs, .. } => {
171 append_lines(&mut content, theirs);
172 theirs_line_pos += theirs.len();
173 }
174 Hunk::Conflict { base, ours, theirs } => {
175 let conflict_ours_start = ours_line_pos;
176 let conflict_theirs_start = theirs_line_pos;
177 match input.favor {
178 MergeFavor::Ours => {
179 append_lines(&mut content, ours);
180 ours_line_pos += ours.len();
181 }
182 MergeFavor::Theirs => {
183 append_lines(&mut content, theirs);
184 theirs_line_pos += theirs.len();
185 }
186 MergeFavor::Union => {
187 append_lines(&mut content, ours);
188 if !theirs.is_empty()
192 && !ours.is_empty()
193 && !ours.last().map(|l| l.ends_with(b"\n")).unwrap_or(false)
194 {
195 content.push(b'\n');
196 }
197 append_lines(&mut content, theirs);
198 ours_line_pos += ours.len();
199 theirs_line_pos += theirs.len();
200 }
201 MergeFavor::None => {
202 conflicts += 1;
203 if matches!(input.style, ConflictStyle::ZealousDiff3) {
204 let (mut prefix_len, mut suffix_len) =
205 common_prefix_suffix(ours, theirs);
206
207 if prefix_len > 0
208 && idx > 0
209 && hunk_output_lines(&hunks[idx - 1])
210 .map(|lines| lines_end_with(lines, &ours[..prefix_len]))
211 .unwrap_or(false)
212 {
213 prefix_len = 0;
214 }
215
216 if suffix_len > 0
217 && idx + 1 < hunks.len()
218 && hunk_output_lines(&hunks[idx + 1])
219 .map(|lines| {
220 lines_start_with(lines, &ours[ours.len() - suffix_len..])
221 })
222 .unwrap_or(false)
223 {
224 suffix_len = 0;
225 }
226
227 if prefix_len > 0 {
228 append_lines(&mut content, &ours[..prefix_len]);
229 }
230 let i1 = conflict_ours_start + prefix_len;
231 let i2 = conflict_theirs_start + prefix_len;
232 let needs_cr = conflict_markers_need_cr_at(
233 &ours_lines,
234 &theirs_lines,
235 &base_lines,
236 i1,
237 i2,
238 );
239 emit_conflict(
240 &mut content,
241 base,
242 &ours[prefix_len..ours.len() - suffix_len],
243 &theirs[prefix_len..theirs.len() - suffix_len],
244 input.label_ours,
245 input.label_base,
246 input.label_theirs,
247 input.style,
248 marker,
249 needs_cr,
250 );
251 if suffix_len > 0 {
252 append_lines(&mut content, &ours[ours.len() - suffix_len..]);
253 }
254 } else if matches!(input.style, ConflictStyle::Merge) {
255 let (prefix_len, suffix_len) =
256 common_prefix_suffix_merge_lines(ours, theirs);
257 let pre = &ours[..prefix_len];
258 let suf_start = ours.len().saturating_sub(suffix_len);
259 let o_mid = &ours[prefix_len..suf_start];
260 let t_mid =
261 &theirs[prefix_len..theirs.len().saturating_sub(suffix_len)];
262 let suf = &ours[suf_start..];
263 append_lines(&mut content, pre);
264 let i1 = conflict_ours_start + prefix_len;
265 let i2 = conflict_theirs_start + prefix_len;
266 let needs_cr = conflict_markers_need_cr_at(
267 &ours_lines,
268 &theirs_lines,
269 &base_lines,
270 i1,
271 i2,
272 );
273 emit_conflict(
274 &mut content,
275 base,
276 o_mid,
277 t_mid,
278 input.label_ours,
279 input.label_base,
280 input.label_theirs,
281 input.style,
282 marker,
283 needs_cr,
284 );
285 append_lines(&mut content, suf);
286 } else {
287 let needs_cr = conflict_markers_need_cr_at(
288 &ours_lines,
289 &theirs_lines,
290 &base_lines,
291 conflict_ours_start,
292 conflict_theirs_start,
293 );
294 emit_conflict(
295 &mut content,
296 base,
297 ours,
298 theirs,
299 input.label_ours,
300 input.label_base,
301 input.label_theirs,
302 input.style,
303 marker,
304 needs_cr,
305 );
306 }
307 ours_line_pos += ours.len();
308 theirs_line_pos += theirs.len();
309 }
310 }
311 }
312 }
313 }
314
315 Ok(MergeOutput { content, conflicts })
316}
317
318fn append_lines(out: &mut Vec<u8>, lines: &[Vec<u8>]) {
319 if !out.is_empty() && !out.ends_with(b"\n") && !lines.is_empty() {
322 out.push(b'\n');
323 }
324 for line in lines {
325 out.extend_from_slice(line);
326 }
327}
328
329fn common_prefix_suffix(ours: &[Vec<u8>], theirs: &[Vec<u8>]) -> (usize, usize) {
330 let mut prefix = 0usize;
331 while prefix < ours.len() && prefix < theirs.len() && ours[prefix] == theirs[prefix] {
332 prefix += 1;
333 }
334
335 let mut suffix = 0usize;
336 while suffix < ours.len().saturating_sub(prefix)
337 && suffix < theirs.len().saturating_sub(prefix)
338 && ours[ours.len() - 1 - suffix] == theirs[theirs.len() - 1 - suffix]
339 {
340 suffix += 1;
341 }
342
343 (prefix, suffix)
344}
345
346fn hunk_output_lines(hunk: &Hunk) -> Option<&[Vec<u8>]> {
347 match hunk {
348 Hunk::Unchanged(lines) => Some(lines),
349 Hunk::OnlyOurs { ours, .. } => Some(ours),
350 Hunk::OnlyTheirs { theirs, .. } => Some(theirs),
351 Hunk::Conflict { .. } => None,
352 }
353}
354
355fn lines_end_with(lines: &[Vec<u8>], suffix: &[Vec<u8>]) -> bool {
356 if suffix.is_empty() || suffix.len() > lines.len() {
357 return false;
358 }
359 lines[lines.len() - suffix.len()..] == *suffix
360}
361
362fn lines_start_with(lines: &[Vec<u8>], prefix: &[Vec<u8>]) -> bool {
363 if prefix.is_empty() || prefix.len() > lines.len() {
364 return false;
365 }
366 lines[..prefix.len()] == *prefix
367}
368
369#[allow(clippy::too_many_arguments)]
371fn emit_conflict(
372 out: &mut Vec<u8>,
373 base: &[Vec<u8>],
374 ours: &[Vec<u8>],
375 theirs: &[Vec<u8>],
376 label_ours: &str,
377 label_base: &str,
378 label_theirs: &str,
379 style: ConflictStyle,
380 marker: usize,
381 needs_cr: bool,
382) {
383 let open = "<".repeat(marker);
384 let eq = "=".repeat(marker);
385 let close = ">".repeat(marker);
386 let marker_eol: &[u8] = if needs_cr { b"\r\n" } else { b"\n" };
387
388 if !out.is_empty() && !out.ends_with(b"\n") && !out.ends_with(b"\r\n") {
391 out.extend_from_slice(marker_eol);
392 }
393
394 write_conflict_marker_line(out, &open, Some(label_ours), marker_eol);
395 for line in ours {
396 out.extend_from_slice(line);
397 }
398 if out.last().copied() != Some(b'\n') {
400 out.extend_from_slice(marker_eol);
401 }
402 match style {
403 ConflictStyle::Diff3 | ConflictStyle::ZealousDiff3 => {
404 let pipe = "|".repeat(marker);
405 write_conflict_marker_line(out, &pipe, Some(label_base), marker_eol);
406 for line in base {
407 out.extend_from_slice(line);
408 }
409 if out.last().copied() != Some(b'\n') {
410 out.extend_from_slice(marker_eol);
411 }
412 write_conflict_marker_line(out, &eq, None, marker_eol);
413 }
414 ConflictStyle::Merge => {
415 write_conflict_marker_line(out, &eq, None, marker_eol);
416 }
417 }
418 for line in theirs {
419 out.extend_from_slice(line);
420 }
421 if out.last().copied() != Some(b'\n') {
422 out.extend_from_slice(marker_eol);
423 }
424 write_conflict_marker_line(out, &close, Some(label_theirs), marker_eol);
425}
426
427fn write_conflict_marker_line(out: &mut Vec<u8>, marker: &str, label: Option<&str>, eol: &[u8]) {
428 out.extend_from_slice(marker.as_bytes());
429 let open_or_close = marker.starts_with('<') || marker.starts_with('>');
432 let has_label = label.is_some_and(|l| !l.is_empty());
433 if open_or_close || has_label {
436 out.push(b' ');
437 }
438 if let Some(label) = label {
439 if !label.is_empty() {
440 out.extend_from_slice(label.as_bytes());
441 }
442 }
443 out.extend_from_slice(eol);
444}
445
446fn strip_line_terminator(line: &[u8]) -> &[u8] {
448 if line.ends_with(b"\r\n") {
449 return &line[..line.len() - 2];
450 }
451 if line.ends_with(b"\n") {
452 return &line[..line.len() - 1];
453 }
454 if line.ends_with(b"\r") {
455 return &line[..line.len() - 1];
456 }
457 line
458}
459
460fn merge_line_equal(a: &[u8], b: &[u8]) -> bool {
462 strip_line_terminator(a) == strip_line_terminator(b)
463}
464
465fn common_prefix_suffix_merge_lines(ours: &[Vec<u8>], theirs: &[Vec<u8>]) -> (usize, usize) {
466 let mut prefix = 0usize;
467 while prefix < ours.len()
468 && prefix < theirs.len()
469 && merge_line_equal(&ours[prefix], &theirs[prefix])
470 {
471 prefix += 1;
472 }
473
474 let mut suffix = 0usize;
475 while suffix < ours.len().saturating_sub(prefix)
476 && suffix < theirs.len().saturating_sub(prefix)
477 && merge_line_equal(
478 &ours[ours.len() - 1 - suffix],
479 &theirs[theirs.len() - 1 - suffix],
480 )
481 {
482 suffix += 1;
483 }
484
485 (prefix, suffix)
486}
487
488fn is_eol_crlf_line(line: &[u8]) -> i8 {
490 if line.ends_with(b"\r\n") {
491 return 1;
492 }
493 if line.ends_with(b"\n") {
494 return 0;
495 }
496 -1
497}
498
499fn conflict_markers_need_cr_at(
501 ours_lines: &[Vec<u8>],
502 theirs_lines: &[Vec<u8>],
503 base_lines: &[Vec<u8>],
504 i1: usize,
505 i2: usize,
506) -> bool {
507 let mut needs = if i1 > 0 {
508 is_eol_crlf_line(ours_lines[i1 - 1].as_slice())
509 } else {
510 -1
511 };
512 if needs > 0 {
513 let t0 = if i2 > 0 {
514 is_eol_crlf_line(theirs_lines[i2 - 1].as_slice())
515 } else {
516 -1
517 };
518 if t0 >= 0 {
519 needs = t0;
520 }
521 }
522 if needs > 0 && !base_lines.is_empty() {
523 needs = is_eol_crlf_line(base_lines[0].as_slice());
524 }
525 needs > 0
526}
527
528fn line_contains_alnum(line: &[u8]) -> bool {
529 strip_line_terminator(line)
530 .iter()
531 .any(|b| b.is_ascii_alphanumeric())
532}
533
534fn gap_blocks_merge_between_conflicts(gap: &[Vec<u8>], simplify_if_no_alnum: bool) -> bool {
539 if gap.len() <= 3 {
540 return false;
541 }
542 !simplify_if_no_alnum || gap.iter().any(|l| line_contains_alnum(l.as_slice()))
543}
544
545fn merge_two_adjacent_conflicts(left: &Hunk, right: &Hunk) -> Option<Hunk> {
546 let Hunk::Conflict {
547 base: b1,
548 ours: o1,
549 theirs: t1,
550 } = left
551 else {
552 return None;
553 };
554 let Hunk::Conflict {
555 base: b2,
556 ours: o2,
557 theirs: t2,
558 } = right
559 else {
560 return None;
561 };
562 let mut merged_base = b1.clone();
563 merged_base.extend(b2.iter().cloned());
564 let mut merged_ours = o1.clone();
565 merged_ours.extend(o2.iter().cloned());
566 let mut merged_theirs = t1.clone();
567 merged_theirs.extend(t2.iter().cloned());
568 Some(Hunk::Conflict {
569 base: merged_base,
570 ours: merged_ours,
571 theirs: merged_theirs,
572 })
573}
574
575fn simplify_non_conflicts_between_conflicts(
577 hunks: Vec<Hunk>,
578 simplify_if_no_alnum: bool,
579) -> Vec<Hunk> {
580 let mut out: Vec<Hunk> = Vec::with_capacity(hunks.len());
581 let mut i = 0usize;
582 while i < hunks.len() {
583 let Hunk::Conflict { .. } = &hunks[i] else {
584 out.push(hunks[i].clone());
585 i += 1;
586 continue;
587 };
588 let mut merged = hunks[i].clone();
589 let mut j = i;
590 loop {
591 let Some(Hunk::Unchanged(gap)) = hunks.get(j + 1) else {
592 break;
593 };
594 let Some(Hunk::Conflict { .. }) = hunks.get(j + 2) else {
595 break;
596 };
597 if gap_blocks_merge_between_conflicts(gap, simplify_if_no_alnum) {
598 break;
599 }
600 let Some(m) = merge_two_adjacent_conflicts(&merged, &hunks[j + 2]) else {
601 break;
602 };
603 merged = m;
604 j += 2;
605 }
606 out.push(merged);
607 i = j + 1;
608 }
609 out
610}
611
612fn refine_conflicts_by_subdiff(
614 hunks: Vec<Hunk>,
615 algo: Algorithm,
616 ws_mode: &WhitespaceMode,
617) -> Vec<Hunk> {
618 let mut out = Vec::with_capacity(hunks.len());
619 for h in hunks {
620 let Hunk::Conflict { base, ours, theirs } = h else {
621 out.push(h);
622 continue;
623 };
624 if ours.is_empty() || theirs.is_empty() {
625 out.push(Hunk::Conflict { base, ours, theirs });
626 continue;
627 }
628 let o_cmp = normalize_lines_for_compare(&ours, ws_mode);
629 let t_cmp = normalize_lines_for_compare(&theirs, ws_mode);
630 let sub_ops = similar::capture_diff_slices(algo, &o_cmp, &t_cmp);
631 if sub_ops.len() == 1 && sub_ops[0].tag() == DiffTag::Equal {
632 out.push(Hunk::Unchanged(ours));
633 continue;
634 }
635 for op in &sub_ops {
636 let old = op.old_range();
637 let new_ = op.new_range();
638 match op.tag() {
639 DiffTag::Equal => {
640 if !old.is_empty() {
641 out.push(Hunk::Unchanged(ours[old.start..old.end].to_vec()));
642 }
643 }
644 DiffTag::Delete => {
645 if !old.is_empty() {
646 out.push(Hunk::OnlyOurs {
647 base: Vec::new(),
648 ours: ours[old.start..old.end].to_vec(),
649 });
650 }
651 }
652 DiffTag::Insert => {
653 if !new_.is_empty() {
654 out.push(Hunk::OnlyTheirs {
655 base: Vec::new(),
656 theirs: theirs[new_.start..new_.end].to_vec(),
657 });
658 }
659 }
660 DiffTag::Replace => {
661 out.push(Hunk::Conflict {
662 base: Vec::new(),
663 ours: ours[old.start..old.end].to_vec(),
664 theirs: theirs[new_.start..new_.end].to_vec(),
665 });
666 }
667 }
668 }
669 }
670 out
671}
672
673#[derive(Debug, Clone)]
675enum Hunk {
676 Unchanged(Vec<Vec<u8>>),
678 OnlyOurs {
680 base: Vec<Vec<u8>>,
682 ours: Vec<Vec<u8>>,
684 },
685 OnlyTheirs {
687 base: Vec<Vec<u8>>,
689 theirs: Vec<Vec<u8>>,
691 },
692 Conflict {
694 base: Vec<Vec<u8>>,
695 ours: Vec<Vec<u8>>,
696 theirs: Vec<Vec<u8>>,
697 },
698}
699
700fn compute_hunks(
705 base: &[Vec<u8>],
706 ours: &[Vec<u8>],
707 theirs: &[Vec<u8>],
708 ours_ops: &[DiffOp],
709 theirs_ops: &[DiffOp],
710 ws_mode: &WhitespaceMode,
711) -> Vec<Hunk> {
712 let ours_changed = changed_mask(ours_ops, base.len());
713 let theirs_changed = changed_mask(theirs_ops, base.len());
714
715 let ours_inserts = collect_inserts(ours_ops, base.len());
716 let theirs_inserts = collect_inserts(theirs_ops, base.len());
717
718 let mut hunks: Vec<Hunk> = Vec::new();
719 let mut pos = 0usize;
720
721 while pos <= base.len() {
722 emit_inserts_at(
724 pos,
725 &ours_inserts,
726 &theirs_inserts,
727 ours,
728 theirs,
729 &mut hunks,
730 );
731
732 if pos == base.len() {
733 break;
734 }
735
736 let o = ours_changed[pos];
737 let t = theirs_changed[pos];
738
739 if !o && !t {
740 let mut end = pos + 1;
744 while end < base.len()
745 && !ours_changed[end]
746 && !theirs_changed[end]
747 && ours_inserts[end].is_empty()
748 && theirs_inserts[end].is_empty()
749 {
750 end += 1;
751 }
752 let terminator_only_diff = base.len() == ours.len()
753 && (pos..end).all(|i| merge_line_equal(&base[i], &ours[i]))
754 && (pos..end).any(|i| base[i] != ours[i]);
755 let unchanged_lines = if ws_mode.ignore_all_space
756 || ws_mode.ignore_space_change
757 || ws_mode.ignore_space_at_eol
758 || ws_mode.ignore_cr_at_eol
759 || terminator_only_diff
760 {
761 &ours[pos..end]
762 } else {
763 &base[pos..end]
764 };
765 hunks.push(Hunk::Unchanged(unchanged_lines.to_vec()));
766 pos = end;
767 continue;
768 }
769
770 let mut end = pos + 1;
773 loop {
774 let new_end = furthest_changed_op_end(ours_ops, pos, end)
775 .max(furthest_changed_op_end(theirs_ops, pos, end));
776 if new_end <= end {
777 break;
778 }
779 end = new_end;
780 }
781
782 let any_ours = (pos..end).any(|p| ours_changed[p]);
784 let any_theirs = (pos..end).any(|p| theirs_changed[p]);
785
786 match (any_ours, any_theirs) {
787 (true, false) => {
788 let c = collect_new_lines(ours_ops, ours, pos, end);
789 hunks.push(Hunk::OnlyOurs {
790 base: base[pos..end].to_vec(),
791 ours: c,
792 });
793 }
794 (false, true) => {
795 let c = collect_new_lines(theirs_ops, theirs, pos, end);
796 hunks.push(Hunk::OnlyTheirs {
797 base: base[pos..end].to_vec(),
798 theirs: c,
799 });
800 }
801 (true, true) => {
802 let o = collect_new_lines(ours_ops, ours, pos, end);
803 let t = collect_new_lines(theirs_ops, theirs, pos, end);
804 hunks.push(Hunk::Conflict {
807 base: base[pos..end].to_vec(),
808 ours: o,
809 theirs: t,
810 });
811 }
812 (false, false) => {
813 hunks.push(Hunk::Unchanged(base[pos..end].to_vec()));
815 }
816 }
817
818 pos = end;
819 }
820
821 hunks
822}
823
824fn changed_mask(ops: &[DiffOp], base_len: usize) -> Vec<bool> {
827 let mut mask = vec![false; base_len];
828 for op in ops {
829 if op.tag() == DiffTag::Equal {
830 continue;
831 }
832 for p in op.old_range() {
833 if p < base_len {
834 mask[p] = true;
835 }
836 }
837 }
838 mask
839}
840
841fn collect_inserts(ops: &[DiffOp], base_len: usize) -> Vec<Vec<(usize, usize)>> {
846 let mut result: Vec<Vec<(usize, usize)>> = vec![Vec::new(); base_len + 1];
847 for op in ops {
848 let old = op.old_range();
849 let new_ = op.new_range();
850 if old.is_empty() && !new_.is_empty() {
851 let pos = old.start.min(base_len);
852 result[pos].push((new_.start, new_.end));
853 }
854 }
855 result
856}
857
858fn emit_inserts_at(
860 pos: usize,
861 ours_inserts: &[Vec<(usize, usize)>],
862 theirs_inserts: &[Vec<(usize, usize)>],
863 ours: &[Vec<u8>],
864 theirs: &[Vec<u8>],
865 hunks: &mut Vec<Hunk>,
866) {
867 let o_ins = &ours_inserts[pos];
868 let t_ins = &theirs_inserts[pos];
869
870 let has_ours = !o_ins.is_empty();
871 let has_theirs = !t_ins.is_empty();
872
873 if has_ours && has_theirs {
874 let o_lines: Vec<Vec<u8>> = o_ins
878 .iter()
879 .flat_map(|&(s, e)| ours[s..e].to_vec())
880 .collect();
881 let t_lines: Vec<Vec<u8>> = t_ins
882 .iter()
883 .flat_map(|&(s, e)| theirs[s..e].to_vec())
884 .collect();
885
886 let mut common_len = 0usize;
887 while common_len < o_lines.len()
888 && common_len < t_lines.len()
889 && o_lines[common_len] == t_lines[common_len]
890 {
891 common_len += 1;
892 }
893
894 if common_len > 0 {
895 hunks.push(Hunk::Unchanged(o_lines[..common_len].to_vec()));
896 }
897
898 let ours_tail = o_lines[common_len..].to_vec();
899 let theirs_tail = t_lines[common_len..].to_vec();
900
901 let ours_has_extra = !ours_tail.is_empty();
902 let theirs_has_extra = !theirs_tail.is_empty();
903
904 if ours_has_extra && theirs_has_extra {
905 hunks.push(Hunk::Conflict {
906 base: Vec::new(),
907 ours: ours_tail,
908 theirs: theirs_tail,
909 });
910 } else if ours_has_extra {
911 hunks.push(Hunk::OnlyOurs {
912 base: Vec::new(),
913 ours: ours_tail,
914 });
915 } else if theirs_has_extra {
916 hunks.push(Hunk::OnlyTheirs {
917 base: Vec::new(),
918 theirs: theirs_tail,
919 });
920 }
921 } else if has_ours {
922 for &(ns, ne) in o_ins {
923 let lines: Vec<Vec<u8>> = ours[ns..ne].to_vec();
924 if !lines.is_empty() {
925 hunks.push(Hunk::OnlyOurs {
926 base: Vec::new(),
927 ours: lines,
928 });
929 }
930 }
931 } else if has_theirs {
932 for &(ns, ne) in t_ins {
933 let lines: Vec<Vec<u8>> = theirs[ns..ne].to_vec();
934 if !lines.is_empty() {
935 hunks.push(Hunk::OnlyTheirs {
936 base: Vec::new(),
937 theirs: lines,
938 });
939 }
940 }
941 }
942}
943
944fn adjust_zealous_hunks(hunks: Vec<Hunk>) -> Vec<Hunk> {
945 let mut out: Vec<Hunk> = Vec::new();
946 let mut i = 0usize;
947
948 while i < hunks.len() {
949 let mut consumed = 1usize;
950 let mut transformed: Option<Vec<Hunk>> = None;
951
952 let (pre_insert, mid_idx) = match &hunks[i] {
953 Hunk::OnlyTheirs { base, theirs } if base.is_empty() => {
954 (Some(theirs.as_slice()), i + 1)
955 }
956 _ => (None, i),
957 };
958
959 if let Some(Hunk::OnlyOurs { base, ours }) = hunks.get(mid_idx) {
960 if !base.is_empty() {
961 let post_insert = match hunks.get(mid_idx + 1) {
962 Some(Hunk::OnlyTheirs { base, theirs }) if base.is_empty() => {
963 Some(theirs.as_slice())
964 }
965 _ => None,
966 };
967
968 let mut prefix_len = 0usize;
969 if let Some(pre) = pre_insert {
970 if !pre.is_empty() && ours.starts_with(pre) {
971 prefix_len = pre.len();
972 }
973 }
974
975 let mut suffix_len = 0usize;
976 if let Some(post) = post_insert {
977 if !post.is_empty() && ours[prefix_len..].ends_with(post) {
978 suffix_len = post.len();
979 }
980 }
981
982 if prefix_len > 0 || suffix_len > 0 {
983 consumed = if pre_insert.is_some() {
984 if post_insert.is_some() {
985 3
986 } else {
987 2
988 }
989 } else if post_insert.is_some() {
990 2
991 } else {
992 1
993 };
994
995 let mut replacement: Vec<Hunk> = Vec::new();
996 if prefix_len > 0 {
997 replacement.push(Hunk::Unchanged(ours[..prefix_len].to_vec()));
998 }
999 replacement.push(Hunk::Conflict {
1000 base: base.clone(),
1001 ours: ours[prefix_len..ours.len() - suffix_len].to_vec(),
1002 theirs: base.clone(),
1003 });
1004 if suffix_len > 0 {
1005 replacement.push(Hunk::Unchanged(ours[ours.len() - suffix_len..].to_vec()));
1006 }
1007 transformed = Some(replacement);
1008 }
1009 }
1010 }
1011
1012 if let Some(replacement) = transformed {
1013 for h in replacement {
1014 push_hunk_with_unchanged_merge(&mut out, h);
1015 }
1016 i += consumed;
1017 continue;
1018 }
1019
1020 push_hunk_with_unchanged_merge(&mut out, hunks[i].clone());
1021 i += 1;
1022 }
1023
1024 out
1025}
1026
1027fn merge_adjacent_one_sided_line_changes_to_conflict(hunks: Vec<Hunk>) -> Vec<Hunk> {
1034 let mut out: Vec<Hunk> = Vec::with_capacity(hunks.len());
1035 let mut i = 0usize;
1036 while i < hunks.len() {
1037 let merged = match (&hunks[i], hunks.get(i + 1)) {
1038 (
1039 Hunk::OnlyTheirs {
1040 base: b1,
1041 theirs: t1,
1042 },
1043 Some(Hunk::OnlyOurs { base: b2, ours: o2 }),
1044 ) if b1.len() == 1 && b2.len() == 1 && t1.len() == 1 && o2.len() == 1 => {
1045 Some(Hunk::Conflict {
1046 base: vec![b1[0].clone(), b2[0].clone()],
1047 ours: vec![b1[0].clone(), o2[0].clone()],
1048 theirs: vec![t1[0].clone(), b2[0].clone()],
1049 })
1050 }
1051 (
1052 Hunk::OnlyOurs { base: b1, ours: o1 },
1053 Some(Hunk::OnlyTheirs {
1054 base: b2,
1055 theirs: t2,
1056 }),
1057 ) if b1.len() == 1 && b2.len() == 1 && o1.len() == 1 && t2.len() == 1 => {
1058 Some(Hunk::Conflict {
1059 base: vec![b1[0].clone(), b2[0].clone()],
1060 ours: vec![o1[0].clone(), b2[0].clone()],
1061 theirs: vec![b1[0].clone(), t2[0].clone()],
1062 })
1063 }
1064 _ => None,
1065 };
1066 if let Some(h) = merged {
1067 out.push(h);
1068 i += 2;
1069 } else {
1070 out.push(hunks[i].clone());
1071 i += 1;
1072 }
1073 }
1074 out
1075}
1076
1077fn merge_adjacent_replace_and_trailing_insert_conflicts(hunks: Vec<Hunk>) -> Vec<Hunk> {
1078 let mut out: Vec<Hunk> = Vec::with_capacity(hunks.len());
1079 let mut i = 0usize;
1080 while i < hunks.len() {
1081 let merged = match (&hunks[i], hunks.get(i + 1)) {
1082 (Hunk::OnlyTheirs { base, theirs }, Some(Hunk::OnlyOurs { base: bo, ours: o }))
1083 if !base.is_empty() && bo.is_empty() && !o.is_empty() && !theirs.is_empty() =>
1084 {
1085 Some(Hunk::Conflict {
1086 base: base.clone(),
1087 ours: o.clone(),
1088 theirs: theirs.clone(),
1089 })
1090 }
1091 (
1092 Hunk::OnlyOurs { base, ours },
1093 Some(Hunk::OnlyTheirs {
1094 base: bt,
1095 theirs: t,
1096 }),
1097 ) if !base.is_empty() && bt.is_empty() && !t.is_empty() && !ours.is_empty() => {
1098 Some(Hunk::Conflict {
1099 base: base.clone(),
1100 ours: ours.clone(),
1101 theirs: t.clone(),
1102 })
1103 }
1104 _ => None,
1105 };
1106 if let Some(h) = merged {
1107 out.push(h);
1108 i += 2;
1109 } else {
1110 out.push(hunks[i].clone());
1111 i += 1;
1112 }
1113 }
1114 out
1115}
1116
1117fn coalesce_nearby_conflicts(hunks: Vec<Hunk>, max_gap_lines: usize, enable: bool) -> Vec<Hunk> {
1118 if !enable {
1119 return hunks;
1120 }
1121 let mut out: Vec<Hunk> = Vec::new();
1122 let mut i = 0usize;
1123
1124 while i < hunks.len() {
1125 let Some(Hunk::Conflict { base, ours, theirs }) = hunks.get(i) else {
1126 out.push(hunks[i].clone());
1127 i += 1;
1128 continue;
1129 };
1130
1131 let mut merged_base = base.clone();
1132 let mut merged_ours = ours.clone();
1133 let mut merged_theirs = theirs.clone();
1134 let mut j = i;
1135
1136 loop {
1137 let Some(Hunk::Unchanged(gap)) = hunks.get(j + 1) else {
1138 break;
1139 };
1140 let Some(Hunk::Conflict {
1141 base: next_base,
1142 ours: next_ours,
1143 theirs: next_theirs,
1144 }) = hunks.get(j + 2)
1145 else {
1146 break;
1147 };
1148 if gap.len() > max_gap_lines {
1149 break;
1150 }
1151
1152 merged_base.extend(gap.iter().cloned());
1153 merged_base.extend(next_base.iter().cloned());
1154 merged_ours.extend(gap.iter().cloned());
1155 merged_ours.extend(next_ours.iter().cloned());
1156 merged_theirs.extend(gap.iter().cloned());
1157 merged_theirs.extend(next_theirs.iter().cloned());
1158 j += 2;
1159 }
1160
1161 out.push(Hunk::Conflict {
1162 base: merged_base,
1163 ours: merged_ours,
1164 theirs: merged_theirs,
1165 });
1166 i = j + 1;
1167 }
1168
1169 out
1170}
1171
1172fn push_hunk_with_unchanged_merge(out: &mut Vec<Hunk>, hunk: Hunk) {
1173 match hunk {
1174 Hunk::Unchanged(mut lines) => {
1175 if let Some(Hunk::Unchanged(prev)) = out.last_mut() {
1176 prev.append(&mut lines);
1177 } else if !lines.is_empty() {
1178 out.push(Hunk::Unchanged(lines));
1179 }
1180 }
1181 other => out.push(other),
1182 }
1183}
1184
1185fn furthest_changed_op_end(ops: &[DiffOp], run_start: usize, current_end: usize) -> usize {
1189 let mut max = current_end;
1190 for op in ops {
1191 if op.tag() == DiffTag::Equal {
1192 continue;
1193 }
1194 let old = op.old_range();
1195 if old.start < current_end && old.end > run_start && old.end > max {
1196 max = old.end;
1197 }
1198 }
1199 max
1200}
1201
1202fn collect_new_lines(
1208 ops: &[DiffOp],
1209 new: &[Vec<u8>],
1210 base_start: usize,
1211 base_end: usize,
1212) -> Vec<Vec<u8>> {
1213 let mut lines = Vec::new();
1214 for op in ops {
1215 let old = op.old_range();
1216 let new_ = op.new_range();
1217 if old.is_empty() {
1218 continue; }
1220 if old.end <= base_start || old.start >= base_end {
1221 continue; }
1223 if op.tag() == DiffTag::Equal {
1224 let overlap_start = base_start.max(old.start);
1225 let overlap_end = base_end.min(old.end);
1226 let offset = overlap_start - old.start;
1227 let len = overlap_end - overlap_start;
1228 for i in offset..offset + len {
1229 if new_.start + i < new_.end {
1230 lines.push(new[new_.start + i].clone());
1231 }
1232 }
1233 } else {
1234 for i in new_.clone() {
1235 lines.push(new[i].clone());
1236 }
1237 }
1238 }
1239 lines
1240}
1241
1242fn _diff_ops(old: &[Vec<u8>], new: &[Vec<u8>]) -> Vec<DiffOp> {
1244 similar::capture_diff_slices(Algorithm::Myers, old, new)
1245}
1246
1247fn split_lines(data: &[u8]) -> Vec<Vec<u8>> {
1249 if data.is_empty() {
1250 return Vec::new();
1251 }
1252 let mut lines = Vec::new();
1253 let mut start = 0;
1254 for i in 0..data.len() {
1255 if data[i] == b'\n' {
1256 lines.push(data[start..=i].to_vec());
1257 start = i + 1;
1258 }
1259 }
1260 if start < data.len() {
1261 lines.push(data[start..].to_vec());
1262 }
1263 lines
1264}
1265
1266#[derive(Clone, Copy, Debug, Default)]
1267struct WhitespaceMode {
1268 ignore_all_space: bool,
1269 ignore_space_change: bool,
1270 ignore_space_at_eol: bool,
1271 ignore_cr_at_eol: bool,
1272}
1273
1274fn normalize_lines_for_compare(lines: &[Vec<u8>], mode: &WhitespaceMode) -> Vec<Vec<u8>> {
1275 lines
1276 .iter()
1277 .map(|line| normalize_line_for_compare(line, mode))
1278 .collect()
1279}
1280
1281fn lines_for_merge_diff_compare(lines: &[Vec<u8>], mode: &WhitespaceMode) -> Vec<Vec<u8>> {
1282 lines
1283 .iter()
1284 .map(|line| normalize_line_for_compare(strip_line_terminator(line), mode))
1285 .collect()
1286}
1287
1288fn normalize_line_for_compare(line: &[u8], mode: &WhitespaceMode) -> Vec<u8> {
1289 let mut bytes = line.to_vec();
1290
1291 if mode.ignore_cr_at_eol && bytes.ends_with(b"\r\n") {
1292 let len = bytes.len();
1293 bytes.remove(len - 2);
1294 }
1295
1296 if mode.ignore_all_space {
1297 return bytes
1298 .into_iter()
1299 .filter(|b| !b.is_ascii_whitespace())
1300 .collect();
1301 }
1302
1303 if mode.ignore_space_change {
1304 let mut out = Vec::with_capacity(bytes.len());
1305 let mut in_ws = false;
1306 for ch in bytes {
1307 if ch.is_ascii_whitespace() {
1308 if !in_ws {
1309 out.push(b' ');
1310 in_ws = true;
1311 }
1312 } else {
1313 out.push(ch);
1314 in_ws = false;
1315 }
1316 }
1317 while out.last().is_some_and(|b| b.is_ascii_whitespace()) {
1318 out.pop();
1319 }
1320 return out;
1321 }
1322
1323 if mode.ignore_space_at_eol {
1324 if bytes.last().copied() == Some(b'\n') {
1325 let mut body = bytes[..bytes.len() - 1].to_vec();
1326 while body.last().is_some_and(|b| b.is_ascii_whitespace()) {
1327 body.pop();
1328 }
1329 body.push(b'\n');
1330 bytes = body;
1331 } else {
1332 while bytes.last().is_some_and(|b| b.is_ascii_whitespace()) {
1333 bytes.pop();
1334 }
1335 }
1336 }
1337
1338 bytes
1339}
1340
1341#[must_use]
1345pub fn is_binary(data: &[u8]) -> bool {
1346 data.contains(&0u8)
1347}
1348
1349#[cfg(test)]
1350mod tests {
1351 use super::*;
1352
1353 fn do_merge(base: &str, ours: &str, theirs: &str) -> (String, usize) {
1354 let input = MergeInput {
1355 base: base.as_bytes(),
1356 ours: ours.as_bytes(),
1357 theirs: theirs.as_bytes(),
1358 label_ours: "ours",
1359 label_base: "base",
1360 label_theirs: "theirs",
1361 favor: MergeFavor::None,
1362 style: ConflictStyle::Merge,
1363 marker_size: 7,
1364 diff_algorithm: None,
1365 ignore_all_space: false,
1366 ignore_space_change: false,
1367 ignore_space_at_eol: false,
1368 ignore_cr_at_eol: false,
1369 };
1370 let out = merge(&input).unwrap();
1371 (String::from_utf8(out.content).unwrap(), out.conflicts)
1372 }
1373
1374 #[test]
1375 fn no_changes() {
1376 let t = "line1\nline2\nline3\n";
1377 let (r, c) = do_merge(t, t, t);
1378 assert_eq!(r, t);
1379 assert_eq!(c, 0);
1380 }
1381
1382 #[test]
1383 fn only_ours() {
1384 let (r, c) = do_merge("a\nb\nc\n", "a\nB\nc\n", "a\nb\nc\n");
1385 assert_eq!(r, "a\nB\nc\n");
1386 assert_eq!(c, 0);
1387 }
1388
1389 #[test]
1390 fn only_theirs() {
1391 let (r, c) = do_merge("a\nb\nc\n", "a\nb\nc\n", "a\nB\nc\n");
1392 assert_eq!(r, "a\nB\nc\n");
1393 assert_eq!(c, 0);
1394 }
1395
1396 #[test]
1397 fn conflict() {
1398 let (r, c) = do_merge("a\nb\nc\n", "a\nX\nc\n", "a\nY\nc\n");
1399 assert_eq!(c, 1);
1400 assert!(r.contains("<<<<<<< ours\nX\n=======\nY\n>>>>>>> theirs"));
1401 }
1402
1403 #[test]
1404 fn t4108_style_overlapping_edits_conflict() {
1405 let base = "1\n2\n3\n4\n5\n6\n7\n";
1406 let ours = "1\ntwo\n3\n4\n5\nsix\n7\n";
1407 let theirs = "1\n2\n3\n4\nfive\n6\n7\n";
1408 let (_r, c) = do_merge(base, ours, theirs);
1409 assert!(
1410 c >= 1,
1411 "expected at least one conflict region (Git merge reports CONFLICT for this shape)"
1412 );
1413 }
1414
1415 #[test]
1416 fn conflict_delete_vs_change() {
1417 let base = "a\nb\nc\n";
1419 let ours = "c\n"; let theirs = "A\nb\nc\n"; let (r, c) = do_merge(base, ours, theirs);
1422 assert_eq!(c, 1, "expected conflict, got: {r:?}");
1423 }
1424
1425 #[test]
1426 fn conflict_replace_vs_insert_after_same_line() {
1427 let base = "hello\n";
1429 let ours = "hello\nhello\n";
1430 let theirs = "remove-conflict\n";
1431 let (r, c) = do_merge(base, ours, theirs);
1432 assert_eq!(c, 1, "expected conflict, got: {r:?}");
1433 }
1434
1435 #[test]
1436 fn favor_ours() {
1437 let input = MergeInput {
1438 base: b"a\nb\nc\n",
1439 ours: b"a\nX\nc\n",
1440 theirs: b"a\nY\nc\n",
1441 label_ours: "o",
1442 label_base: "b",
1443 label_theirs: "t",
1444 favor: MergeFavor::Ours,
1445 style: ConflictStyle::Merge,
1446 marker_size: 7,
1447 diff_algorithm: None,
1448 ignore_all_space: false,
1449 ignore_space_change: false,
1450 ignore_space_at_eol: false,
1451 ignore_cr_at_eol: false,
1452 };
1453 let out = merge(&input).unwrap();
1454 assert_eq!(out.content, b"a\nX\nc\n");
1455 assert_eq!(out.conflicts, 0);
1456 }
1457
1458 #[test]
1459 fn union_missing_lf() {
1460 let input = MergeInput {
1461 base: b"line1\nline2\nline3",
1462 ours: b"line1\nline2\nline3x",
1463 theirs: b"line1\nline2\nline3y",
1464 label_ours: "o",
1465 label_base: "b",
1466 label_theirs: "t",
1467 favor: MergeFavor::Union,
1468 style: ConflictStyle::Merge,
1469 marker_size: 7,
1470 diff_algorithm: None,
1471 ignore_all_space: false,
1472 ignore_space_change: false,
1473 ignore_space_at_eol: false,
1474 ignore_cr_at_eol: false,
1475 };
1476 let out = merge(&input).unwrap();
1477 assert_eq!(out.content, b"line1\nline2\nline3x\nline3y");
1479 }
1480
1481 #[test]
1482 fn zdiff3_interesting_conflict_shape() {
1483 let input = MergeInput {
1484 base: b"1\n2\n3\n4\n5\n6\n7\n8\n9\n",
1485 ours: b"1\n2\n3\n4\nA\nB\nC\nD\nE\nF\nG\nH\nI\nJ\n7\n8\n9\n",
1486 theirs: b"1\n2\n3\n4\nA\nB\nC\n5\n6\nG\nH\nI\nJ\n7\n8\n9\n",
1487 label_ours: "HEAD",
1488 label_base: "base",
1489 label_theirs: "right^0",
1490 favor: MergeFavor::None,
1491 style: ConflictStyle::ZealousDiff3,
1492 marker_size: 7,
1493 diff_algorithm: None,
1494 ignore_all_space: false,
1495 ignore_space_change: false,
1496 ignore_space_at_eol: false,
1497 ignore_cr_at_eol: false,
1498 };
1499 let out = merge(&input).unwrap();
1500 let rendered = String::from_utf8(out.content).unwrap();
1501 assert_eq!(out.conflicts, 1, "{rendered}");
1502 assert!(rendered.contains("<<<<<<< HEAD\nD\nE\nF\n"), "{rendered}");
1503 }
1504
1505 #[test]
1506 fn preserves_shared_and_superset_insertions() {
1507 let input = MergeInput {
1508 base: b"1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n",
1509 ours: b"1\n2\n3\n4\n5\n5.5\n6\n7\n8\n9\n10\n",
1510 theirs: b"1\n2\n3\n4\n5\n5.5\n6\n7\n8\n9\n10\n10.5\n",
1511 label_ours: "ours",
1512 label_base: "base",
1513 label_theirs: "theirs",
1514 favor: MergeFavor::None,
1515 style: ConflictStyle::Merge,
1516 marker_size: 7,
1517 diff_algorithm: None,
1518 ignore_all_space: false,
1519 ignore_space_change: false,
1520 ignore_space_at_eol: false,
1521 ignore_cr_at_eol: false,
1522 };
1523 let base_lines = split_lines(input.base);
1524 let ours_lines = split_lines(input.ours);
1525 let theirs_lines = split_lines(input.theirs);
1526 let ws_mode = WhitespaceMode::default();
1527 let base_compare_lines = normalize_lines_for_compare(&base_lines, &ws_mode);
1528 let ours_compare_lines = normalize_lines_for_compare(&ours_lines, &ws_mode);
1529 let theirs_compare_lines = normalize_lines_for_compare(&theirs_lines, &ws_mode);
1530 let ours_ops = similar::capture_diff_slices(
1531 Algorithm::Myers,
1532 &base_compare_lines,
1533 &ours_compare_lines,
1534 );
1535 let theirs_ops = similar::capture_diff_slices(
1536 Algorithm::Myers,
1537 &base_compare_lines,
1538 &theirs_compare_lines,
1539 );
1540 let hunks = compute_hunks(
1541 &base_lines,
1542 &ours_lines,
1543 &theirs_lines,
1544 &ours_ops,
1545 &theirs_ops,
1546 &ws_mode,
1547 );
1548 assert_eq!(
1549 hunks.len(),
1550 4,
1551 "expected unchanged shared insertion and theirs-only tail insertion"
1552 );
1553 let out = merge(&input).unwrap();
1554 let rendered = String::from_utf8(out.content).unwrap();
1555 assert_eq!(out.conflicts, 0, "{rendered}");
1556 assert_eq!(
1557 rendered, "1\n2\n3\n4\n5\n5.5\n6\n7\n8\n9\n10\n10.5\n",
1558 "{rendered}"
1559 );
1560 }
1561}