use crate::line_diff::{
DiffAlgorithm, DiffLine, DiffOp, WsIgnore, canonicalize_line, diff_lines_with_algorithm,
myers_diff_lines, myers_diff_lines_ws, split_lines,
};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum MergeFavor {
None,
Ours,
Theirs,
Union,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ConflictStyle {
#[default]
Merge,
Diff3,
ZDiff3,
}
#[derive(Debug, Clone, Copy)]
pub struct MergeBlobOptions<'a> {
pub ours_label: &'a str,
pub theirs_label: &'a str,
pub base_label: &'a str,
pub style: ConflictStyle,
pub favor: MergeFavor,
pub ws_ignore: WsIgnore,
pub marker_size: usize,
}
impl Default for MergeBlobOptions<'_> {
fn default() -> Self {
Self {
ours_label: "ours",
theirs_label: "theirs",
base_label: "base",
style: ConflictStyle::Merge,
favor: MergeFavor::None,
ws_ignore: WsIgnore::EMPTY,
marker_size: 7,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MergeBlobResult {
pub content: Vec<u8>,
pub conflicted: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct MergeFileOptions<'a> {
pub ours_label: &'a str,
pub base_label: &'a str,
pub theirs_label: &'a str,
pub style: ConflictStyle,
pub favor: MergeFavor,
pub marker_size: usize,
pub algorithm: DiffAlgorithm,
}
impl Default for MergeFileOptions<'_> {
fn default() -> Self {
Self {
ours_label: "ours",
base_label: "base",
theirs_label: "theirs",
style: ConflictStyle::Merge,
favor: MergeFavor::None,
marker_size: 7,
algorithm: DiffAlgorithm::Myers,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MergeFileOutcome {
pub content: Vec<u8>,
pub conflicts: usize,
}
pub fn merge_file(
base: &[u8],
ours: &[u8],
theirs: &[u8],
options: &MergeFileOptions<'_>,
) -> MergeFileOutcome {
let blob_options = MergeBlobOptions {
ours_label: options.ours_label,
base_label: options.base_label,
theirs_label: options.theirs_label,
style: options.style,
favor: options.favor,
ws_ignore: WsIgnore::EMPTY,
marker_size: options.marker_size,
};
let (result, conflicts) =
merge_blobs_internal(base, ours, theirs, &blob_options, options.algorithm);
MergeFileOutcome {
content: result.content,
conflicts,
}
}
pub fn merge_blobs(
base: &[u8],
ours: &[u8],
theirs: &[u8],
options: &MergeBlobOptions<'_>,
) -> MergeBlobResult {
merge_blobs_internal(base, ours, theirs, options, DiffAlgorithm::Myers).0
}
fn merge_blobs_internal(
base: &[u8],
ours: &[u8],
theirs: &[u8],
options: &MergeBlobOptions<'_>,
algorithm: DiffAlgorithm,
) -> (MergeBlobResult, usize) {
let base_lines = split_lines(base);
let ours_lines = split_lines(ours);
let theirs_lines = split_lines(theirs);
let ours_matches = matching_regions(&base_lines, &ours_lines, options.ws_ignore, algorithm);
let theirs_matches = matching_regions(&base_lines, &theirs_lines, options.ws_ignore, algorithm);
let stable = common_stable_segments(&ours_matches, &theirs_matches);
let stable = if options.style == ConflictStyle::Merge {
simplify_conflict_separators(
stable,
&base_lines,
&ours_lines,
&theirs_lines,
options.ws_ignore,
)
} else {
stable
};
let mut writer = MergeWriter::new(options, ours);
let mut base_idx = 0usize;
let mut our_idx = 0usize;
let mut their_idx = 0usize;
for seg in &stable {
let base_region = &base_lines[base_idx..seg.base_start];
let our_region = &ours_lines[our_idx..seg.ours_start];
let their_region = &theirs_lines[their_idx..seg.theirs_start];
emit_region(
&mut writer,
base_region,
our_region,
their_region,
options.ws_ignore,
);
writer.emit_lines(&ours_lines[seg.ours_start..seg.ours_start + seg.len]);
base_idx = seg.base_start + seg.len;
our_idx = seg.ours_start + seg.len;
their_idx = seg.theirs_start + seg.len;
}
emit_region(
&mut writer,
&base_lines[base_idx..],
&ours_lines[our_idx..],
&theirs_lines[their_idx..],
options.ws_ignore,
);
writer.finish()
}
fn emit_region(
writer: &mut MergeWriter<'_>,
base_region: &[DiffLine<'_>],
our_region: &[DiffLine<'_>],
their_region: &[DiffLine<'_>],
ws_ignore: WsIgnore,
) {
if our_region.is_empty() && their_region.is_empty() {
return;
}
let our_changed = !regions_match(our_region, base_region, ws_ignore);
let their_changed = !regions_match(their_region, base_region, ws_ignore);
match (our_changed, their_changed) {
(false, false) => writer.emit_lines(our_region),
(true, false) => writer.emit_lines(our_region),
(false, true) => writer.emit_lines(their_region),
(true, true) => {
if regions_match(our_region, their_region, ws_ignore) {
writer.emit_lines(our_region);
} else {
writer.emit_conflict_refined(our_region, base_region, their_region);
}
}
}
}
fn regions_match(a: &[DiffLine<'_>], b: &[DiffLine<'_>], ws_ignore: WsIgnore) -> bool {
if ws_ignore.is_empty() {
return a == b;
}
a.len() == b.len()
&& a.iter().zip(b).all(|(x, y)| {
canonicalize_line(x.content, ws_ignore) == canonicalize_line(y.content, ws_ignore)
})
}
enum RefineItem {
Context(std::ops::Range<usize>),
Conflict(std::ops::Range<usize>, std::ops::Range<usize>),
}
fn refine_conflict_items(ours: &[DiffLine<'_>], theirs: &[DiffLine<'_>]) -> Vec<RefineItem> {
let ops = myers_diff_lines(ours, theirs);
let mut raw: Vec<RefineItem> = Vec::new();
let mut oi = 0usize;
let mut ti = 0usize;
let mut pending: Option<(usize, usize, usize, usize)> = None; for op in ops {
match op {
DiffOp::Equal(n) => {
if let Some((o0, o1, t0, t1)) = pending.take() {
raw.push(RefineItem::Conflict(o0..o1, t0..t1));
}
raw.push(RefineItem::Context(oi..oi + n));
oi += n;
ti += n;
}
DiffOp::Delete(n) => {
let entry = pending.get_or_insert((oi, oi, ti, ti));
entry.1 = oi + n;
oi += n;
}
DiffOp::Insert(n) => {
let entry = pending.get_or_insert((oi, oi, ti, ti));
entry.3 = ti + n;
ti += n;
}
}
}
if let Some((o0, o1, t0, t1)) = pending.take() {
raw.push(RefineItem::Conflict(o0..o1, t0..t1));
}
let mut out: Vec<RefineItem> = Vec::new();
let mut idx = 0usize;
while idx < raw.len() {
match &raw[idx] {
RefineItem::Context(range) => {
let small = ours[range.clone()]
.iter()
.filter(|line| line.content.iter().any(u8::is_ascii_alphanumeric))
.count()
<= 3;
let prev_conflict = matches!(out.last(), Some(RefineItem::Conflict(..)));
let next_conflict = matches!(raw.get(idx + 1), Some(RefineItem::Conflict(..)));
if small && prev_conflict && next_conflict {
let Some(RefineItem::Conflict(po, pt)) = out.pop() else {
unreachable!()
};
let RefineItem::Conflict(no, nt) = &raw[idx + 1] else {
unreachable!()
};
out.push(RefineItem::Conflict(po.start..no.end, pt.start..nt.end));
idx += 2;
} else {
out.push(RefineItem::Context(range.clone()));
idx += 1;
}
}
RefineItem::Conflict(o, t) => {
out.push(RefineItem::Conflict(o.clone(), t.clone()));
idx += 1;
}
}
}
out
}
#[derive(Debug, Clone, Copy)]
struct MatchRegion {
base_start: usize,
side_start: usize,
len: usize,
}
#[derive(Debug, Clone, Copy)]
struct StableSegment {
base_start: usize,
ours_start: usize,
theirs_start: usize,
len: usize,
}
fn matching_regions(
base: &[DiffLine<'_>],
side: &[DiffLine<'_>],
ws_ignore: WsIgnore,
algorithm: DiffAlgorithm,
) -> Vec<MatchRegion> {
let ops = if ws_ignore.is_empty() {
diff_lines_with_algorithm(base, side, algorithm)
} else {
myers_diff_lines_ws(base, side, ws_ignore, algorithm)
};
let mut regions = Vec::new();
let mut base_idx = 0usize;
let mut side_idx = 0usize;
for op in ops {
match op {
DiffOp::Equal(n) => {
regions.push(MatchRegion {
base_start: base_idx,
side_start: side_idx,
len: n,
});
base_idx += n;
side_idx += n;
}
DiffOp::Delete(n) => base_idx += n,
DiffOp::Insert(n) => side_idx += n,
}
}
regions
}
fn common_stable_segments(ours: &[MatchRegion], theirs: &[MatchRegion]) -> Vec<StableSegment> {
let mut segments = Vec::new();
let mut oi = 0usize;
let mut ti = 0usize;
while oi < ours.len() && ti < theirs.len() {
let o = ours[oi];
let t = theirs[ti];
let o_end = o.base_start + o.len;
let t_end = t.base_start + t.len;
let lo = o.base_start.max(t.base_start);
let hi = o_end.min(t_end);
if lo < hi {
segments.push(StableSegment {
base_start: lo,
ours_start: o.side_start + (lo - o.base_start),
theirs_start: t.side_start + (lo - t.base_start),
len: hi - lo,
});
}
if o_end <= t_end {
oi += 1;
} else {
ti += 1;
}
}
segments
}
fn simplify_conflict_separators<'a>(
stable: Vec<StableSegment>,
base: &[DiffLine<'a>],
ours: &[DiffLine<'a>],
theirs: &[DiffLine<'a>],
ws_ignore: WsIgnore,
) -> Vec<StableSegment> {
stable
.iter()
.enumerate()
.filter_map(|(index, segment)| {
let previous = index.checked_sub(1).and_then(|index| stable.get(index));
let next = stable.get(index + 1);
let before = changed_span_conflicts(
&base[previous.map_or(0, |item| item.base_start + item.len)..segment.base_start],
&ours[previous.map_or(0, |item| item.ours_start + item.len)..segment.ours_start],
&theirs
[previous.map_or(0, |item| item.theirs_start + item.len)..segment.theirs_start],
ws_ignore,
);
let after = changed_span_conflicts(
&base[segment.base_start + segment.len
..next.map_or(base.len(), |item| item.base_start)],
&ours[segment.ours_start + segment.len
..next.map_or(ours.len(), |item| item.ours_start)],
&theirs[segment.theirs_start + segment.len
..next.map_or(theirs.len(), |item| item.theirs_start)],
ws_ignore,
);
let alnum_lines = base[segment.base_start..segment.base_start + segment.len]
.iter()
.filter(|line| line.content.iter().any(u8::is_ascii_alphanumeric))
.count();
(!(before && after && alnum_lines <= 3)).then_some(*segment)
})
.collect()
}
fn changed_span_conflicts(
base: &[DiffLine<'_>],
ours: &[DiffLine<'_>],
theirs: &[DiffLine<'_>],
ws_ignore: WsIgnore,
) -> bool {
!regions_match(ours, base, ws_ignore)
&& !regions_match(theirs, base, ws_ignore)
&& !regions_match(ours, theirs, ws_ignore)
}
struct MergeWriter<'a> {
out: Vec<u8>,
conflicted: bool,
conflicts: usize,
crlf: bool,
options: &'a MergeBlobOptions<'a>,
}
impl<'a> MergeWriter<'a> {
fn new(options: &'a MergeBlobOptions<'a>, ours: &[u8]) -> Self {
Self {
out: Vec::new(),
conflicted: false,
conflicts: 0,
crlf: ours.windows(2).any(|window| window == b"\r\n"),
options,
}
}
fn emit_lines(&mut self, lines: &[DiffLine<'_>]) {
for line in lines {
self.out.extend_from_slice(line.content);
}
}
fn emit_conflict(
&mut self,
ours: &[DiffLine<'_>],
base: &[DiffLine<'_>],
theirs: &[DiffLine<'_>],
) {
if self.options.favor == MergeFavor::Ours {
self.emit_section(ours);
return;
}
if self.options.favor == MergeFavor::Theirs {
self.emit_section(theirs);
return;
}
if self.options.favor == MergeFavor::Union {
self.emit_section(ours);
self.ensure_newline();
self.emit_section(theirs);
return;
}
let (prefix, suffix) = if self.options.style == ConflictStyle::ZDiff3 {
let prefix = common_prefix_len(ours, theirs);
let suffix = common_suffix_len(ours, theirs, prefix);
(prefix, suffix)
} else {
(0, 0)
};
if prefix != 0 {
self.emit_section(&ours[..prefix]);
}
let ours_inner = &ours[prefix..ours.len() - suffix];
let theirs_inner = &theirs[prefix..theirs.len() - suffix];
self.conflicted = true;
self.conflicts += 1;
self.write_marker(b'<', self.options.ours_label);
self.emit_section(ours_inner);
if matches!(
self.options.style,
ConflictStyle::Diff3 | ConflictStyle::ZDiff3
) {
self.ensure_newline();
self.write_marker(b'|', self.options.base_label);
self.emit_section(base);
}
self.ensure_newline();
self.write_divider();
self.emit_section(theirs_inner);
self.ensure_newline();
self.write_marker(b'>', self.options.theirs_label);
if suffix != 0 {
self.emit_section(&ours[ours.len() - suffix..]);
}
}
fn emit_conflict_refined(
&mut self,
ours: &[DiffLine<'_>],
base: &[DiffLine<'_>],
theirs: &[DiffLine<'_>],
) {
if matches!(
self.options.style,
ConflictStyle::Diff3 | ConflictStyle::ZDiff3
) || self.options.favor != MergeFavor::None
|| ours.is_empty()
|| theirs.is_empty()
{
self.emit_conflict(ours, base, theirs);
return;
}
for item in refine_conflict_items(ours, theirs) {
match item {
RefineItem::Context(range) => self.emit_lines(&ours[range]),
RefineItem::Conflict(o, t) => self.emit_conflict(&ours[o], &[], &theirs[t]),
}
}
}
fn emit_section(&mut self, lines: &[DiffLine<'_>]) {
for line in lines {
self.out.extend_from_slice(line.content);
}
}
fn ensure_newline(&mut self) {
if !self.out.is_empty() && self.out.last() != Some(&b'\n') {
self.write_line_terminator();
}
}
fn write_marker(&mut self, ch: u8, label: &str) {
for _ in 0..self.options.marker_size {
self.out.push(ch);
}
if !label.is_empty() {
self.out.push(b' ');
self.out.extend_from_slice(label.as_bytes());
}
self.write_line_terminator();
}
fn write_divider(&mut self) {
for _ in 0..self.options.marker_size {
self.out.push(b'=');
}
self.write_line_terminator();
}
fn write_line_terminator(&mut self) {
if self.crlf {
self.out.extend_from_slice(b"\r\n");
} else {
self.out.push(b'\n');
}
}
fn finish(self) -> (MergeBlobResult, usize) {
let result = MergeBlobResult {
content: self.out,
conflicted: self.conflicted,
};
(result, self.conflicts)
}
}
fn common_prefix_len(a: &[DiffLine<'_>], b: &[DiffLine<'_>]) -> usize {
a.iter()
.zip(b)
.take_while(|(left, right)| left == right)
.count()
}
fn common_suffix_len(a: &[DiffLine<'_>], b: &[DiffLine<'_>], prefix: usize) -> usize {
let max = a.len().min(b.len()).saturating_sub(prefix);
let mut len = 0;
while len < max && a[a.len() - 1 - len] == b[b.len() - 1 - len] {
len += 1;
}
len
}
#[cfg(test)]
mod merge_file_tests {
use super::*;
fn options() -> MergeFileOptions<'static> {
MergeFileOptions {
ours_label: "ours",
base_label: "base",
theirs_label: "theirs",
..MergeFileOptions::default()
}
}
#[test]
fn zealous_merge_absorbs_small_alphanumeric_separator() {
let base = b"a\nbase-one\ncontext\nbase-two\nz\n";
let ours = b"a\nours-one\ncontext\nours-two\nz\n";
let theirs = b"a\ntheirs-one\ncontext\ntheirs-two\nz\n";
let outcome = merge_file(base, ours, theirs, &options());
assert_eq!(outcome.conflicts, 1);
assert_eq!(
outcome
.content
.windows(7)
.filter(|window| *window == b"=======")
.count(),
1
);
}
#[test]
fn zealous_alnum_absorbs_more_than_three_blank_lines() {
let base = b"a\nbase-one\n\n\n\n\nbase-two\nz\n";
let ours = b"a\nours-one\n\n\n\n\nours-two\nz\n";
let theirs = b"a\ntheirs-one\n\n\n\n\ntheirs-two\nz\n";
let outcome = merge_file(base, ours, theirs, &options());
assert_eq!(outcome.conflicts, 1);
}
#[test]
fn diff3_does_not_apply_zealous_conflict_coalescing() {
let base = b"a\nbase-one\ncontext\nbase-two\nz\n";
let ours = b"a\nours-one\ncontext\nours-two\nz\n";
let theirs = b"a\ntheirs-one\ncontext\ntheirs-two\nz\n";
let mut options = options();
options.style = ConflictStyle::Diff3;
let outcome = merge_file(base, ours, theirs, &options);
assert_eq!(outcome.conflicts, 2);
}
#[test]
fn conflict_markers_follow_crlf_input() {
let base = b"one\r\nbase\r\nthree";
let ours = b"one\r\nours\r\nthree";
let theirs = b"one\r\ntheirs\r\nthree";
let outcome = merge_file(base, ours, theirs, &options());
assert!(outcome.content.windows(9).any(|line| line == b"<<<<<<< o"));
assert!(
outcome
.content
.split(|byte| *byte == b'\n')
.filter(|line| line.starts_with(b"<<<<<<<")
|| line.starts_with(b"=======")
|| line.starts_with(b">>>>>>>"))
.all(|line| line.ends_with(b"\r"))
);
}
}