#![expect(missing_docs)]
use std::borrow::Borrow;
use std::collections::VecDeque;
use std::iter;
use std::mem;
use bstr::BStr;
use bstr::BString;
use either::Either;
use itertools::Itertools as _;
use crate::diff::ContentDiff;
use crate::diff::DiffHunk;
use crate::diff::DiffHunkKind;
use crate::merge::Merge;
use crate::merge::SameChange;
use crate::tree_merge::MergeOptions;
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct DiffLine<'a> {
pub line_number: DiffLineNumber,
pub hunks: Vec<(DiffLineHunkSide, &'a BStr)>,
}
impl DiffLine<'_> {
pub fn has_left_content(&self) -> bool {
self.hunks
.iter()
.any(|&(side, _)| side != DiffLineHunkSide::Right)
}
pub fn has_right_content(&self) -> bool {
self.hunks
.iter()
.any(|&(side, _)| side != DiffLineHunkSide::Left)
}
pub fn is_unmodified(&self) -> bool {
self.hunks
.iter()
.all(|&(side, _)| side == DiffLineHunkSide::Both)
}
fn take(&mut self) -> Self {
Self {
line_number: self.line_number,
hunks: mem::take(&mut self.hunks),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DiffLineNumber {
pub left: u32,
pub right: u32,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DiffLineHunkSide {
Both,
Left,
Right,
}
pub struct DiffLineIterator<'a, I> {
diff_hunks: iter::Fuse<I>,
current_line: DiffLine<'a>,
queued_lines: VecDeque<DiffLine<'a>>,
}
impl<'a, I> DiffLineIterator<'a, I>
where
I: Iterator,
I::Item: Borrow<DiffHunk<'a>>,
{
pub fn new(diff_hunks: I) -> Self {
let line_number = DiffLineNumber { left: 1, right: 1 };
Self::with_line_number(diff_hunks, line_number)
}
pub fn with_line_number(diff_hunks: I, line_number: DiffLineNumber) -> Self {
let current_line = DiffLine {
line_number,
hunks: vec![],
};
Self {
diff_hunks: diff_hunks.fuse(),
current_line,
queued_lines: VecDeque::new(),
}
}
}
impl<I> DiffLineIterator<'_, I> {
pub fn next_line_number(&self) -> DiffLineNumber {
let next_line = self.queued_lines.front().unwrap_or(&self.current_line);
next_line.line_number
}
}
impl<'a, I> Iterator for DiffLineIterator<'a, I>
where
I: Iterator,
I::Item: Borrow<DiffHunk<'a>>,
{
type Item = DiffLine<'a>;
fn next(&mut self) -> Option<Self::Item> {
while self.queued_lines.is_empty() {
let Some(hunk) = self.diff_hunks.next() else {
break;
};
let hunk = hunk.borrow();
match hunk.kind {
DiffHunkKind::Matching => {
debug_assert!(hunk.contents.iter().all_equal());
let text = hunk.contents[0];
let lines = text.split_inclusive(|b| *b == b'\n').map(BStr::new);
for line in lines {
self.current_line.hunks.push((DiffLineHunkSide::Both, line));
if line.ends_with(b"\n") {
self.queued_lines.push_back(self.current_line.take());
self.current_line.line_number.left += 1;
self.current_line.line_number.right += 1;
}
}
}
DiffHunkKind::Different => {
let [left_text, right_text] = hunk.contents[..]
.try_into()
.expect("hunk should have exactly two inputs");
let left_lines = left_text.split_inclusive(|b| *b == b'\n').map(BStr::new);
for left_line in left_lines {
self.current_line
.hunks
.push((DiffLineHunkSide::Left, left_line));
if left_line.ends_with(b"\n") {
self.queued_lines.push_back(self.current_line.take());
self.current_line.line_number.left += 1;
}
}
let mut right_lines =
right_text.split_inclusive(|b| *b == b'\n').map(BStr::new);
if right_text.starts_with(b"\n")
&& self.current_line.hunks.is_empty()
&& self
.queued_lines
.front()
.is_some_and(|queued| queued.has_right_content())
{
let blank_line = right_lines.next().unwrap();
assert_eq!(blank_line, b"\n");
self.current_line.line_number.right += 1;
}
for right_line in right_lines {
self.current_line
.hunks
.push((DiffLineHunkSide::Right, right_line));
if right_line.ends_with(b"\n") {
self.queued_lines.push_back(self.current_line.take());
self.current_line.line_number.right += 1;
}
}
}
}
}
if let Some(line) = self.queued_lines.pop_front() {
return Some(line);
}
if !self.current_line.hunks.is_empty() {
return Some(self.current_line.take());
}
None
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConflictDiffHunk<'input> {
pub kind: DiffHunkKind,
pub lefts: Merge<&'input BStr>,
pub rights: Merge<&'input BStr>,
}
pub fn conflict_diff_hunks<'input, I>(
diff_hunks: I,
num_lefts: usize,
) -> impl Iterator<Item = ConflictDiffHunk<'input>>
where
I: IntoIterator,
I::Item: Borrow<DiffHunk<'input>>,
{
fn to_merge<'input>(contents: &[&'input BStr]) -> Merge<&'input BStr> {
if contents.iter().all_equal() {
Merge::resolved(contents[0])
} else {
Merge::from_vec(contents)
}
}
diff_hunks.into_iter().map(move |hunk| {
let hunk = hunk.borrow();
let (lefts, rights) = hunk.contents.split_at(num_lefts);
if let ([left], [right]) = (lefts, rights) {
ConflictDiffHunk {
kind: hunk.kind,
lefts: Merge::resolved(left),
rights: Merge::resolved(right),
}
} else {
let lefts = to_merge(lefts);
let rights = to_merge(rights);
let kind = match hunk.kind {
DiffHunkKind::Matching => DiffHunkKind::Matching,
DiffHunkKind::Different if lefts == rights => DiffHunkKind::Matching,
DiffHunkKind::Different => DiffHunkKind::Different,
};
ConflictDiffHunk {
kind,
lefts,
rights,
}
}
})
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum FileMergeHunkLevel {
Line,
Word,
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub enum MergeResult {
Resolved(BString),
Conflict(Vec<Merge<BString>>),
}
pub fn merge_hunks<T: AsRef<[u8]>>(inputs: &Merge<T>, options: &MergeOptions) -> MergeResult {
merge_inner(inputs, options)
}
pub fn merge<T: AsRef<[u8]>>(inputs: &Merge<T>, options: &MergeOptions) -> Merge<BString> {
merge_inner(inputs, options)
}
pub fn try_merge<T: AsRef<[u8]>>(inputs: &Merge<T>, options: &MergeOptions) -> Option<BString> {
merge_inner(inputs, options)
}
fn merge_inner<'input, T, B>(inputs: &'input Merge<T>, options: &MergeOptions) -> B
where
T: AsRef<[u8]>,
B: FromMergeHunks<'input>,
{
let num_diffs = inputs.removes().len();
let diff = ContentDiff::by_line(inputs.removes().chain(inputs.adds()));
let hunks = resolve_diff_hunks(&diff, num_diffs, options.same_change);
match options.hunk_level {
FileMergeHunkLevel::Line => B::from_hunks(hunks.map(MergeHunk::Borrowed)),
FileMergeHunkLevel::Word => {
B::from_hunks(hunks.map(|h| merge_hunk_by_word(h, options.same_change)))
}
}
}
fn merge_hunk_by_word(inputs: Merge<&BStr>, same_change: SameChange) -> MergeHunk<'_> {
if inputs.is_resolved() {
return MergeHunk::Borrowed(inputs);
}
let num_diffs = inputs.removes().len();
let diff = ContentDiff::by_word(inputs.removes().chain(inputs.adds()));
let hunks = resolve_diff_hunks(&diff, num_diffs, same_change);
if let Some(content) = collect_resolved(hunks.map(MergeHunk::Borrowed)) {
MergeHunk::Owned(Merge::resolved(content))
} else {
drop(diff);
MergeHunk::Borrowed(inputs)
}
}
#[derive(Clone, Debug)]
enum MergeHunk<'input> {
Borrowed(Merge<&'input BStr>),
Owned(Merge<BString>),
}
impl MergeHunk<'_> {
fn len(&self) -> usize {
match self {
MergeHunk::Borrowed(merge) => merge.as_slice().len(),
MergeHunk::Owned(merge) => merge.as_slice().len(),
}
}
fn iter(&self) -> impl Iterator<Item = &BStr> {
match self {
MergeHunk::Borrowed(merge) => Either::Left(merge.iter().copied()),
MergeHunk::Owned(merge) => Either::Right(merge.iter().map(Borrow::borrow)),
}
}
fn as_resolved(&self) -> Option<&BStr> {
match self {
MergeHunk::Borrowed(merge) => merge.as_resolved().copied(),
MergeHunk::Owned(merge) => merge.as_resolved().map(Borrow::borrow),
}
}
fn into_owned(self) -> Merge<BString> {
match self {
MergeHunk::Borrowed(merge) => merge.map(|&s| s.to_owned()),
MergeHunk::Owned(merge) => merge,
}
}
}
trait FromMergeHunks<'input>: Sized {
fn from_hunks<I: IntoIterator<Item = MergeHunk<'input>>>(hunks: I) -> Self;
}
impl<'input> FromMergeHunks<'input> for MergeResult {
fn from_hunks<I: IntoIterator<Item = MergeHunk<'input>>>(hunks: I) -> Self {
collect_hunks(hunks)
}
}
impl<'input> FromMergeHunks<'input> for Merge<BString> {
fn from_hunks<I: IntoIterator<Item = MergeHunk<'input>>>(hunks: I) -> Self {
collect_merged(hunks)
}
}
impl<'input> FromMergeHunks<'input> for Option<BString> {
fn from_hunks<I: IntoIterator<Item = MergeHunk<'input>>>(hunks: I) -> Self {
collect_resolved(hunks)
}
}
fn collect_hunks<'input>(hunks: impl IntoIterator<Item = MergeHunk<'input>>) -> MergeResult {
let mut resolved_hunk = BString::new(vec![]);
let mut merge_hunks: Vec<Merge<BString>> = vec![];
for hunk in hunks {
if let Some(content) = hunk.as_resolved() {
resolved_hunk.extend_from_slice(content);
} else {
if !resolved_hunk.is_empty() {
merge_hunks.push(Merge::resolved(resolved_hunk));
resolved_hunk = BString::new(vec![]);
}
merge_hunks.push(hunk.into_owned());
}
}
if merge_hunks.is_empty() {
MergeResult::Resolved(resolved_hunk)
} else {
if !resolved_hunk.is_empty() {
merge_hunks.push(Merge::resolved(resolved_hunk));
}
MergeResult::Conflict(merge_hunks)
}
}
fn collect_merged<'input>(hunks: impl IntoIterator<Item = MergeHunk<'input>>) -> Merge<BString> {
let mut maybe_resolved = Merge::resolved(BString::default());
for hunk in hunks {
if let Some(content) = hunk.as_resolved() {
for buf in &mut maybe_resolved {
buf.extend_from_slice(content);
}
} else {
maybe_resolved = match maybe_resolved.into_resolved() {
Ok(content) => Merge::from_vec(vec![content; hunk.len()]),
Err(conflict) => conflict,
};
assert_eq!(maybe_resolved.as_slice().len(), hunk.len());
for (buf, s) in iter::zip(&mut maybe_resolved, hunk.iter()) {
buf.extend_from_slice(s);
}
}
}
maybe_resolved
}
fn collect_resolved<'input>(hunks: impl IntoIterator<Item = MergeHunk<'input>>) -> Option<BString> {
let mut resolved_content = BString::default();
for hunk in hunks {
resolved_content.extend_from_slice(hunk.as_resolved()?);
}
Some(resolved_content)
}
fn resolve_diff_hunks<'input>(
diff: &ContentDiff<'input>,
num_diffs: usize,
same_change: SameChange,
) -> impl Iterator<Item = Merge<&'input BStr>> {
diff.hunks().map(move |diff_hunk| match diff_hunk.kind {
DiffHunkKind::Matching => {
debug_assert!(diff_hunk.contents.iter().all_equal());
Merge::resolved(diff_hunk.contents[0])
}
DiffHunkKind::Different => {
let merge = Merge::from_removes_adds(
diff_hunk.contents[..num_diffs].iter().copied(),
diff_hunk.contents[num_diffs..].iter().copied(),
);
match merge.resolve_trivial(same_change) {
Some(&content) => Merge::resolved(content),
None => merge,
}
}
})
}
#[cfg(test)]
mod tests {
use indoc::indoc;
use super::*;
fn conflict<const N: usize>(values: [&[u8]; N]) -> Merge<BString> {
Merge::from_vec(values.map(hunk).to_vec())
}
fn resolved(value: &[u8]) -> Merge<BString> {
Merge::resolved(hunk(value))
}
fn hunk(data: &[u8]) -> BString {
data.into()
}
#[test]
fn test_diff_line_iterator_line_numbers() {
let mut line_iter = DiffLineIterator::with_line_number(
[DiffHunk::different(["a\nb", "c\nd\n"])].into_iter(),
DiffLineNumber { left: 1, right: 10 },
);
assert_eq!(
line_iter.next_line_number(),
DiffLineNumber { left: 1, right: 10 }
);
assert_eq!(
line_iter.next().unwrap(),
DiffLine {
line_number: DiffLineNumber { left: 1, right: 10 },
hunks: vec![(DiffLineHunkSide::Left, "a\n".as_ref())],
}
);
assert_eq!(
line_iter.next_line_number(),
DiffLineNumber { left: 2, right: 10 }
);
assert_eq!(
line_iter.next().unwrap(),
DiffLine {
line_number: DiffLineNumber { left: 2, right: 10 },
hunks: vec![
(DiffLineHunkSide::Left, "b".as_ref()),
(DiffLineHunkSide::Right, "c\n".as_ref()),
],
}
);
assert_eq!(
line_iter.next_line_number(),
DiffLineNumber { left: 2, right: 11 }
);
assert_eq!(
line_iter.next().unwrap(),
DiffLine {
line_number: DiffLineNumber { left: 2, right: 11 },
hunks: vec![(DiffLineHunkSide::Right, "d\n".as_ref())],
}
);
assert_eq!(
line_iter.next_line_number(),
DiffLineNumber { left: 2, right: 12 }
);
assert!(line_iter.next().is_none());
assert_eq!(
line_iter.next_line_number(),
DiffLineNumber { left: 2, right: 12 }
);
}
#[test]
fn test_diff_line_iterator_blank_right_line_single_left() {
let mut line_iter = DiffLineIterator::new(
[
DiffHunk::matching(["a"].repeat(2)),
DiffHunk::different(["x\n", "\ny\n"]),
]
.into_iter(),
);
assert_eq!(
line_iter.next().unwrap(),
DiffLine {
line_number: DiffLineNumber { left: 1, right: 1 },
hunks: vec![
(DiffLineHunkSide::Both, "a".as_ref()),
(DiffLineHunkSide::Left, "x\n".as_ref()),
],
}
);
assert_eq!(
line_iter.next().unwrap(),
DiffLine {
line_number: DiffLineNumber { left: 2, right: 2 },
hunks: vec![(DiffLineHunkSide::Right, "y\n".as_ref())],
}
);
}
#[test]
fn test_diff_line_iterator_blank_right_line_multiple_lefts() {
let mut line_iter = DiffLineIterator::new(
[
DiffHunk::matching(["a"].repeat(2)),
DiffHunk::different(["x\n\n", "\ny\n"]),
]
.into_iter(),
);
assert_eq!(
line_iter.next().unwrap(),
DiffLine {
line_number: DiffLineNumber { left: 1, right: 1 },
hunks: vec![
(DiffLineHunkSide::Both, "a".as_ref()),
(DiffLineHunkSide::Left, "x\n".as_ref()),
],
}
);
assert_eq!(
line_iter.next().unwrap(),
DiffLine {
line_number: DiffLineNumber { left: 2, right: 1 },
hunks: vec![(DiffLineHunkSide::Left, "\n".as_ref())],
}
);
assert_eq!(
line_iter.next().unwrap(),
DiffLine {
line_number: DiffLineNumber { left: 3, right: 2 },
hunks: vec![(DiffLineHunkSide::Right, "y\n".as_ref())],
}
);
}
#[test]
fn test_diff_line_iterator_blank_right_line_after_non_empty_left() {
let mut line_iter = DiffLineIterator::new(
[
DiffHunk::matching(["a"].repeat(2)),
DiffHunk::different(["x\nz", "\ny\n"]),
]
.into_iter(),
);
assert_eq!(
line_iter.next().unwrap(),
DiffLine {
line_number: DiffLineNumber { left: 1, right: 1 },
hunks: vec![
(DiffLineHunkSide::Both, "a".as_ref()),
(DiffLineHunkSide::Left, "x\n".as_ref()),
],
}
);
assert_eq!(
line_iter.next().unwrap(),
DiffLine {
line_number: DiffLineNumber { left: 2, right: 1 },
hunks: vec![
(DiffLineHunkSide::Left, "z".as_ref()),
(DiffLineHunkSide::Right, "\n".as_ref()),
],
}
);
assert_eq!(
line_iter.next().unwrap(),
DiffLine {
line_number: DiffLineNumber { left: 2, right: 2 },
hunks: vec![(DiffLineHunkSide::Right, "y\n".as_ref())],
}
);
}
#[test]
fn test_diff_line_iterator_blank_right_line_without_preceding_lines() {
let mut line_iter = DiffLineIterator::new([DiffHunk::different(["", "\ny\n"])].into_iter());
assert_eq!(
line_iter.next().unwrap(),
DiffLine {
line_number: DiffLineNumber { left: 1, right: 1 },
hunks: vec![(DiffLineHunkSide::Right, "\n".as_ref())],
}
);
assert_eq!(
line_iter.next().unwrap(),
DiffLine {
line_number: DiffLineNumber { left: 1, right: 2 },
hunks: vec![(DiffLineHunkSide::Right, "y\n".as_ref())],
}
);
}
#[test]
fn test_conflict_diff_hunks_no_conflicts() {
let diff_hunks = [
DiffHunk::matching(["a\n"].repeat(2)),
DiffHunk::different(["b\n", "c\n"]),
];
let num_lefts = 1;
insta::assert_debug_snapshot!(
conflict_diff_hunks(&diff_hunks, num_lefts).collect_vec(), @r#"
[
ConflictDiffHunk {
kind: Matching,
lefts: Resolved(
"a\n",
),
rights: Resolved(
"a\n",
),
},
ConflictDiffHunk {
kind: Different,
lefts: Resolved(
"b\n",
),
rights: Resolved(
"c\n",
),
},
]
"#);
}
#[test]
fn test_conflict_diff_hunks_simple_conflicts() {
let diff_hunks = [
DiffHunk::different(["a\n", "X\n", "b\n", "c\n"]),
DiffHunk::matching(["d\n"].repeat(4)),
DiffHunk::different(["e\n", "e\n", "e\n", "f\n"]),
];
let num_lefts = 3;
insta::assert_debug_snapshot!(
conflict_diff_hunks(&diff_hunks, num_lefts).collect_vec(), @r#"
[
ConflictDiffHunk {
kind: Different,
lefts: Conflicted(
[
"a\n",
"X\n",
"b\n",
],
),
rights: Resolved(
"c\n",
),
},
ConflictDiffHunk {
kind: Matching,
lefts: Resolved(
"d\n",
),
rights: Resolved(
"d\n",
),
},
ConflictDiffHunk {
kind: Different,
lefts: Resolved(
"e\n",
),
rights: Resolved(
"f\n",
),
},
]
"#);
}
#[test]
fn test_conflict_diff_hunks_matching_conflicts() {
let diff_hunks = [
DiffHunk::different(["a\n", "X\n", "b\n", "a\n", "X\n", "b\n"]),
DiffHunk::matching(["c\n"].repeat(6)),
];
let num_lefts = 3;
insta::assert_debug_snapshot!(
conflict_diff_hunks(&diff_hunks, num_lefts).collect_vec(), @r#"
[
ConflictDiffHunk {
kind: Matching,
lefts: Conflicted(
[
"a\n",
"X\n",
"b\n",
],
),
rights: Conflicted(
[
"a\n",
"X\n",
"b\n",
],
),
},
ConflictDiffHunk {
kind: Matching,
lefts: Resolved(
"c\n",
),
rights: Resolved(
"c\n",
),
},
]
"#);
}
#[test]
fn test_conflict_diff_hunks_no_trivial_resolution() {
let diff_hunks = [DiffHunk::different(["a", "b", "a", "a"])];
let num_lefts = 1;
insta::assert_debug_snapshot!(
conflict_diff_hunks(&diff_hunks, num_lefts).collect_vec(), @r#"
[
ConflictDiffHunk {
kind: Different,
lefts: Resolved(
"a",
),
rights: Conflicted(
[
"b",
"a",
"a",
],
),
},
]
"#);
let num_lefts = 3;
insta::assert_debug_snapshot!(
conflict_diff_hunks(&diff_hunks, num_lefts).collect_vec(), @r#"
[
ConflictDiffHunk {
kind: Different,
lefts: Conflicted(
[
"a",
"b",
"a",
],
),
rights: Resolved(
"a",
),
},
]
"#);
}
#[test]
fn test_merge_single_hunk() {
let options = MergeOptions {
hunk_level: FileMergeHunkLevel::Line,
same_change: SameChange::Accept,
};
let merge_hunks = |inputs: &_| merge_hunks(inputs, &options);
assert_eq!(
merge_hunks(&conflict([b"", b"", b""])),
MergeResult::Resolved(hunk(b""))
);
assert_eq!(
merge_hunks(&conflict([b"a", b"a", b"a"])),
MergeResult::Resolved(hunk(b"a"))
);
assert_eq!(
merge_hunks(&conflict([b"", b"a\n", b"a\n"])),
MergeResult::Resolved(hunk(b""))
);
assert_eq!(
merge_hunks(&conflict([b"a\n", b"a\n", b""])),
MergeResult::Resolved(hunk(b""))
);
assert_eq!(
merge_hunks(&conflict([b"", b"a\n", b""])),
MergeResult::Resolved(hunk(b""))
);
assert_eq!(
merge_hunks(&conflict([b"a b", b"a", b"a"])),
MergeResult::Resolved(hunk(b"a b"))
);
assert_eq!(
merge_hunks(&conflict([b"a", b"a", b"a b"])),
MergeResult::Resolved(hunk(b"a b"))
);
assert_eq!(
merge_hunks(&conflict([b"a\n", b"", b"a\n", b"", b"a\n"])),
MergeResult::Resolved(hunk(b"a\n"))
);
assert_eq!(
merge_hunks(&conflict([b"b", b"a", b"b", b"", b"b"])),
MergeResult::Conflict(vec![conflict([b"b", b"a", b"b", b"", b"b"])])
);
assert_eq!(
merge_hunks(&conflict([b"", b"a\n", b"", b"a\n", b"", b"a\n", b""])),
MergeResult::Resolved(hunk(b""))
);
assert_eq!(
merge_hunks(&conflict([b"b\n", b"a\n", b"", b"a\n", b""])),
MergeResult::Conflict(vec![conflict([b"b\n", b"a\n", b"", b"a\n", b""])])
);
assert_eq!(
merge_hunks(&conflict([b"b", b"a", b"b", b"a", b"b"])),
MergeResult::Resolved(hunk(b"b"))
);
assert_eq!(
merge_hunks(&conflict([b"", b"a\n", b"b\n"])),
MergeResult::Conflict(vec![conflict([b"", b"a\n", b"b\n"])])
);
assert_eq!(
merge_hunks(&conflict([b"b\n", b"a\n", b""])),
MergeResult::Conflict(vec![conflict([b"b\n", b"a\n", b""])])
);
assert_eq!(
merge_hunks(&conflict([b"b", b"a", b"c"])),
MergeResult::Conflict(vec![conflict([b"b", b"a", b"c"])])
);
assert_eq!(
merge_hunks(&conflict([b"a", b"a", b"", b"a", b"a"])),
MergeResult::Resolved(hunk(b""))
);
assert_eq!(
merge_hunks(&conflict([b"b", b"a", b"a", b"a", b"b"])),
MergeResult::Resolved(hunk(b"b"))
);
assert_eq!(
merge_hunks(&conflict([b"b", b"a", b"a", b"a", b"c"])),
MergeResult::Conflict(vec![conflict([b"b", b"a", b"a", b"a", b"c"])])
);
assert_eq!(
merge_hunks(&conflict([b"b", b"a", b"a", b"b", b"c"])),
MergeResult::Resolved(hunk(b"c"))
);
assert_eq!(
merge_hunks(&conflict([b"c", b"a", b"d", b"b", b"e"])),
MergeResult::Conflict(vec![conflict([b"c", b"a", b"d", b"b", b"e"])])
);
assert_eq!(
merge_hunks(&conflict([b"c", b"a", b"c", b"b", b"c"])),
MergeResult::Conflict(vec![conflict([b"c", b"a", b"c", b"b", b"c"])])
);
}
#[test]
fn test_merge_multi_hunk() {
let options = MergeOptions {
hunk_level: FileMergeHunkLevel::Line,
same_change: SameChange::Accept,
};
let merge_hunks = |inputs: &_| merge_hunks(inputs, &options);
let merge = |inputs: &_| merge(inputs, &options);
let try_merge = |inputs: &_| try_merge(inputs, &options);
let inputs = conflict([b"a\nb\n", b"a\n", b"a\nc\n"]);
assert_eq!(
merge_hunks(&inputs),
MergeResult::Conflict(vec![resolved(b"a\n"), conflict([b"b\n", b"", b"c\n"])])
);
assert_eq!(merge(&inputs), conflict([b"a\nb\n", b"a\n", b"a\nc\n"]));
assert_eq!(try_merge(&inputs), None);
let inputs = conflict([b"a2\nb\nc\n", b"a\nb\nc\n", b"a\nb\nc2\n"]);
assert_eq!(
merge_hunks(&inputs),
MergeResult::Resolved(hunk(b"a2\nb\nc2\n"))
);
assert_eq!(merge(&inputs), resolved(b"a2\nb\nc2\n"));
assert_eq!(try_merge(&inputs), Some(hunk(b"a2\nb\nc2\n")));
let inputs = conflict([b"a\nb1\nc\n", b"a\nb\nc\n", b"a\nb2\nc\n"]);
assert_eq!(
merge_hunks(&inputs),
MergeResult::Conflict(vec![
resolved(b"a\n"),
conflict([b"b1\n", b"b\n", b"b2\n"]),
resolved(b"c\n"),
])
);
assert_eq!(
merge(&inputs),
conflict([b"a\nb1\nc\n", b"a\nb\nc\n", b"a\nb2\nc\n"])
);
assert_eq!(try_merge(&inputs), None);
let inputs = conflict([b"a\nb\nc\n", b"a1\nb\nc\n", b"a2\nb\nc2\n"]);
assert_eq!(
merge_hunks(&inputs),
MergeResult::Conflict(vec![
conflict([b"a\n", b"a1\n", b"a2\n"]),
resolved(b"b\nc2\n"),
])
);
assert_eq!(
merge(&inputs),
conflict([b"a\nb\nc2\n", b"a1\nb\nc2\n", b"a2\nb\nc2\n"])
);
assert_eq!(try_merge(&inputs), None);
let base = indoc! {b"
a {
p
}
"};
let left = indoc! {b"
a {
q
}
b {
x
}
"};
let right = indoc! {b"
a {
p
}
b {
x
}
"};
let merged = indoc! {b"
a {
q
}
b {
x
}
b {
x
}
"};
assert_eq!(merge(&conflict([left, base, right])), resolved(merged));
}
#[test]
fn test_merge_hunk_by_word() {
let options = MergeOptions {
hunk_level: FileMergeHunkLevel::Word,
same_change: SameChange::Accept,
};
let merge = |inputs: &_| merge(inputs, &options);
assert_eq!(
merge(&conflict([b"c\nb\n", b"a\nb\n", b"a\nd\n"])),
resolved(b"c\nd\n")
);
assert_eq!(merge(&conflict([b"a b", b"a", b"c a"])), resolved(b"c a b"));
assert_eq!(
merge(&conflict([b"a b", b"a", b"a c"])),
conflict([b"a b", b"a", b"a c"])
);
assert_eq!(
merge(&conflict([b"a b", b"a", b"x a c"])),
conflict([b"a b", b"a", b"x a c"])
);
}
}