1use crate::error::Result;
20use similar::{Algorithm, DiffOp, DiffTag};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
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 = fold_adjacent_inserts_into_conflict(hunks);
132 hunks = merge_adjacent_one_sided_line_changes_to_conflict(hunks);
133 }
134 if !matches!(
136 input.style,
137 ConflictStyle::Diff3 | ConflictStyle::ZealousDiff3
138 ) {
139 hunks = refine_conflicts_by_subdiff(hunks, algo, &ws_mode);
140 hunks = simplify_non_conflicts_between_conflicts(hunks, true);
141 }
142 hunks = coalesce_nearby_conflicts(hunks, 3, false);
145 if matches!(input.style, ConflictStyle::ZealousDiff3) {
146 hunks = adjust_zealous_hunks(hunks);
147 }
148
149 let marker = if input.marker_size == 0 {
150 7
151 } else {
152 input.marker_size
153 };
154
155 let mut content: Vec<u8> = Vec::new();
156 let mut conflicts = 0usize;
157 let mut ours_line_pos = 0usize;
158 let mut theirs_line_pos = 0usize;
159
160 for (idx, hunk) in hunks.iter().enumerate() {
161 match hunk {
162 Hunk::Unchanged(lines) => {
163 append_lines(&mut content, lines);
164 ours_line_pos += lines.len();
165 theirs_line_pos += lines.len();
166 }
167 Hunk::OnlyOurs { ours, .. } => {
168 append_lines(&mut content, ours);
169 ours_line_pos += ours.len();
170 }
171 Hunk::OnlyTheirs { theirs, .. } => {
172 append_lines(&mut content, theirs);
173 theirs_line_pos += theirs.len();
174 }
175 Hunk::Conflict { base, ours, theirs } => {
176 let conflict_ours_start = ours_line_pos;
177 let conflict_theirs_start = theirs_line_pos;
178 match input.favor {
179 MergeFavor::Ours => {
180 append_lines(&mut content, ours);
181 ours_line_pos += ours.len();
182 }
183 MergeFavor::Theirs => {
184 append_lines(&mut content, theirs);
185 theirs_line_pos += theirs.len();
186 }
187 MergeFavor::Union => {
188 append_lines(&mut content, ours);
189 if !theirs.is_empty()
193 && !ours.is_empty()
194 && !ours.last().map(|l| l.ends_with(b"\n")).unwrap_or(false)
195 {
196 content.push(b'\n');
197 }
198 append_lines(&mut content, theirs);
199 ours_line_pos += ours.len();
200 theirs_line_pos += theirs.len();
201 }
202 MergeFavor::None => {
203 conflicts += 1;
204 if matches!(input.style, ConflictStyle::ZealousDiff3) {
205 let (mut prefix_len, mut suffix_len) =
206 common_prefix_suffix(ours, theirs);
207
208 if prefix_len > 0
209 && idx > 0
210 && hunk_output_lines(&hunks[idx - 1])
211 .map(|lines| lines_end_with(lines, &ours[..prefix_len]))
212 .unwrap_or(false)
213 {
214 prefix_len = 0;
215 }
216
217 if suffix_len > 0
218 && idx + 1 < hunks.len()
219 && hunk_output_lines(&hunks[idx + 1])
220 .map(|lines| {
221 lines_start_with(lines, &ours[ours.len() - suffix_len..])
222 })
223 .unwrap_or(false)
224 {
225 suffix_len = 0;
226 }
227
228 if prefix_len > 0 {
229 append_lines(&mut content, &ours[..prefix_len]);
230 }
231 let i1 = conflict_ours_start + prefix_len;
232 let i2 = conflict_theirs_start + prefix_len;
233 let needs_cr = conflict_markers_need_cr_at(
234 &ours_lines,
235 &theirs_lines,
236 &base_lines,
237 i1,
238 i2,
239 );
240 emit_conflict(
241 &mut content,
242 base,
243 &ours[prefix_len..ours.len() - suffix_len],
244 &theirs[prefix_len..theirs.len() - suffix_len],
245 input.label_ours,
246 input.label_base,
247 input.label_theirs,
248 input.style,
249 marker,
250 needs_cr,
251 );
252 if suffix_len > 0 {
253 append_lines(&mut content, &ours[ours.len() - suffix_len..]);
254 }
255 } else if matches!(input.style, ConflictStyle::Merge) {
256 let (prefix_len, suffix_len) =
257 common_prefix_suffix_merge_lines(ours, theirs, &ws_mode);
258 let pre = &ours[..prefix_len];
259 let suf_start = ours.len().saturating_sub(suffix_len);
260 let o_mid = &ours[prefix_len..suf_start];
261 let t_mid =
262 &theirs[prefix_len..theirs.len().saturating_sub(suffix_len)];
263 let suf = &ours[suf_start..];
264 append_lines(&mut content, pre);
265 let i1 = conflict_ours_start + prefix_len;
266 let i2 = conflict_theirs_start + prefix_len;
267 let needs_cr = conflict_markers_need_cr_at(
268 &ours_lines,
269 &theirs_lines,
270 &base_lines,
271 i1,
272 i2,
273 );
274 emit_conflict(
275 &mut content,
276 base,
277 o_mid,
278 t_mid,
279 input.label_ours,
280 input.label_base,
281 input.label_theirs,
282 input.style,
283 marker,
284 needs_cr,
285 );
286 append_lines(&mut content, suf);
287 } else {
288 let needs_cr = conflict_markers_need_cr_at(
289 &ours_lines,
290 &theirs_lines,
291 &base_lines,
292 conflict_ours_start,
293 conflict_theirs_start,
294 );
295 emit_conflict(
296 &mut content,
297 base,
298 ours,
299 theirs,
300 input.label_ours,
301 input.label_base,
302 input.label_theirs,
303 input.style,
304 marker,
305 needs_cr,
306 );
307 }
308 ours_line_pos += ours.len();
309 theirs_line_pos += theirs.len();
310 }
311 }
312 }
313 }
314 }
315
316 Ok(MergeOutput { content, conflicts })
317}
318
319fn append_lines(out: &mut Vec<u8>, lines: &[Vec<u8>]) {
320 if !out.is_empty() && !out.ends_with(b"\n") && !lines.is_empty() {
323 out.push(b'\n');
324 }
325 for line in lines {
326 out.extend_from_slice(line);
327 }
328}
329
330fn common_prefix_suffix(ours: &[Vec<u8>], theirs: &[Vec<u8>]) -> (usize, usize) {
331 let mut prefix = 0usize;
332 while prefix < ours.len() && prefix < theirs.len() && ours[prefix] == theirs[prefix] {
333 prefix += 1;
334 }
335
336 let mut suffix = 0usize;
337 while suffix < ours.len().saturating_sub(prefix)
338 && suffix < theirs.len().saturating_sub(prefix)
339 && ours[ours.len() - 1 - suffix] == theirs[theirs.len() - 1 - suffix]
340 {
341 suffix += 1;
342 }
343
344 (prefix, suffix)
345}
346
347fn hunk_output_lines(hunk: &Hunk) -> Option<&[Vec<u8>]> {
348 match hunk {
349 Hunk::Unchanged(lines) => Some(lines),
350 Hunk::OnlyOurs { ours, .. } => Some(ours),
351 Hunk::OnlyTheirs { theirs, .. } => Some(theirs),
352 Hunk::Conflict { .. } => None,
353 }
354}
355
356fn lines_end_with(lines: &[Vec<u8>], suffix: &[Vec<u8>]) -> bool {
357 if suffix.is_empty() || suffix.len() > lines.len() {
358 return false;
359 }
360 lines[lines.len() - suffix.len()..] == *suffix
361}
362
363fn lines_start_with(lines: &[Vec<u8>], prefix: &[Vec<u8>]) -> bool {
364 if prefix.is_empty() || prefix.len() > lines.len() {
365 return false;
366 }
367 lines[..prefix.len()] == *prefix
368}
369
370#[allow(clippy::too_many_arguments)]
372fn emit_conflict(
373 out: &mut Vec<u8>,
374 base: &[Vec<u8>],
375 ours: &[Vec<u8>],
376 theirs: &[Vec<u8>],
377 label_ours: &str,
378 label_base: &str,
379 label_theirs: &str,
380 style: ConflictStyle,
381 marker: usize,
382 needs_cr: bool,
383) {
384 let open = "<".repeat(marker);
385 let eq = "=".repeat(marker);
386 let close = ">".repeat(marker);
387 let marker_eol: &[u8] = if needs_cr { b"\r\n" } else { b"\n" };
388
389 if !out.is_empty() && !out.ends_with(b"\n") && !out.ends_with(b"\r\n") {
392 out.extend_from_slice(marker_eol);
393 }
394
395 write_conflict_marker_line(out, &open, Some(label_ours), marker_eol);
396 for line in ours {
397 out.extend_from_slice(line);
398 }
399 if out.last().copied() != Some(b'\n') {
401 out.extend_from_slice(marker_eol);
402 }
403 match style {
404 ConflictStyle::Diff3 | ConflictStyle::ZealousDiff3 => {
405 let pipe = "|".repeat(marker);
406 write_conflict_marker_line(out, &pipe, Some(label_base), marker_eol);
407 for line in base {
408 out.extend_from_slice(line);
409 }
410 if out.last().copied() != Some(b'\n') {
411 out.extend_from_slice(marker_eol);
412 }
413 write_conflict_marker_line(out, &eq, None, marker_eol);
414 }
415 ConflictStyle::Merge => {
416 write_conflict_marker_line(out, &eq, None, marker_eol);
417 }
418 }
419 for line in theirs {
420 out.extend_from_slice(line);
421 }
422 if out.last().copied() != Some(b'\n') {
423 out.extend_from_slice(marker_eol);
424 }
425 write_conflict_marker_line(out, &close, Some(label_theirs), marker_eol);
426}
427
428fn write_conflict_marker_line(out: &mut Vec<u8>, marker: &str, label: Option<&str>, eol: &[u8]) {
429 out.extend_from_slice(marker.as_bytes());
430 let open_or_close = marker.starts_with('<') || marker.starts_with('>');
433 let has_label = label.is_some_and(|l| !l.is_empty());
434 if open_or_close || has_label {
437 out.push(b' ');
438 }
439 if let Some(label) = label {
440 if !label.is_empty() {
441 out.extend_from_slice(label.as_bytes());
442 }
443 }
444 out.extend_from_slice(eol);
445}
446
447fn strip_line_terminator(line: &[u8]) -> &[u8] {
449 if line.ends_with(b"\r\n") {
450 return &line[..line.len() - 2];
451 }
452 if line.ends_with(b"\n") {
453 return &line[..line.len() - 1];
454 }
455 if line.ends_with(b"\r") {
456 return &line[..line.len() - 1];
457 }
458 line
459}
460
461fn line_content_for_merge_compare(line: &[u8], mode: &WhitespaceMode) -> Vec<u8> {
464 let (body, from_crlf_record) = if line.ends_with(b"\r\n") {
465 (&line[..line.len() - 1], true)
466 } else if line.ends_with(b"\n") {
467 (&line[..line.len() - 1], false)
468 } else {
469 (line, false)
471 };
472 let mut bytes = body.to_vec();
473 if mode.ignore_cr_at_eol && from_crlf_record && bytes.last() == Some(&b'\r') {
474 bytes.pop();
475 }
476 normalize_line_for_compare(&bytes, mode)
477}
478
479fn merge_line_equal(a: &[u8], b: &[u8], mode: &WhitespaceMode) -> bool {
481 line_content_for_merge_compare(a, mode) == line_content_for_merge_compare(b, mode)
482}
483
484fn merge_lines_equal(a: &[Vec<u8>], b: &[Vec<u8>], mode: &WhitespaceMode) -> bool {
488 a.len() == b.len()
489 && a.iter()
490 .zip(b.iter())
491 .all(|(x, y)| merge_line_equal(x, y, mode))
492}
493
494fn common_prefix_suffix_merge_lines(
495 ours: &[Vec<u8>],
496 theirs: &[Vec<u8>],
497 mode: &WhitespaceMode,
498) -> (usize, usize) {
499 let mut prefix = 0usize;
500 while prefix < ours.len()
501 && prefix < theirs.len()
502 && merge_line_equal(&ours[prefix], &theirs[prefix], mode)
503 {
504 prefix += 1;
505 }
506
507 let mut suffix = 0usize;
508 while suffix < ours.len().saturating_sub(prefix)
509 && suffix < theirs.len().saturating_sub(prefix)
510 && merge_line_equal(
511 &ours[ours.len() - 1 - suffix],
512 &theirs[theirs.len() - 1 - suffix],
513 mode,
514 )
515 {
516 suffix += 1;
517 }
518
519 (prefix, suffix)
520}
521
522fn is_eol_crlf_line(line: &[u8]) -> i8 {
524 if line.ends_with(b"\r\n") {
525 return 1;
526 }
527 if line.ends_with(b"\n") {
528 return 0;
529 }
530 -1
531}
532
533fn conflict_markers_need_cr_at(
535 ours_lines: &[Vec<u8>],
536 theirs_lines: &[Vec<u8>],
537 base_lines: &[Vec<u8>],
538 i1: usize,
539 i2: usize,
540) -> bool {
541 let mut needs = if i1 > 0 {
542 is_eol_crlf_line(ours_lines[i1 - 1].as_slice())
543 } else {
544 -1
545 };
546 if needs > 0 {
547 let t0 = if i2 > 0 {
548 is_eol_crlf_line(theirs_lines[i2 - 1].as_slice())
549 } else {
550 -1
551 };
552 if t0 >= 0 {
553 needs = t0;
554 }
555 }
556 if needs > 0 && !base_lines.is_empty() {
557 needs = is_eol_crlf_line(base_lines[0].as_slice());
558 }
559 needs > 0
560}
561
562fn line_contains_alnum(line: &[u8]) -> bool {
563 strip_line_terminator(line)
564 .iter()
565 .any(|b| b.is_ascii_alphanumeric())
566}
567
568fn gap_blocks_merge_between_conflicts(gap: &[Vec<u8>], simplify_if_no_alnum: bool) -> bool {
573 if gap.len() <= 3 {
574 return false;
575 }
576 !simplify_if_no_alnum || gap.iter().any(|l| line_contains_alnum(l.as_slice()))
577}
578
579fn merge_two_adjacent_conflicts(left: &Hunk, gap: &[Vec<u8>], right: &Hunk) -> Option<Hunk> {
580 let Hunk::Conflict {
581 base: b1,
582 ours: o1,
583 theirs: t1,
584 } = left
585 else {
586 return None;
587 };
588 let Hunk::Conflict {
589 base: b2,
590 ours: o2,
591 theirs: t2,
592 } = right
593 else {
594 return None;
595 };
596 let mut merged_base = b1.clone();
599 merged_base.extend(gap.iter().cloned());
600 merged_base.extend(b2.iter().cloned());
601 let mut merged_ours = o1.clone();
602 merged_ours.extend(gap.iter().cloned());
603 merged_ours.extend(o2.iter().cloned());
604 let mut merged_theirs = t1.clone();
605 merged_theirs.extend(gap.iter().cloned());
606 merged_theirs.extend(t2.iter().cloned());
607 Some(Hunk::Conflict {
608 base: merged_base,
609 ours: merged_ours,
610 theirs: merged_theirs,
611 })
612}
613
614fn simplify_non_conflicts_between_conflicts(
616 hunks: Vec<Hunk>,
617 simplify_if_no_alnum: bool,
618) -> Vec<Hunk> {
619 let mut out: Vec<Hunk> = Vec::with_capacity(hunks.len());
620 let mut i = 0usize;
621 while i < hunks.len() {
622 let Hunk::Conflict { .. } = &hunks[i] else {
623 out.push(hunks[i].clone());
624 i += 1;
625 continue;
626 };
627 let mut merged = hunks[i].clone();
628 let mut j = i;
629 while let (Some(Hunk::Unchanged(gap)), Some(next @ Hunk::Conflict { .. })) =
630 (hunks.get(j + 1), hunks.get(j + 2))
631 {
632 if gap_blocks_merge_between_conflicts(gap, simplify_if_no_alnum) {
633 break;
634 }
635 let Some(m) = merge_two_adjacent_conflicts(&merged, gap, next) else {
636 break;
637 };
638 merged = m;
639 j += 2;
640 }
641 out.push(merged);
642 i = j + 1;
643 }
644 out
645}
646
647fn refine_conflicts_by_subdiff(
649 hunks: Vec<Hunk>,
650 algo: Algorithm,
651 ws_mode: &WhitespaceMode,
652) -> Vec<Hunk> {
653 let mut out = Vec::with_capacity(hunks.len());
654 for h in hunks {
655 let Hunk::Conflict { base, ours, theirs } = h else {
656 out.push(h);
657 continue;
658 };
659 if ours.is_empty() || theirs.is_empty() {
660 out.push(Hunk::Conflict { base, ours, theirs });
661 continue;
662 }
663 let o_cmp = normalize_lines_for_compare(&ours, ws_mode);
664 let t_cmp = normalize_lines_for_compare(&theirs, ws_mode);
665 let sub_ops = similar::capture_diff_slices(algo, &o_cmp, &t_cmp);
666 if sub_ops.len() == 1 && sub_ops[0].tag() == DiffTag::Equal {
667 out.push(Hunk::Unchanged(ours));
668 continue;
669 }
670 for op in &sub_ops {
671 let old = op.old_range();
672 let new_ = op.new_range();
673 match op.tag() {
674 DiffTag::Equal => {
675 if !old.is_empty() {
676 out.push(Hunk::Unchanged(ours[old.start..old.end].to_vec()));
677 }
678 }
679 DiffTag::Delete => {
680 if !old.is_empty() {
684 out.push(Hunk::Conflict {
685 base: Vec::new(),
686 ours: ours[old.start..old.end].to_vec(),
687 theirs: Vec::new(),
688 });
689 }
690 }
691 DiffTag::Insert => {
692 if !new_.is_empty() {
693 out.push(Hunk::Conflict {
694 base: Vec::new(),
695 ours: Vec::new(),
696 theirs: theirs[new_.start..new_.end].to_vec(),
697 });
698 }
699 }
700 DiffTag::Replace => {
701 out.push(Hunk::Conflict {
702 base: Vec::new(),
703 ours: ours[old.start..old.end].to_vec(),
704 theirs: theirs[new_.start..new_.end].to_vec(),
705 });
706 }
707 }
708 }
709 }
710 out
711}
712
713#[derive(Debug, Clone)]
715enum Hunk {
716 Unchanged(Vec<Vec<u8>>),
718 OnlyOurs {
720 base: Vec<Vec<u8>>,
722 ours: Vec<Vec<u8>>,
724 },
725 OnlyTheirs {
727 base: Vec<Vec<u8>>,
729 theirs: Vec<Vec<u8>>,
731 },
732 Conflict {
734 base: Vec<Vec<u8>>,
735 ours: Vec<Vec<u8>>,
736 theirs: Vec<Vec<u8>>,
737 },
738}
739
740fn compute_hunks(
745 base: &[Vec<u8>],
746 ours: &[Vec<u8>],
747 theirs: &[Vec<u8>],
748 ours_ops: &[DiffOp],
749 theirs_ops: &[DiffOp],
750 ws_mode: &WhitespaceMode,
751) -> Vec<Hunk> {
752 let ours_changed = changed_mask(ours_ops, base.len());
753 let theirs_changed = changed_mask(theirs_ops, base.len());
754
755 let ours_inserts = collect_inserts(ours_ops, base.len());
756 let theirs_inserts = collect_inserts(theirs_ops, base.len());
757
758 let mut hunks: Vec<Hunk> = Vec::new();
759 let mut pos = 0usize;
760
761 while pos <= base.len() {
762 emit_inserts_at(
764 pos,
765 &ours_inserts,
766 &theirs_inserts,
767 ours,
768 theirs,
769 ws_mode,
770 &mut hunks,
771 );
772
773 if pos == base.len() {
774 break;
775 }
776
777 let o = ours_changed[pos];
778 let t = theirs_changed[pos];
779
780 if !o && !t {
781 let mut end = pos + 1;
785 while end < base.len()
786 && !ours_changed[end]
787 && !theirs_changed[end]
788 && ours_inserts[end].is_empty()
789 && theirs_inserts[end].is_empty()
790 {
791 end += 1;
792 }
793 let terminator_only_diff = base.len() == ours.len()
794 && (pos..end).all(|i| merge_line_equal(&base[i], &ours[i], ws_mode))
795 && (pos..end).any(|i| base[i] != ours[i]);
796 let unchanged_lines = if ws_mode.ignore_all_space
797 || ws_mode.ignore_space_change
798 || ws_mode.ignore_space_at_eol
799 || ws_mode.ignore_cr_at_eol
800 || terminator_only_diff
801 {
802 &ours[pos..end]
803 } else {
804 &base[pos..end]
805 };
806 hunks.push(Hunk::Unchanged(unchanged_lines.to_vec()));
807 pos = end;
808 continue;
809 }
810
811 let mut end = pos + 1;
821 loop {
822 let mut new_end = furthest_changed_op_end(ours_ops, pos, end)
823 .max(furthest_changed_op_end(theirs_ops, pos, end));
824 while new_end < base.len() && (ours_changed[new_end] || theirs_changed[new_end]) {
825 new_end += 1;
826 }
827 if new_end <= end {
828 break;
829 }
830 end = new_end;
831 }
832
833 let any_ours = (pos..end).any(|p| ours_changed[p]);
835 let any_theirs = (pos..end).any(|p| theirs_changed[p]);
836
837 match (any_ours, any_theirs) {
838 (true, false) => {
839 let c = collect_new_lines(ours_ops, ours, pos, end);
840 hunks.push(Hunk::OnlyOurs {
841 base: base[pos..end].to_vec(),
842 ours: c,
843 });
844 }
845 (false, true) => {
846 let c = collect_new_lines(theirs_ops, theirs, pos, end);
847 hunks.push(Hunk::OnlyTheirs {
848 base: base[pos..end].to_vec(),
849 theirs: c,
850 });
851 }
852 (true, true) => {
853 let o = collect_new_lines(ours_ops, ours, pos, end);
854 let t = collect_new_lines(theirs_ops, theirs, pos, end);
855 if merge_lines_equal(&o, &t, ws_mode) {
861 hunks.push(Hunk::OnlyOurs {
862 base: base[pos..end].to_vec(),
863 ours: o,
864 });
865 } else {
866 hunks.push(Hunk::Conflict {
867 base: base[pos..end].to_vec(),
868 ours: o,
869 theirs: t,
870 });
871 }
872 }
873 (false, false) => {
874 hunks.push(Hunk::Unchanged(base[pos..end].to_vec()));
876 }
877 }
878
879 pos = end;
880 }
881
882 hunks
883}
884
885fn changed_mask(ops: &[DiffOp], base_len: usize) -> Vec<bool> {
888 let mut mask = vec![false; base_len];
889 for op in ops {
890 if op.tag() == DiffTag::Equal {
891 continue;
892 }
893 for p in op.old_range() {
894 if p < base_len {
895 mask[p] = true;
896 }
897 }
898 }
899 mask
900}
901
902fn collect_inserts(ops: &[DiffOp], base_len: usize) -> Vec<Vec<(usize, usize)>> {
907 let mut result: Vec<Vec<(usize, usize)>> = vec![Vec::new(); base_len + 1];
908 for op in ops {
909 let old = op.old_range();
910 let new_ = op.new_range();
911 if old.is_empty() && !new_.is_empty() {
912 let pos = old.start.min(base_len);
913 result[pos].push((new_.start, new_.end));
914 }
915 }
916 result
917}
918
919fn emit_inserts_at(
921 pos: usize,
922 ours_inserts: &[Vec<(usize, usize)>],
923 theirs_inserts: &[Vec<(usize, usize)>],
924 ours: &[Vec<u8>],
925 theirs: &[Vec<u8>],
926 ws_mode: &WhitespaceMode,
927 hunks: &mut Vec<Hunk>,
928) {
929 let o_ins = &ours_inserts[pos];
930 let t_ins = &theirs_inserts[pos];
931
932 let has_ours = !o_ins.is_empty();
933 let has_theirs = !t_ins.is_empty();
934
935 if has_ours && has_theirs {
936 let o_lines: Vec<Vec<u8>> = o_ins
945 .iter()
946 .flat_map(|&(s, e)| ours[s..e].to_vec())
947 .collect();
948 let t_lines: Vec<Vec<u8>> = t_ins
949 .iter()
950 .flat_map(|&(s, e)| theirs[s..e].to_vec())
951 .collect();
952
953 let mut common_prefix = 0usize;
954 while common_prefix < o_lines.len()
955 && common_prefix < t_lines.len()
956 && o_lines[common_prefix] == t_lines[common_prefix]
957 {
958 common_prefix += 1;
959 }
960
961 let mut common_suffix = 0usize;
962 while common_suffix < o_lines.len() - common_prefix
963 && common_suffix < t_lines.len() - common_prefix
964 && o_lines[o_lines.len() - 1 - common_suffix]
965 == t_lines[t_lines.len() - 1 - common_suffix]
966 {
967 common_suffix += 1;
968 }
969
970 if common_prefix > 0 {
971 hunks.push(Hunk::Unchanged(o_lines[..common_prefix].to_vec()));
972 }
973
974 let ours_tail = o_lines[common_prefix..o_lines.len() - common_suffix].to_vec();
975 let theirs_tail = t_lines[common_prefix..t_lines.len() - common_suffix].to_vec();
976
977 if !ours_tail.is_empty() || !theirs_tail.is_empty() {
978 if merge_lines_equal(&ours_tail, &theirs_tail, ws_mode) {
981 hunks.push(Hunk::OnlyOurs {
982 base: Vec::new(),
983 ours: ours_tail,
984 });
985 } else {
986 hunks.push(Hunk::Conflict {
987 base: Vec::new(),
988 ours: ours_tail,
989 theirs: theirs_tail,
990 });
991 }
992 }
993
994 if common_suffix > 0 {
995 hunks.push(Hunk::Unchanged(
996 o_lines[o_lines.len() - common_suffix..].to_vec(),
997 ));
998 }
999 } else if has_ours {
1000 for &(ns, ne) in o_ins {
1001 let lines: Vec<Vec<u8>> = ours[ns..ne].to_vec();
1002 if !lines.is_empty() {
1003 hunks.push(Hunk::OnlyOurs {
1004 base: Vec::new(),
1005 ours: lines,
1006 });
1007 }
1008 }
1009 } else if has_theirs {
1010 for &(ns, ne) in t_ins {
1011 let lines: Vec<Vec<u8>> = theirs[ns..ne].to_vec();
1012 if !lines.is_empty() {
1013 hunks.push(Hunk::OnlyTheirs {
1014 base: Vec::new(),
1015 theirs: lines,
1016 });
1017 }
1018 }
1019 }
1020}
1021
1022fn adjust_zealous_hunks(hunks: Vec<Hunk>) -> Vec<Hunk> {
1023 let mut out: Vec<Hunk> = Vec::new();
1024 let mut i = 0usize;
1025
1026 while i < hunks.len() {
1027 let mut consumed = 1usize;
1028 let mut transformed: Option<Vec<Hunk>> = None;
1029
1030 let (pre_insert, mid_idx) = match &hunks[i] {
1031 Hunk::OnlyTheirs { base, theirs } if base.is_empty() => {
1032 (Some(theirs.as_slice()), i + 1)
1033 }
1034 _ => (None, i),
1035 };
1036
1037 if let Some(Hunk::OnlyOurs { base, ours }) = hunks.get(mid_idx) {
1038 if !base.is_empty() {
1039 let post_insert = match hunks.get(mid_idx + 1) {
1040 Some(Hunk::OnlyTheirs { base, theirs }) if base.is_empty() => {
1041 Some(theirs.as_slice())
1042 }
1043 _ => None,
1044 };
1045
1046 let mut prefix_len = 0usize;
1047 if let Some(pre) = pre_insert {
1048 if !pre.is_empty() && ours.starts_with(pre) {
1049 prefix_len = pre.len();
1050 }
1051 }
1052
1053 let mut suffix_len = 0usize;
1054 if let Some(post) = post_insert {
1055 if !post.is_empty() && ours[prefix_len..].ends_with(post) {
1056 suffix_len = post.len();
1057 }
1058 }
1059
1060 if prefix_len > 0 || suffix_len > 0 {
1061 consumed = if pre_insert.is_some() {
1062 if post_insert.is_some() {
1063 3
1064 } else {
1065 2
1066 }
1067 } else if post_insert.is_some() {
1068 2
1069 } else {
1070 1
1071 };
1072
1073 let mut replacement: Vec<Hunk> = Vec::new();
1074 if prefix_len > 0 {
1075 replacement.push(Hunk::Unchanged(ours[..prefix_len].to_vec()));
1076 }
1077 replacement.push(Hunk::Conflict {
1078 base: base.clone(),
1079 ours: ours[prefix_len..ours.len() - suffix_len].to_vec(),
1080 theirs: base.clone(),
1081 });
1082 if suffix_len > 0 {
1083 replacement.push(Hunk::Unchanged(ours[ours.len() - suffix_len..].to_vec()));
1084 }
1085 transformed = Some(replacement);
1086 }
1087 }
1088 }
1089
1090 if let Some(replacement) = transformed {
1091 for h in replacement {
1092 push_hunk_with_unchanged_merge(&mut out, h);
1093 }
1094 i += consumed;
1095 continue;
1096 }
1097
1098 push_hunk_with_unchanged_merge(&mut out, hunks[i].clone());
1099 i += 1;
1100 }
1101
1102 out
1103}
1104
1105fn merge_adjacent_one_sided_line_changes_to_conflict(hunks: Vec<Hunk>) -> Vec<Hunk> {
1112 let mut out: Vec<Hunk> = Vec::with_capacity(hunks.len());
1113 let mut i = 0usize;
1114 while i < hunks.len() {
1115 let merged = match (&hunks[i], hunks.get(i + 1)) {
1116 (
1117 Hunk::OnlyTheirs {
1118 base: b1,
1119 theirs: t1,
1120 },
1121 Some(Hunk::OnlyOurs { base: b2, ours: o2 }),
1122 ) if b1.len() == 1 && b2.len() == 1 && t1.len() == 1 && o2.len() == 1 => {
1123 Some(Hunk::Conflict {
1124 base: vec![b1[0].clone(), b2[0].clone()],
1125 ours: vec![b1[0].clone(), o2[0].clone()],
1126 theirs: vec![t1[0].clone(), b2[0].clone()],
1127 })
1128 }
1129 (
1130 Hunk::OnlyOurs { base: b1, ours: o1 },
1131 Some(Hunk::OnlyTheirs {
1132 base: b2,
1133 theirs: t2,
1134 }),
1135 ) if b1.len() == 1 && b2.len() == 1 && o1.len() == 1 && t2.len() == 1 => {
1136 Some(Hunk::Conflict {
1137 base: vec![b1[0].clone(), b2[0].clone()],
1138 ours: vec![o1[0].clone(), b2[0].clone()],
1139 theirs: vec![b1[0].clone(), t2[0].clone()],
1140 })
1141 }
1142 _ => None,
1143 };
1144 if let Some(h) = merged {
1145 out.push(h);
1146 i += 2;
1147 } else {
1148 out.push(hunks[i].clone());
1149 i += 1;
1150 }
1151 }
1152 out
1153}
1154
1155fn merge_adjacent_replace_and_trailing_insert_conflicts(hunks: Vec<Hunk>) -> Vec<Hunk> {
1156 let mut out: Vec<Hunk> = Vec::with_capacity(hunks.len());
1157 let mut i = 0usize;
1158 while i < hunks.len() {
1159 let merged = match (&hunks[i], hunks.get(i + 1)) {
1160 (Hunk::OnlyTheirs { base, theirs }, Some(Hunk::OnlyOurs { base: bo, ours: o }))
1161 if !base.is_empty() && bo.is_empty() && !o.is_empty() && !theirs.is_empty() =>
1162 {
1163 Some(Hunk::Conflict {
1164 base: base.clone(),
1165 ours: o.clone(),
1166 theirs: theirs.clone(),
1167 })
1168 }
1169 (
1170 Hunk::OnlyOurs { base, ours },
1171 Some(Hunk::OnlyTheirs {
1172 base: bt,
1173 theirs: t,
1174 }),
1175 ) if !base.is_empty() && bt.is_empty() && !t.is_empty() && !ours.is_empty() => {
1176 let mut conflict_theirs = base.clone();
1177 conflict_theirs.extend(t.iter().cloned());
1178 Some(Hunk::Conflict {
1179 base: base.clone(),
1180 ours: ours.clone(),
1181 theirs: conflict_theirs,
1182 })
1183 }
1184 (Hunk::OnlyOurs { base: bo, ours }, Some(Hunk::OnlyTheirs { base: bt, theirs }))
1190 if bo.is_empty() && !bt.is_empty() && !ours.is_empty() && !theirs.is_empty() =>
1191 {
1192 let mut conflict_ours = ours.clone();
1193 conflict_ours.extend(bt.iter().cloned());
1194 Some(Hunk::Conflict {
1195 base: bt.clone(),
1196 ours: conflict_ours,
1197 theirs: theirs.clone(),
1198 })
1199 }
1200 (Hunk::OnlyTheirs { base: bt, theirs }, Some(Hunk::OnlyOurs { base: bo, ours }))
1201 if bt.is_empty() && !bo.is_empty() && !theirs.is_empty() && !ours.is_empty() =>
1202 {
1203 let mut conflict_theirs = theirs.clone();
1204 conflict_theirs.extend(bo.iter().cloned());
1205 Some(Hunk::Conflict {
1206 base: bo.clone(),
1207 ours: ours.clone(),
1208 theirs: conflict_theirs,
1209 })
1210 }
1211 _ => None,
1212 };
1213 if let Some(h) = merged {
1214 out.push(h);
1215 i += 2;
1216 } else {
1217 out.push(hunks[i].clone());
1218 i += 1;
1219 }
1220 }
1221 out
1222}
1223
1224fn fold_adjacent_inserts_into_conflict(hunks: Vec<Hunk>) -> Vec<Hunk> {
1238 let mut out: Vec<Hunk> = Vec::with_capacity(hunks.len());
1239 for hunk in hunks {
1240 match hunk {
1241 Hunk::OnlyOurs { ref base, ref ours } if base.is_empty() => {
1244 if let Some(Hunk::Conflict { ours: c_ours, .. }) = out.last_mut() {
1245 c_ours.extend(ours.iter().cloned());
1246 continue;
1247 }
1248 out.push(hunk);
1249 }
1250 Hunk::OnlyTheirs {
1251 ref base,
1252 ref theirs,
1253 } if base.is_empty() => {
1254 if let Some(Hunk::Conflict {
1255 theirs: c_theirs, ..
1256 }) = out.last_mut()
1257 {
1258 c_theirs.extend(theirs.iter().cloned());
1259 continue;
1260 }
1261 out.push(hunk);
1262 }
1263 other => out.push(other),
1264 }
1265 }
1266 out
1267}
1268
1269fn coalesce_nearby_conflicts(hunks: Vec<Hunk>, max_gap_lines: usize, enable: bool) -> Vec<Hunk> {
1270 if !enable {
1271 return hunks;
1272 }
1273 let mut out: Vec<Hunk> = Vec::new();
1274 let mut i = 0usize;
1275
1276 while i < hunks.len() {
1277 let Some(Hunk::Conflict { base, ours, theirs }) = hunks.get(i) else {
1278 out.push(hunks[i].clone());
1279 i += 1;
1280 continue;
1281 };
1282
1283 let mut merged_base = base.clone();
1284 let mut merged_ours = ours.clone();
1285 let mut merged_theirs = theirs.clone();
1286 let mut j = i;
1287
1288 while let (
1289 Some(Hunk::Unchanged(gap)),
1290 Some(Hunk::Conflict {
1291 base: next_base,
1292 ours: next_ours,
1293 theirs: next_theirs,
1294 }),
1295 ) = (hunks.get(j + 1), hunks.get(j + 2))
1296 {
1297 if gap.len() > max_gap_lines {
1298 break;
1299 }
1300
1301 merged_base.extend(gap.iter().cloned());
1302 merged_base.extend(next_base.iter().cloned());
1303 merged_ours.extend(gap.iter().cloned());
1304 merged_ours.extend(next_ours.iter().cloned());
1305 merged_theirs.extend(gap.iter().cloned());
1306 merged_theirs.extend(next_theirs.iter().cloned());
1307 j += 2;
1308 }
1309
1310 out.push(Hunk::Conflict {
1311 base: merged_base,
1312 ours: merged_ours,
1313 theirs: merged_theirs,
1314 });
1315 i = j + 1;
1316 }
1317
1318 out
1319}
1320
1321fn push_hunk_with_unchanged_merge(out: &mut Vec<Hunk>, hunk: Hunk) {
1322 match hunk {
1323 Hunk::Unchanged(mut lines) => {
1324 if let Some(Hunk::Unchanged(prev)) = out.last_mut() {
1325 prev.append(&mut lines);
1326 } else if !lines.is_empty() {
1327 out.push(Hunk::Unchanged(lines));
1328 }
1329 }
1330 other => out.push(other),
1331 }
1332}
1333
1334fn furthest_changed_op_end(ops: &[DiffOp], run_start: usize, current_end: usize) -> usize {
1338 let mut max = current_end;
1339 for op in ops {
1340 if op.tag() == DiffTag::Equal {
1341 continue;
1342 }
1343 let old = op.old_range();
1344 if old.start < current_end && old.end > run_start && old.end > max {
1345 max = old.end;
1346 }
1347 }
1348 max
1349}
1350
1351fn collect_new_lines(
1357 ops: &[DiffOp],
1358 new: &[Vec<u8>],
1359 base_start: usize,
1360 base_end: usize,
1361) -> Vec<Vec<u8>> {
1362 let mut lines = Vec::new();
1363 for op in ops {
1364 let old = op.old_range();
1365 let new_ = op.new_range();
1366 if old.is_empty() {
1367 continue; }
1369 if old.end <= base_start || old.start >= base_end {
1370 continue; }
1372 if op.tag() == DiffTag::Equal {
1373 let overlap_start = base_start.max(old.start);
1374 let overlap_end = base_end.min(old.end);
1375 let offset = overlap_start - old.start;
1376 let len = overlap_end - overlap_start;
1377 for i in offset..offset + len {
1378 if new_.start + i < new_.end {
1379 lines.push(new[new_.start + i].clone());
1380 }
1381 }
1382 } else {
1383 for i in new_.clone() {
1384 lines.push(new[i].clone());
1385 }
1386 }
1387 }
1388 lines
1389}
1390
1391fn _diff_ops(old: &[Vec<u8>], new: &[Vec<u8>]) -> Vec<DiffOp> {
1393 similar::capture_diff_slices(Algorithm::Myers, old, new)
1394}
1395
1396fn split_lines(data: &[u8]) -> Vec<Vec<u8>> {
1398 if data.is_empty() {
1399 return Vec::new();
1400 }
1401 let mut lines = Vec::new();
1402 let mut start = 0;
1403 for i in 0..data.len() {
1404 if data[i] == b'\n' {
1405 lines.push(data[start..=i].to_vec());
1406 start = i + 1;
1407 }
1408 }
1409 if start < data.len() {
1410 lines.push(data[start..].to_vec());
1411 }
1412 lines
1413}
1414
1415#[derive(Clone, Copy, Debug, Default)]
1416struct WhitespaceMode {
1417 ignore_all_space: bool,
1418 ignore_space_change: bool,
1419 ignore_space_at_eol: bool,
1420 ignore_cr_at_eol: bool,
1421}
1422
1423fn normalize_lines_for_compare(lines: &[Vec<u8>], mode: &WhitespaceMode) -> Vec<Vec<u8>> {
1424 lines
1425 .iter()
1426 .map(|line| line_content_for_merge_compare(line, mode))
1427 .collect()
1428}
1429
1430fn lines_for_merge_diff_compare(lines: &[Vec<u8>], mode: &WhitespaceMode) -> Vec<Vec<u8>> {
1431 lines
1432 .iter()
1433 .map(|line| line_content_for_merge_compare(line, mode))
1434 .collect()
1435}
1436
1437fn normalize_line_for_compare(line: &[u8], mode: &WhitespaceMode) -> Vec<u8> {
1438 let mut bytes = line.to_vec();
1439
1440 if mode.ignore_all_space {
1441 return bytes
1442 .into_iter()
1443 .filter(|b| !b.is_ascii_whitespace())
1444 .collect();
1445 }
1446
1447 if mode.ignore_space_change {
1448 let mut out = Vec::with_capacity(bytes.len());
1449 let mut in_ws = false;
1450 for ch in bytes {
1451 if ch.is_ascii_whitespace() {
1452 if !in_ws {
1453 out.push(b' ');
1454 in_ws = true;
1455 }
1456 } else {
1457 out.push(ch);
1458 in_ws = false;
1459 }
1460 }
1461 while out.last().is_some_and(|b| b.is_ascii_whitespace()) {
1462 out.pop();
1463 }
1464 return out;
1465 }
1466
1467 if mode.ignore_space_at_eol {
1468 if bytes.last().copied() == Some(b'\n') {
1469 let mut body = bytes[..bytes.len() - 1].to_vec();
1470 while body.last().is_some_and(|b| b.is_ascii_whitespace()) {
1471 body.pop();
1472 }
1473 body.push(b'\n');
1474 bytes = body;
1475 } else {
1476 while bytes.last().is_some_and(|b| b.is_ascii_whitespace()) {
1477 bytes.pop();
1478 }
1479 }
1480 }
1481
1482 bytes
1483}
1484
1485#[must_use]
1489pub fn is_binary(data: &[u8]) -> bool {
1490 data.contains(&0u8)
1491}
1492
1493#[cfg(test)]
1494mod tests {
1495 use super::*;
1496
1497 fn do_merge(base: &str, ours: &str, theirs: &str) -> (String, usize) {
1498 let input = MergeInput {
1499 base: base.as_bytes(),
1500 ours: ours.as_bytes(),
1501 theirs: theirs.as_bytes(),
1502 label_ours: "ours",
1503 label_base: "base",
1504 label_theirs: "theirs",
1505 favor: MergeFavor::None,
1506 style: ConflictStyle::Merge,
1507 marker_size: 7,
1508 diff_algorithm: None,
1509 ignore_all_space: false,
1510 ignore_space_change: false,
1511 ignore_space_at_eol: false,
1512 ignore_cr_at_eol: false,
1513 };
1514 let out = merge(&input).unwrap();
1515 (String::from_utf8(out.content).unwrap(), out.conflicts)
1516 }
1517
1518 #[test]
1519 fn no_changes() {
1520 let t = "line1\nline2\nline3\n";
1521 let (r, c) = do_merge(t, t, t);
1522 assert_eq!(r, t);
1523 assert_eq!(c, 0);
1524 }
1525
1526 #[test]
1527 fn only_ours() {
1528 let (r, c) = do_merge("a\nb\nc\n", "a\nB\nc\n", "a\nb\nc\n");
1529 assert_eq!(r, "a\nB\nc\n");
1530 assert_eq!(c, 0);
1531 }
1532
1533 #[test]
1534 fn only_theirs() {
1535 let (r, c) = do_merge("a\nb\nc\n", "a\nb\nc\n", "a\nB\nc\n");
1536 assert_eq!(r, "a\nB\nc\n");
1537 assert_eq!(c, 0);
1538 }
1539
1540 #[test]
1541 fn conflict() {
1542 let (r, c) = do_merge("a\nb\nc\n", "a\nX\nc\n", "a\nY\nc\n");
1543 assert_eq!(c, 1);
1544 assert!(r.contains("<<<<<<< ours\nX\n=======\nY\n>>>>>>> theirs"));
1545 }
1546
1547 #[test]
1548 fn t4108_style_overlapping_edits_conflict() {
1549 let base = "1\n2\n3\n4\n5\n6\n7\n";
1550 let ours = "1\ntwo\n3\n4\n5\nsix\n7\n";
1551 let theirs = "1\n2\n3\n4\nfive\n6\n7\n";
1552 let (_r, c) = do_merge(base, ours, theirs);
1553 assert!(
1554 c >= 1,
1555 "expected at least one conflict region (Git merge reports CONFLICT for this shape)"
1556 );
1557 }
1558
1559 #[test]
1560 fn conflict_delete_vs_change() {
1561 let base = "a\nb\nc\n";
1563 let ours = "c\n"; let theirs = "A\nb\nc\n"; let (r, c) = do_merge(base, ours, theirs);
1566 assert_eq!(c, 1, "expected conflict, got: {r:?}");
1567 }
1568
1569 #[test]
1570 fn conflict_replace_vs_insert_after_same_line() {
1571 let base = "hello\n";
1573 let ours = "hello\nhello\n";
1574 let theirs = "remove-conflict\n";
1575 let (r, c) = do_merge(base, ours, theirs);
1576 assert_eq!(c, 1, "expected conflict, got: {r:?}");
1577 }
1578
1579 #[test]
1580 fn conflict_insert_before_modified_line() {
1581 let base = "d\n";
1586 let ours = "c\nd\n";
1587 let theirs = "e\n";
1588 let (r, c) = do_merge(base, ours, theirs);
1589 assert_eq!(c, 1, "expected conflict, got: {r:?}");
1590 assert!(r.contains("c\nd\n"), "ours side should keep c\\nd: {r}");
1592 assert!(r.contains("e\n"), "theirs side should be e: {r}");
1593 }
1594
1595 #[test]
1596 fn conflict_insert_before_modified_line_symmetric() {
1597 let base = "d\n";
1599 let ours = "e\n";
1600 let theirs = "c\nd\n";
1601 let (r, c) = do_merge(base, ours, theirs);
1602 assert_eq!(c, 1, "expected conflict, got: {r:?}");
1603 }
1604
1605 #[test]
1606 fn clean_insert_separated_from_modified_line() {
1607 let base = "a\nb\nc\n";
1610 let ours = "x\na\nb\nc\n"; let theirs = "a\nb\nC\n"; let (r, c) = do_merge(base, ours, theirs);
1613 assert_eq!(c, 0, "expected clean merge, got: {r:?}");
1614 }
1615
1616 #[test]
1617 fn favor_ours() {
1618 let input = MergeInput {
1619 base: b"a\nb\nc\n",
1620 ours: b"a\nX\nc\n",
1621 theirs: b"a\nY\nc\n",
1622 label_ours: "o",
1623 label_base: "b",
1624 label_theirs: "t",
1625 favor: MergeFavor::Ours,
1626 style: ConflictStyle::Merge,
1627 marker_size: 7,
1628 diff_algorithm: None,
1629 ignore_all_space: false,
1630 ignore_space_change: false,
1631 ignore_space_at_eol: false,
1632 ignore_cr_at_eol: false,
1633 };
1634 let out = merge(&input).unwrap();
1635 assert_eq!(out.content, b"a\nX\nc\n");
1636 assert_eq!(out.conflicts, 0);
1637 }
1638
1639 #[test]
1640 fn union_missing_lf() {
1641 let input = MergeInput {
1642 base: b"line1\nline2\nline3",
1643 ours: b"line1\nline2\nline3x",
1644 theirs: b"line1\nline2\nline3y",
1645 label_ours: "o",
1646 label_base: "b",
1647 label_theirs: "t",
1648 favor: MergeFavor::Union,
1649 style: ConflictStyle::Merge,
1650 marker_size: 7,
1651 diff_algorithm: None,
1652 ignore_all_space: false,
1653 ignore_space_change: false,
1654 ignore_space_at_eol: false,
1655 ignore_cr_at_eol: false,
1656 };
1657 let out = merge(&input).unwrap();
1658 assert_eq!(out.content, b"line1\nline2\nline3x\nline3y");
1660 }
1661
1662 #[test]
1663 fn zdiff3_interesting_conflict_shape() {
1664 let input = MergeInput {
1665 base: b"1\n2\n3\n4\n5\n6\n7\n8\n9\n",
1666 ours: b"1\n2\n3\n4\nA\nB\nC\nD\nE\nF\nG\nH\nI\nJ\n7\n8\n9\n",
1667 theirs: b"1\n2\n3\n4\nA\nB\nC\n5\n6\nG\nH\nI\nJ\n7\n8\n9\n",
1668 label_ours: "HEAD",
1669 label_base: "base",
1670 label_theirs: "right^0",
1671 favor: MergeFavor::None,
1672 style: ConflictStyle::ZealousDiff3,
1673 marker_size: 7,
1674 diff_algorithm: None,
1675 ignore_all_space: false,
1676 ignore_space_change: false,
1677 ignore_space_at_eol: false,
1678 ignore_cr_at_eol: false,
1679 };
1680 let out = merge(&input).unwrap();
1681 let rendered = String::from_utf8(out.content).unwrap();
1682 assert_eq!(out.conflicts, 1, "{rendered}");
1683 assert!(rendered.contains("<<<<<<< HEAD\nD\nE\nF\n"), "{rendered}");
1684 }
1685
1686 #[test]
1687 fn is_binary_detects_nul_byte() {
1688 assert!(is_binary(b"text\0more"));
1689 assert!(!is_binary(b"just text\n"));
1690 }
1691
1692 #[test]
1693 fn diff3_style_emits_base_section() {
1694 let input = MergeInput {
1695 base: b"a\nb\nc\n",
1696 ours: b"a\nOURS\nc\n",
1697 theirs: b"a\nTHEIRS\nc\n",
1698 label_ours: "ours",
1699 label_base: "base",
1700 label_theirs: "theirs",
1701 favor: MergeFavor::None,
1702 style: ConflictStyle::Diff3,
1703 marker_size: 7,
1704 diff_algorithm: None,
1705 ignore_all_space: false,
1706 ignore_space_change: false,
1707 ignore_space_at_eol: false,
1708 ignore_cr_at_eol: false,
1709 };
1710 let out = merge(&input).unwrap();
1711 let s = String::from_utf8(out.content).unwrap();
1712 assert!(s.contains("||||||| base"));
1713 }
1714
1715 #[test]
1716 fn ignore_space_change_merges_space_only_edits() {
1717 let input = MergeInput {
1718 base: b"a b\n",
1719 ours: b"a b\n",
1720 theirs: b"a b\nline\n",
1721 label_ours: "ours",
1722 label_base: "base",
1723 label_theirs: "theirs",
1724 favor: MergeFavor::None,
1725 style: ConflictStyle::Merge,
1726 marker_size: 7,
1727 diff_algorithm: None,
1728 ignore_all_space: false,
1729 ignore_space_change: true,
1730 ignore_space_at_eol: false,
1731 ignore_cr_at_eol: false,
1732 };
1733 let out = merge(&input).unwrap();
1734 assert_eq!(out.conflicts, 0);
1735 }
1736
1737 #[test]
1738 fn ignore_all_space_can_clean_merge() {
1739 let input = MergeInput {
1740 base: b"a \nb\n",
1741 ours: b"a\nb\n",
1742 theirs: b"a\nb changed\n",
1743 label_ours: "ours",
1744 label_base: "base",
1745 label_theirs: "theirs",
1746 favor: MergeFavor::None,
1747 style: ConflictStyle::Merge,
1748 marker_size: 7,
1749 diff_algorithm: None,
1750 ignore_all_space: true,
1751 ignore_space_change: false,
1752 ignore_space_at_eol: false,
1753 ignore_cr_at_eol: false,
1754 };
1755 let out = merge(&input).unwrap();
1756 assert_eq!(out.conflicts, 0);
1757 }
1758
1759 #[test]
1760 fn custom_marker_size_shortens_conflict_markers() {
1761 let input = MergeInput {
1762 base: b"a\nb\nc\n",
1763 ours: b"a\nX\nc\n",
1764 theirs: b"a\nY\nc\n",
1765 label_ours: "ours",
1766 label_base: "base",
1767 label_theirs: "theirs",
1768 favor: MergeFavor::None,
1769 style: ConflictStyle::Merge,
1770 marker_size: 5,
1771 diff_algorithm: None,
1772 ignore_all_space: false,
1773 ignore_space_change: false,
1774 ignore_space_at_eol: false,
1775 ignore_cr_at_eol: false,
1776 };
1777 let out = merge(&input).unwrap();
1778 let s = String::from_utf8(out.content).unwrap();
1779 assert!(s.contains("<<<<< ours"));
1780 assert!(!s.contains("<<<<<<< ours"));
1781 }
1782
1783 #[test]
1784 fn zealous_diff3_prefix_suppressed_when_carried_by_previous_hunk() {
1785 let input = MergeInput {
1786 base: b"keep\nbase\nline\n",
1787 ours: b"keep\nours\nline\n",
1788 theirs: b"keep\ntheirs\nline\n",
1789 label_ours: "ours",
1790 label_base: "base",
1791 label_theirs: "theirs",
1792 favor: MergeFavor::None,
1793 style: ConflictStyle::ZealousDiff3,
1794 marker_size: 7,
1795 diff_algorithm: None,
1796 ignore_all_space: false,
1797 ignore_space_change: false,
1798 ignore_space_at_eol: false,
1799 ignore_cr_at_eol: false,
1800 };
1801 let out = merge(&input).unwrap();
1802 assert_eq!(out.conflicts, 1);
1803 let s = String::from_utf8(out.content).unwrap();
1804 assert!(s.contains("<<<<<<< ours"));
1805 }
1806
1807 #[test]
1808 fn histogram_diff_algorithm_runs() {
1809 let input = MergeInput {
1810 base: b"a\nb\nc\n",
1811 ours: b"a\nB\nc\n",
1812 theirs: b"a\nb\nc\n",
1813 label_ours: "ours",
1814 label_base: "base",
1815 label_theirs: "theirs",
1816 favor: MergeFavor::None,
1817 style: ConflictStyle::Merge,
1818 marker_size: 7,
1819 diff_algorithm: Some("histogram".to_string()),
1820 ignore_all_space: false,
1821 ignore_space_change: false,
1822 ignore_space_at_eol: false,
1823 ignore_cr_at_eol: false,
1824 };
1825 let out = merge(&input).unwrap();
1826 assert_eq!(out.content, b"a\nB\nc\n");
1827 }
1828
1829 #[test]
1830 fn patience_diff_algorithm_runs() {
1831 let input = MergeInput {
1832 base: b"x\ny\n",
1833 ours: b"x\nY\n",
1834 theirs: b"x\ny\n",
1835 label_ours: "ours",
1836 label_base: "base",
1837 label_theirs: "theirs",
1838 favor: MergeFavor::None,
1839 style: ConflictStyle::Merge,
1840 marker_size: 7,
1841 diff_algorithm: Some("patience".to_string()),
1842 ignore_all_space: false,
1843 ignore_space_change: false,
1844 ignore_space_at_eol: false,
1845 ignore_cr_at_eol: false,
1846 };
1847 let out = merge(&input).unwrap();
1848 assert_eq!(out.content, b"x\nY\n");
1849 }
1850
1851 #[test]
1852 fn ignore_space_at_eol_treats_trailing_whitespace_as_equal() {
1853 let input = MergeInput {
1854 base: b"line \n",
1855 ours: b"line\n",
1856 theirs: b"line \n",
1857 label_ours: "ours",
1858 label_base: "base",
1859 label_theirs: "theirs",
1860 favor: MergeFavor::None,
1861 style: ConflictStyle::Merge,
1862 marker_size: 7,
1863 diff_algorithm: None,
1864 ignore_all_space: false,
1865 ignore_space_change: false,
1866 ignore_space_at_eol: true,
1867 ignore_cr_at_eol: false,
1868 };
1869 let out = merge(&input).unwrap();
1870 assert_eq!(out.conflicts, 0);
1871 }
1872
1873 #[cfg_attr(
1874 not(feature = "full-tests"),
1875 ignore = "starts system git or is heavy; run with --features full-tests"
1876 )]
1877 #[test]
1878 fn add_add_without_base_performs_content_merge() {
1879 let input = MergeInput {
1880 base: b"",
1881 ours: b"ours\n",
1882 theirs: b"theirs\n",
1883 label_ours: "ours",
1884 label_base: "base",
1885 label_theirs: "theirs",
1886 favor: MergeFavor::None,
1887 style: ConflictStyle::Merge,
1888 marker_size: 7,
1889 diff_algorithm: None,
1890 ignore_all_space: false,
1891 ignore_space_change: false,
1892 ignore_space_at_eol: false,
1893 ignore_cr_at_eol: false,
1894 };
1895 let out = merge(&input).unwrap();
1896 assert_eq!(out.conflicts, 1);
1897 let s = String::from_utf8(out.content).unwrap();
1898 assert!(s.contains("<<<<<<< ours"));
1899 }
1900
1901 #[test]
1902 fn crlf_inputs_use_cr_lf_in_conflict_markers() {
1903 let input = MergeInput {
1904 base: b"a\r\nb\r\n",
1905 ours: b"a\r\nOURS\r\n",
1906 theirs: b"a\r\nTHEIRS\r\n",
1907 label_ours: "ours",
1908 label_base: "base",
1909 label_theirs: "theirs",
1910 favor: MergeFavor::None,
1911 style: ConflictStyle::Merge,
1912 marker_size: 7,
1913 diff_algorithm: None,
1914 ignore_all_space: false,
1915 ignore_space_change: false,
1916 ignore_space_at_eol: false,
1917 ignore_cr_at_eol: false,
1918 };
1919 let out = merge(&input).unwrap();
1920 let s = String::from_utf8(out.content).unwrap();
1921 assert_eq!(out.conflicts, 1);
1922 assert!(s.contains("<<<<<<< ours\r\n"));
1923 }
1924
1925 #[test]
1926 fn append_lines_separates_when_buffer_lacks_trailing_newline() {
1927 let input = MergeInput {
1928 base: b"no-final-newline",
1929 ours: b"no-final-newline\nours-line\n",
1930 theirs: b"no-final-newline",
1931 label_ours: "ours",
1932 label_base: "base",
1933 label_theirs: "theirs",
1934 favor: MergeFavor::None,
1935 style: ConflictStyle::Merge,
1936 marker_size: 7,
1937 diff_algorithm: None,
1938 ignore_all_space: false,
1939 ignore_space_change: false,
1940 ignore_space_at_eol: false,
1941 ignore_cr_at_eol: false,
1942 };
1943 let out = merge(&input).unwrap();
1944 assert_eq!(out.content, b"no-final-newline\nours-line\n");
1945 }
1946
1947 #[test]
1948 fn ignore_cr_at_eol_normalizes_cr_only_ends() {
1949 let input = MergeInput {
1950 base: b"a\r\nb\r\n",
1951 ours: b"a\r\nB\r\n",
1952 theirs: b"a\r\nb\r\n",
1953 label_ours: "ours",
1954 label_base: "base",
1955 label_theirs: "theirs",
1956 favor: MergeFavor::None,
1957 style: ConflictStyle::Merge,
1958 marker_size: 7,
1959 diff_algorithm: None,
1960 ignore_all_space: false,
1961 ignore_space_change: false,
1962 ignore_space_at_eol: false,
1963 ignore_cr_at_eol: true,
1964 };
1965 let out = merge(&input).unwrap();
1966 assert_eq!(out.conflicts, 0);
1967 }
1968
1969 #[test]
1970 fn unknown_diff_algorithm_falls_back_to_myers() {
1971 let input = MergeInput {
1972 base: b"a\n",
1973 ours: b"b\n",
1974 theirs: b"a\n",
1975 label_ours: "ours",
1976 label_base: "base",
1977 label_theirs: "theirs",
1978 favor: MergeFavor::None,
1979 style: ConflictStyle::Merge,
1980 marker_size: 7,
1981 diff_algorithm: Some("unknown-algo".to_string()),
1982 ignore_all_space: false,
1983 ignore_space_change: false,
1984 ignore_space_at_eol: false,
1985 ignore_cr_at_eol: false,
1986 };
1987 let out = merge(&input).unwrap();
1988 assert_eq!(out.content, b"b\n");
1989 }
1990
1991 #[test]
1992 fn refine_splits_large_conflict_regions() {
1993 let base = b"alpha beta gamma\n";
1994 let ours = b"alpha BETA gamma\n";
1995 let theirs = b"alpha beta GAMMA\n";
1996 let input = MergeInput {
1997 base,
1998 ours,
1999 theirs,
2000 label_ours: "ours",
2001 label_base: "base",
2002 label_theirs: "theirs",
2003 favor: MergeFavor::None,
2004 style: ConflictStyle::Merge,
2005 marker_size: 7,
2006 diff_algorithm: None,
2007 ignore_all_space: false,
2008 ignore_space_change: false,
2009 ignore_space_at_eol: false,
2010 ignore_cr_at_eol: false,
2011 };
2012 let out = merge(&input).unwrap();
2013 assert!(out.conflicts >= 1);
2014 }
2015
2016 #[test]
2017 fn merge_style_conflict_uses_shared_prefix_outside_markers() {
2018 let input = MergeInput {
2019 base: b"pre\nmid\npost\n",
2020 ours: b"pre\nOURS\npost\n",
2021 theirs: b"pre\nTHEIRS\npost\n",
2022 label_ours: "ours",
2023 label_base: "base",
2024 label_theirs: "theirs",
2025 favor: MergeFavor::None,
2026 style: ConflictStyle::Merge,
2027 marker_size: 7,
2028 diff_algorithm: None,
2029 ignore_all_space: false,
2030 ignore_space_change: false,
2031 ignore_space_at_eol: false,
2032 ignore_cr_at_eol: false,
2033 };
2034 let out = merge(&input).unwrap();
2035 let s = String::from_utf8(out.content).unwrap();
2036 assert_eq!(out.conflicts, 1);
2037 assert!(s.starts_with("pre\n"));
2038 assert!(s.ends_with("post\n"));
2039 }
2040
2041 #[test]
2042 fn marker_size_zero_defaults_to_seven() {
2043 let input = MergeInput {
2044 base: b"a\nb\nc\n",
2045 ours: b"a\nX\nc\n",
2046 theirs: b"a\nY\nc\n",
2047 label_ours: "ours",
2048 label_base: "base",
2049 label_theirs: "theirs",
2050 favor: MergeFavor::None,
2051 style: ConflictStyle::Merge,
2052 marker_size: 0,
2053 diff_algorithm: None,
2054 ignore_all_space: false,
2055 ignore_space_change: false,
2056 ignore_space_at_eol: false,
2057 ignore_cr_at_eol: false,
2058 };
2059 let out = merge(&input).unwrap();
2060 let s = String::from_utf8(out.content).unwrap();
2061 assert!(s.contains("<<<<<<< ours"));
2062 }
2063
2064 #[test]
2065 fn favor_theirs_resolves_without_markers() {
2066 let input = MergeInput {
2067 base: b"a\nb\nc\n",
2068 ours: b"a\nX\nc\n",
2069 theirs: b"a\nY\nc\n",
2070 label_ours: "ours",
2071 label_base: "base",
2072 label_theirs: "theirs",
2073 favor: MergeFavor::Theirs,
2074 style: ConflictStyle::Merge,
2075 marker_size: 7,
2076 diff_algorithm: None,
2077 ignore_all_space: false,
2078 ignore_space_change: false,
2079 ignore_space_at_eol: false,
2080 ignore_cr_at_eol: false,
2081 };
2082 let out = merge(&input).unwrap();
2083 assert_eq!(out.content, b"a\nY\nc\n");
2084 assert_eq!(out.conflicts, 0);
2085 }
2086
2087 #[test]
2088 fn preserves_shared_and_superset_insertions() {
2089 let input = MergeInput {
2090 base: b"1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n",
2091 ours: b"1\n2\n3\n4\n5\n5.5\n6\n7\n8\n9\n10\n",
2092 theirs: b"1\n2\n3\n4\n5\n5.5\n6\n7\n8\n9\n10\n10.5\n",
2093 label_ours: "ours",
2094 label_base: "base",
2095 label_theirs: "theirs",
2096 favor: MergeFavor::None,
2097 style: ConflictStyle::Merge,
2098 marker_size: 7,
2099 diff_algorithm: None,
2100 ignore_all_space: false,
2101 ignore_space_change: false,
2102 ignore_space_at_eol: false,
2103 ignore_cr_at_eol: false,
2104 };
2105 let base_lines = split_lines(input.base);
2106 let ours_lines = split_lines(input.ours);
2107 let theirs_lines = split_lines(input.theirs);
2108 let ws_mode = WhitespaceMode::default();
2109 let base_compare_lines = normalize_lines_for_compare(&base_lines, &ws_mode);
2110 let ours_compare_lines = normalize_lines_for_compare(&ours_lines, &ws_mode);
2111 let theirs_compare_lines = normalize_lines_for_compare(&theirs_lines, &ws_mode);
2112 let ours_ops = similar::capture_diff_slices(
2113 Algorithm::Myers,
2114 &base_compare_lines,
2115 &ours_compare_lines,
2116 );
2117 let theirs_ops = similar::capture_diff_slices(
2118 Algorithm::Myers,
2119 &base_compare_lines,
2120 &theirs_compare_lines,
2121 );
2122 let hunks = compute_hunks(
2123 &base_lines,
2124 &ours_lines,
2125 &theirs_lines,
2126 &ours_ops,
2127 &theirs_ops,
2128 &ws_mode,
2129 );
2130 assert_eq!(
2131 hunks.len(),
2132 4,
2133 "expected unchanged shared insertion and theirs-only tail insertion"
2134 );
2135 let out = merge(&input).unwrap();
2136 let rendered = String::from_utf8(out.content).unwrap();
2137 assert_eq!(out.conflicts, 0, "{rendered}");
2138 assert_eq!(
2139 rendered, "1\n2\n3\n4\n5\n5.5\n6\n7\n8\n9\n10\n10.5\n",
2140 "{rendered}"
2141 );
2142 }
2143}