Skip to content

Commit a6ae6e5

Browse files
committed
Ability to merge any list of values that implement Eq + Hash
This makes this crate useful to apply 3-way merges on arbitrary data
1 parent d44d0f0 commit a6ae6e5

File tree

4 files changed

+118
-1
lines changed

4 files changed

+118
-1
lines changed

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,5 +221,5 @@ mod utils;
221221

222222
pub use apply::{apply, apply_bytes, ApplyError};
223223
pub use diff::{create_patch, create_patch_bytes, DiffOptions};
224-
pub use merge::{merge, merge_bytes, ConflictStyle, MergeOptions};
224+
pub use merge::{merge, merge_bytes, merge_custom, ConflictStyle, MergeOptions, MergeConflicts};
225225
pub use patch::{Hunk, HunkRange, Line, ParsePatchError, Patch, PatchFormatter};

src/merge/mod.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::{
44
utils::Classifier,
55
};
66
use std::{cmp, fmt};
7+
use std::hash::Hash;
78

89
#[cfg(test)]
910
mod tests;
@@ -205,6 +206,34 @@ impl MergeOptions {
205206
self.style,
206207
)
207208
}
209+
210+
pub fn merge_custom<'a, T: Eq + Hash>(
211+
&self,
212+
ancestor: &'a [T],
213+
ours: &'a [T],
214+
theirs: &'a [T],
215+
) -> Result<Vec<&'a T>, MergeConflicts> {
216+
let mut classifier = Classifier::default();
217+
let (ancestor_lines, ancestor_ids) = classifier.classify(ancestor);
218+
let (our_lines, our_ids) = classifier.classify(ours);
219+
let (their_lines, their_ids) = classifier.classify(theirs);
220+
221+
let opts = DiffOptions::default();
222+
let our_solution = opts.diff_slice(&ancestor_ids, &our_ids);
223+
let their_solution = opts.diff_slice(&ancestor_ids, &their_ids);
224+
225+
let merged = merge_solutions(&our_solution, &their_solution);
226+
let mut merge = diff3_range_to_merge_range(&merged);
227+
228+
cleanup_conflicts(&mut merge);
229+
230+
output_result_custom(
231+
&ancestor_lines,
232+
&our_lines,
233+
&their_lines,
234+
&merge,
235+
)
236+
}
208237
}
209238

210239
impl Default for MergeOptions {
@@ -277,6 +306,30 @@ pub fn merge_bytes<'a>(
277306
MergeOptions::default().merge_bytes(ancestor, ours, theirs)
278307
}
279308

309+
/// Infos about a merge that went wrong
310+
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)]
311+
pub struct MergeConflicts {
312+
/// How many conflicts have occurred
313+
pub count: usize
314+
}
315+
316+
impl std::fmt::Display for MergeConflicts {
317+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
318+
write!(f, "{} merge conflicts", self.count)
319+
}
320+
}
321+
322+
impl std::error::Error for MergeConflicts {}
323+
324+
/// Perform a 3-way merge between any list of values that support it
325+
pub fn merge_custom<'a, T: Eq + Hash>(
326+
ancestor: &'a [T],
327+
ours: &'a [T],
328+
theirs: &'a [T],
329+
) -> Result<Vec<&'a T>, MergeConflicts> {
330+
MergeOptions::default().merge_custom(ancestor, ours, theirs)
331+
}
332+
280333
fn merge_solutions<'ancestor, 'ours, 'theirs, T: ?Sized + SliceLike>(
281334
our_solution: &[DiffRange<'ancestor, 'ours, T>],
282335
their_solution: &[DiffRange<'ancestor, 'theirs, T>],
@@ -631,3 +684,39 @@ fn add_conflict_marker_bytes(
631684
}
632685
output.push(b'\n');
633686
}
687+
688+
fn output_result_custom<'a, T: Eq + Hash>(
689+
ancestor: &[&'a T],
690+
ours: &[&'a T],
691+
theirs: &[&'a T],
692+
merge: &[MergeRange<[u64]>],
693+
) -> Result<Vec<&'a T>, MergeConflicts> {
694+
let mut conflicts = 0;
695+
let mut output = Vec::new();
696+
697+
for merge_range in merge {
698+
match merge_range {
699+
MergeRange::Equal(range, ..) => {
700+
output.extend(ancestor[range.range()].iter().copied());
701+
}
702+
MergeRange::Conflict(_ancestor_range, _ours_range, _theirs_range) => {
703+
conflicts += 1;
704+
}
705+
MergeRange::Ours(range) => {
706+
output.extend(ours[range.range()].iter().copied());
707+
}
708+
MergeRange::Theirs(range) => {
709+
output.extend(theirs[range.range()].iter().copied());
710+
}
711+
MergeRange::Both(range, _) => {
712+
output.extend(ours[range.range()].iter().copied());
713+
}
714+
}
715+
}
716+
717+
if conflicts != 0 {
718+
Err(MergeConflicts { count: conflicts })
719+
} else {
720+
Ok(output)
721+
}
722+
}

src/merge/tests.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,18 @@ salt
215215
);
216216
}
217217

218+
#[test]
219+
fn test_merge_arbitrary_type() {
220+
let original = [1,2,3,4,5, 6];
221+
let ours = [1,2,3,4,5,100,6];
222+
let theirs = [1, 3,4,5, 6];
223+
let expected = [1, 3,4,5,100,6];
224+
225+
let result = merge_custom(&original, &ours, &theirs).unwrap();
226+
let result_owned: Vec<i32> = result.iter().map(|r| **r).collect();
227+
assert_eq!(result_owned, expected);
228+
}
229+
218230
#[test]
219231
fn myers_diffy_vs_git() {
220232
let original = "\

src/utils.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ impl<'a, T: ?Sized + Text> Classifier<'a, T> {
3232
}
3333
}
3434

35+
impl<'a, T: Eq + Hash> Classifier<'a, T> {
36+
pub fn classify(&mut self, data: &'a [T]) -> (Vec<&'a T>, Vec<u64>) {
37+
data.iter()
38+
.map(|item| (item, self.classify_item(item)))
39+
.unzip()
40+
}
41+
}
42+
3543
impl<T: Eq + Hash + ?Sized> Default for Classifier<'_, T> {
3644
fn default() -> Self {
3745
Self {
@@ -230,6 +238,14 @@ fn find_byte(haystack: &[u8], byte: u8) -> Option<usize> {
230238
mod test {
231239
use super::Classifier;
232240

241+
#[test]
242+
fn classify() {
243+
let input = vec![10, 11, 12, 13];
244+
let mut classifier = Classifier::default();
245+
let (lines, _ids) = classifier.classify(&input);
246+
assert_eq!(lines, vec![&10, &11, &12, &13]);
247+
}
248+
233249
#[test]
234250
fn classify_string() {
235251
let input = "abc\ndef";

0 commit comments

Comments
 (0)