#[derive(Debug, PartialEq, Eq)]
pub struct Merged {
pub text: String,
pub conflicts: usize,
}
pub fn merge(
base: &str,
ours: &str,
theirs: &str,
label_ours: &str,
label_theirs: &str,
) -> Merged {
let b: Vec<&str> = base.split_inclusive('\n').collect();
let o: Vec<&str> = ours.split_inclusive('\n').collect();
let t: Vec<&str> = theirs.split_inclusive('\n').collect();
let in_ours = align(&b, &o);
let in_theirs = align(&b, &t);
let mut text = String::with_capacity(theirs.len());
let mut conflicts = 0;
let (mut bi, mut oi, mut ti) = (0usize, 0usize, 0usize);
loop {
while bi < b.len() {
match (in_ours[bi], in_theirs[bi]) {
(Some(x), Some(y)) if x == oi && y == ti => {
text.push_str(b[bi]);
bi += 1;
oi += 1;
ti += 1;
}
_ => break,
}
}
if bi >= b.len() && oi >= o.len() && ti >= t.len() {
break;
}
let (nb, no, nt) = next_stable(&b, &in_ours, &in_theirs, bi, oi, ti, o.len(), t.len());
let base_slice = &b[bi..nb];
let ours_slice = &o[oi..no];
let theirs_slice = &t[ti..nt];
if same(ours_slice, base_slice) {
push(&mut text, theirs_slice);
} else if same(theirs_slice, base_slice) {
push(&mut text, ours_slice);
} else if same(ours_slice, theirs_slice) {
push(&mut text, ours_slice);
} else {
conflicts += 1;
conflict(&mut text, ours_slice, theirs_slice, label_ours, label_theirs);
}
bi = nb;
oi = no;
ti = nt;
}
Merged { text, conflicts }
}
#[allow(clippy::too_many_arguments)]
fn next_stable(
b: &[&str],
in_ours: &[Option<usize>],
in_theirs: &[Option<usize>],
bi: usize,
oi: usize,
ti: usize,
ours_len: usize,
theirs_len: usize,
) -> (usize, usize, usize) {
for k in bi..b.len() {
if let (Some(x), Some(y)) = (in_ours[k], in_theirs[k]) {
if x >= oi && y >= ti {
return (k, x, y);
}
}
}
(b.len(), ours_len, theirs_len)
}
fn same(a: &[&str], b: &[&str]) -> bool {
a == b
}
fn push(text: &mut String, lines: &[&str]) {
for line in lines {
text.push_str(line);
}
}
fn conflict(
text: &mut String,
ours: &[&str],
theirs: &[&str],
label_ours: &str,
label_theirs: &str,
) {
ensure_newline(text);
text.push_str("<<<<<<< ");
text.push_str(label_ours);
text.push('\n');
push(text, ours);
ensure_newline(text);
text.push_str("=======\n");
push(text, theirs);
ensure_newline(text);
text.push_str(">>>>>>> ");
text.push_str(label_theirs);
text.push('\n');
}
fn ensure_newline(text: &mut String) {
if !text.is_empty() && !text.ends_with('\n') {
text.push('\n');
}
}
fn align(a: &[&str], b: &[&str]) -> Vec<Option<usize>> {
let mut out = vec![None; a.len()];
let mut head = 0;
while head < a.len() && head < b.len() && a[head] == b[head] {
out[head] = Some(head);
head += 1;
}
let mut tail = 0;
while tail < a.len() - head && tail < b.len() - head && a[a.len() - 1 - tail] == b[b.len() - 1 - tail] {
out[a.len() - 1 - tail] = Some(b.len() - 1 - tail);
tail += 1;
}
let a_mid = &a[head..a.len() - tail];
let b_mid = &b[head..b.len() - tail];
if a_mid.is_empty() || b_mid.is_empty() {
return out;
}
const BUDGET: usize = 4_000_000;
if a_mid.len().saturating_mul(b_mid.len()) > BUDGET {
return out;
}
let n = a_mid.len();
let m = b_mid.len();
let mut table = vec![0u32; (n + 1) * (m + 1)];
for i in (0..n).rev() {
for j in (0..m).rev() {
table[i * (m + 1) + j] = if a_mid[i] == b_mid[j] {
table[(i + 1) * (m + 1) + j + 1] + 1
} else {
table[(i + 1) * (m + 1) + j].max(table[i * (m + 1) + j + 1])
};
}
}
let (mut i, mut j) = (0, 0);
while i < n && j < m {
if a_mid[i] == b_mid[j] {
out[head + i] = Some(head + j);
i += 1;
j += 1;
} else if table[(i + 1) * (m + 1) + j] >= table[i * (m + 1) + j + 1] {
i += 1;
} else {
j += 1;
}
}
out
}
pub fn has_conflict_markers(text: &str) -> bool {
text.lines()
.any(|line| line.starts_with("<<<<<<< ") || line == "=======" || line.starts_with(">>>>>>> "))
}
#[cfg(test)]
mod tests {
use super::*;
fn merged(base: &str, ours: &str, theirs: &str) -> Merged {
merge(base, ours, theirs, "yours", "new")
}
#[test]
fn a_file_nobody_touched_comes_back_unchanged() {
let text = "one\ntwo\nthree\n";
assert_eq!(merged(text, text, text).text, text);
assert_eq!(merged(text, text, text).conflicts, 0);
}
#[test]
fn a_change_only_the_new_version_made_is_taken() {
let base = "one\ntwo\nthree\n";
let theirs = "one\nTWO\nthree\n";
let result = merged(base, base, theirs);
assert_eq!(result.text, theirs);
assert_eq!(result.conflicts, 0);
}
#[test]
fn a_change_only_you_made_is_kept() {
let base = "one\ntwo\nthree\n";
let ours = "one\nmine\nthree\n";
let result = merged(base, ours, base);
assert_eq!(result.text, ours);
assert_eq!(result.conflicts, 0);
}
#[test]
fn edits_in_different_places_both_survive() {
let base = "one\ntwo\nthree\n";
let ours = "one\ntwo\nthree\nmine\n";
let theirs = "zero\none\ntwo\nthree\n";
let result = merged(base, ours, theirs);
assert_eq!(result.conflicts, 0);
assert_eq!(result.text, "zero\none\ntwo\nthree\nmine\n");
}
#[test]
fn the_same_edit_made_twice_is_not_a_conflict() {
let base = "one\ntwo\n";
let both = "one\nchanged\n";
let result = merged(base, both, both);
assert_eq!(result.conflicts, 0);
assert_eq!(result.text, both);
}
#[test]
fn two_different_edits_to_one_line_conflict() {
let base = "one\ntwo\nthree\n";
let ours = "one\nmine\nthree\n";
let theirs = "one\ntheirs\nthree\n";
let result = merged(base, ours, theirs);
assert_eq!(result.conflicts, 1);
assert_eq!(
result.text,
"one\n<<<<<<< yours\nmine\n=======\ntheirs\n>>>>>>> new\nthree\n"
);
}
#[test]
fn a_conflict_leaves_markers_the_guard_can_find() {
let result = merged("a\n", "b\n", "c\n");
assert!(has_conflict_markers(&result.text));
assert!(!has_conflict_markers("a\nb\n"));
}
#[test]
fn a_deletion_on_one_side_is_taken() {
let base = "one\ntwo\nthree\n";
let theirs = "one\nthree\n";
assert_eq!(merged(base, base, theirs).text, theirs);
assert_eq!(merged(base, theirs, base).text, theirs);
}
#[test]
fn a_file_with_no_final_newline_keeps_it_that_way() {
let base = "one\ntwo";
let ours = "one\nmine";
let result = merged(base, ours, base);
assert_eq!(result.text, "one\nmine");
}
#[test]
fn crlf_endings_survive() {
let base = "one\r\ntwo\r\n";
let theirs = "one\r\nTWO\r\n";
let result = merged(base, base, theirs);
assert_eq!(result.text, theirs);
}
#[test]
fn two_versions_of_a_file_that_did_not_exist_conflict() {
let result = merged("", "mine\n", "theirs\n");
assert_eq!(result.conflicts, 1);
}
#[test]
fn an_enormous_file_still_takes_a_one_sided_change() {
let base: String = (0..3000).map(|i| format!("line {i}\n")).collect();
let mut theirs = base.clone();
theirs.push_str("added\n");
let result = merged(&base, &base, &theirs);
assert_eq!(result.conflicts, 0);
assert_eq!(result.text, theirs);
}
}