use std::ops::Range;
use similar::{Algorithm, DiffTag, capture_diff_slices};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Change {
pub clean: bool,
pub line: usize,
pub yours: String,
pub default: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Merge {
pub text: String,
pub base: String,
pub changes: Vec<Change>,
pub kept: usize,
pub base_lines: usize,
}
impl Merge {
pub fn applied(&self) -> impl Iterator<Item = &Change> {
self.changes.iter().filter(|c| c.clean)
}
pub fn conflicts(&self) -> impl Iterator<Item = &Change> {
self.changes.iter().filter(|c| !c.clean)
}
}
struct Hunk {
base: Range<usize>,
side: Range<usize>,
}
struct Chunk<'h> {
base: Range<usize>,
yours: Vec<&'h Hunk>,
new: Vec<&'h Hunk>,
}
pub fn merge(base: &str, yours: &str, new: &str) -> Merge {
let base_lines: Vec<&str> = base.split_inclusive('\n').collect();
let your_lines: Vec<&str> = yours.split_inclusive('\n').collect();
let new_lines: Vec<&str> = new.split_inclusive('\n').collect();
let your_hunks = hunks(&base_lines, &your_lines);
let new_hunks = hunks(&base_lines, &new_lines);
let changed: usize = your_hunks.iter().map(|h| h.base.len()).sum();
let mut merged = Merge {
text: String::new(),
base: String::new(),
changes: Vec::new(),
kept: base_lines.len() - changed,
base_lines: base_lines.len(),
};
let mut at = 0;
let mut line = 1;
for chunk in chunks(&your_hunks, &new_hunks) {
let same = base_lines[at..chunk.base.start].concat();
merged.text.push_str(&same);
merged.base.push_str(&same);
line += chunk.base.start - at;
let original = base_lines[chunk.base.clone()].concat();
let (mine, mine_count) = side(&base_lines, &your_lines, &chunk.yours, &chunk.base);
let (theirs, _) = side(&base_lines, &new_lines, &chunk.new, &chunk.base);
let clean = chunk.yours.is_empty() && !chunk.new.is_empty();
if chunk.new.is_empty() {
merged.base.push_str(&original);
} else if mine == theirs {
merged.base.push_str(&theirs);
} else {
merged
.base
.push_str(if clean { &theirs } else { &original });
merged.changes.push(Change {
clean,
line,
yours: mine.clone(),
default: theirs.clone(),
});
}
merged.text.push_str(if clean { &theirs } else { &mine });
line += mine_count;
at = chunk.base.end;
}
let rest = base_lines[at..].concat();
merged.text.push_str(&rest);
merged.base.push_str(&rest);
merged
}
fn hunks(base: &[&str], side: &[&str]) -> Vec<Hunk> {
capture_diff_slices(Algorithm::Myers, base, side)
.iter()
.filter_map(|op| {
let (tag, base, side) = op.as_tag_tuple();
(tag != DiffTag::Equal).then_some(Hunk { base, side })
})
.collect()
}
fn chunks<'h>(yours: &'h [Hunk], new: &'h [Hunk]) -> Vec<Chunk<'h>> {
let mut all: Vec<(bool, &Hunk)> = yours
.iter()
.map(|h| (true, h))
.chain(new.iter().map(|h| (false, h)))
.collect();
all.sort_by_key(|(_, h)| (h.base.start, h.base.end));
let mut chunks: Vec<Chunk<'h>> = Vec::new();
for (mine, hunk) in all {
let joins = chunks
.last()
.is_some_and(|last| hunk.base.start <= last.base.end);
if !joins {
chunks.push(Chunk {
base: hunk.base.clone(),
yours: Vec::new(),
new: Vec::new(),
});
}
let Some(chunk) = chunks.last_mut() else {
continue;
};
chunk.base.end = chunk.base.end.max(hunk.base.end);
if mine {
chunk.yours.push(hunk);
} else {
chunk.new.push(hunk);
}
}
chunks
}
fn side(base: &[&str], side: &[&str], hunks: &[&Hunk], range: &Range<usize>) -> (String, usize) {
let mut text = String::new();
let mut count = 0;
let mut at = range.start;
for hunk in hunks {
text.push_str(&base[at..hunk.base.start].concat());
text.push_str(&side[hunk.side.clone()].concat());
count += hunk.base.start - at + hunk.side.len();
at = hunk.base.end;
}
text.push_str(&base[at..range.end].concat());
(text, count + range.end - at)
}