use crate::line_diff::{
canonicalize_line, myers_diff_lines, myers_diff_lines_ws, split_lines, DiffAlgorithm,
DiffLine, DiffOp, WsIgnore,
};
#[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,
}
#[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,
}
pub fn merge_blobs(
base: &[u8],
ours: &[u8],
theirs: &[u8],
options: &MergeBlobOptions<'_>,
) -> MergeBlobResult {
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);
let theirs_matches = matching_regions(&base_lines, &theirs_lines, options.ws_ignore);
let stable = common_stable_segments(&ours_matches, &theirs_matches);
let mut writer = MergeWriter::new(options);
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 = range.len() <= 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,
) -> Vec<MatchRegion> {
let ops = if ws_ignore.is_empty() {
myers_diff_lines(base, side)
} else {
myers_diff_lines_ws(base, side, ws_ignore, DiffAlgorithm::Myers)
};
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
}
struct MergeWriter<'a> {
out: Vec<u8>,
conflicted: bool,
options: &'a MergeBlobOptions<'a>,
}
impl<'a> MergeWriter<'a> {
fn new(options: &'a MergeBlobOptions<'a>) -> Self {
Self {
out: Vec::new(),
conflicted: false,
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::Union {
self.emit_section(ours);
self.ensure_newline();
self.emit_section(theirs);
return;
}
self.conflicted = true;
self.write_marker(b'<', self.options.ours_label);
self.emit_section(ours);
if self.options.style == ConflictStyle::Diff3 {
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);
self.ensure_newline();
self.write_marker(b'>', self.options.theirs_label);
}
fn emit_conflict_refined(
&mut self,
ours: &[DiffLine<'_>],
base: &[DiffLine<'_>],
theirs: &[DiffLine<'_>],
) {
if self.options.style == ConflictStyle::Diff3
|| 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.out.push(b'\n');
}
}
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.out.push(b'\n');
}
fn write_divider(&mut self) {
for _ in 0..self.options.marker_size {
self.out.push(b'=');
}
self.out.push(b'\n');
}
fn finish(self) -> MergeBlobResult {
MergeBlobResult {
content: self.out,
conflicted: self.conflicted,
}
}
}