use std::hash::Hash;
use similar::{Algorithm, DiffOp, capture_diff_slices};
const MARK_LOCAL: &str = "<<<<<<< local (your edits)";
const MARK_SEP: &str = "=======";
const MARK_DISK: &str = ">>>>>>> disk";
enum Region<T> {
Resolved(Vec<T>),
Conflict {
#[allow(dead_code)]
base: Vec<T>,
ours: Vec<T>,
theirs: Vec<T>,
},
}
pub fn three_way(base: &[String], ours: &[String], theirs: &[String]) -> (Vec<String>, bool) {
let mut out: Vec<String> = Vec::new();
let mut had_conflict = false;
for region in diff3(base, ours, theirs) {
match region {
Region::Resolved(lines) => out.extend(lines),
Region::Conflict {
base, ours, theirs, ..
} => match char_retry(&base, &ours, &theirs) {
Some(merged) => out.extend(merged),
None => {
had_conflict = true;
out.push(MARK_LOCAL.to_string());
out.extend(ours);
out.push(MARK_SEP.to_string());
out.extend(theirs);
out.push(MARK_DISK.to_string());
}
},
}
}
if out.is_empty() {
out.push(String::new());
}
(out, had_conflict)
}
fn char_retry(base: &[String], ours: &[String], theirs: &[String]) -> Option<Vec<String>> {
let b: Vec<char> = base.join("\n").chars().collect();
let o: Vec<char> = ours.join("\n").chars().collect();
let t: Vec<char> = theirs.join("\n").chars().collect();
let mut merged: Vec<char> = Vec::new();
for region in diff3(&b, &o, &t) {
match region {
Region::Resolved(chars) => merged.extend(chars),
Region::Conflict { .. } => return None,
}
}
let text: String = merged.into_iter().collect();
Some(text.split('\n').map(str::to_string).collect())
}
fn diff3<T: Clone + Eq + Hash>(base: &[T], ours: &[T], theirs: &[T]) -> Vec<Region<T>> {
let mut regions = Vec::new();
let (mut ia, mut ib, mut iz) = (0usize, 0usize, 0usize);
for (zmatch, zend, amatch, aend, bmatch, bend) in sync_regions(base, ours, theirs) {
let divergent = amatch > ia || bmatch > ib;
if divergent {
let equal_a = slice_eq(ours, ia, amatch, base, iz, zmatch);
let equal_b = slice_eq(theirs, ib, bmatch, base, iz, zmatch);
let same = slice_eq(ours, ia, amatch, theirs, ib, bmatch);
if same || (equal_b && !equal_a) {
regions.push(Region::Resolved(ours[ia..amatch].to_vec()));
} else if equal_a && !equal_b {
regions.push(Region::Resolved(theirs[ib..bmatch].to_vec()));
} else {
regions.push(Region::Conflict {
base: base[iz..zmatch].to_vec(),
ours: ours[ia..amatch].to_vec(),
theirs: theirs[ib..bmatch].to_vec(),
});
}
}
if zend > zmatch {
regions.push(Region::Resolved(base[zmatch..zend].to_vec()));
}
ia = aend;
ib = bend;
iz = zend;
}
regions
}
fn sync_regions<T: Eq + Hash>(
base: &[T],
ours: &[T],
theirs: &[T],
) -> Vec<(usize, usize, usize, usize, usize, usize)> {
let am = matching_blocks(base, ours);
let bm = matching_blocks(base, theirs);
let mut sl = Vec::new();
let (mut ia, mut ib) = (0usize, 0usize);
while ia < am.len() && ib < bm.len() {
let (abase, amatch, alen) = am[ia];
let (bbase, bmatch, blen) = bm[ib];
let i = abase.max(bbase);
let j = (abase + alen).min(bbase + blen);
if i < j {
let asub = amatch + (i - abase);
let bsub = bmatch + (i - bbase);
let len = j - i;
sl.push((i, j, asub, asub + len, bsub, bsub + len));
}
if abase + alen < bbase + blen {
ia += 1;
} else {
ib += 1;
}
}
sl.push((
base.len(),
base.len(),
ours.len(),
ours.len(),
theirs.len(),
theirs.len(),
));
sl
}
fn matching_blocks<T: Eq + Hash>(base: &[T], side: &[T]) -> Vec<(usize, usize, usize)> {
let mut m: Vec<(usize, usize, usize)> = capture_diff_slices(Algorithm::Myers, base, side)
.into_iter()
.filter_map(|op| match op {
DiffOp::Equal {
old_index,
new_index,
len,
} => Some((old_index, new_index, len)),
_ => None,
})
.collect();
m.push((base.len(), side.len(), 0));
m
}
fn slice_eq<T: Eq>(x: &[T], xlo: usize, xhi: usize, y: &[T], ylo: usize, yhi: usize) -> bool {
xhi - xlo == yhi - ylo && x[xlo..xhi] == y[ylo..yhi]
}
#[cfg(test)]
mod tests {
use super::*;
fn lines(s: &str) -> Vec<String> {
s.split('\n').map(str::to_string).collect()
}
fn merge_str(base: &str, ours: &str, theirs: &str) -> (String, bool) {
let (out, c) = three_way(&lines(base), &lines(ours), &lines(theirs));
(out.join("\n"), c)
}
#[test]
fn disjoint_line_edits_merge_clean() {
let (out, conflict) = merge_str("a\nb\nc", "A\nb\nc", "a\nb\nC");
assert!(!conflict);
assert_eq!(out, "A\nb\nC");
}
#[test]
fn one_sided_change_takes_that_side() {
let (out, conflict) = merge_str("a\nb\nc", "a\nb\nc", "a\nB\nc");
assert!(!conflict);
assert_eq!(out, "a\nB\nc");
}
#[test]
fn identical_change_on_both_sides_is_not_conflict() {
let (out, conflict) = merge_str("a\nb\nc", "a\nX\nc", "a\nX\nc");
assert!(!conflict);
assert_eq!(out, "a\nX\nc");
}
#[test]
fn same_line_disjoint_columns_merge_via_char_pass() {
let (out, conflict) = merge_str(
" foo = calc(x)",
" foo = compute(x)",
" foo = calc(x)",
);
assert!(!conflict, "expected clean merge, got:\n{out}");
assert_eq!(out, " foo = compute(x)");
}
#[test]
fn true_overlap_conflicts_with_markers() {
let (out, conflict) = merge_str("a\nb\nc", "a\nlocal\nc", "a\ndisk\nc");
assert!(conflict);
assert!(out.contains("<<<<<<< local (your edits)"), "{out}");
assert!(out.contains("local"), "{out}");
assert!(out.contains("======="), "{out}");
assert!(out.contains("disk"), "{out}");
assert!(out.contains(">>>>>>> disk"), "{out}");
}
#[test]
fn insertions_on_both_sides_merge() {
let (out, conflict) = merge_str("a\nb", "a\nNEW\nb", "a\nb\nTAIL");
assert!(!conflict);
assert_eq!(out, "a\nNEW\nb\nTAIL");
}
}