conflicts: use new multi-way diff for materialized multi-way conflicts

This copies the conflict marker format I added a while ago to
Mercurial (https://phab.mercurial-scm.org/D9551), except that it uses
`+++++++` instead of `=======` for sections that are pure adds. The
reason I made that change is because we also have support for pure
removes (Mercurial never ends up in that situation because it has
exactly one remove and two adds).

This change resolves part of issue #19.
This commit is contained in:
Martin von Zweigbergk 2021-06-27 09:36:32 -07:00
parent 1390a044e7
commit 443528159e
3 changed files with 93 additions and 101 deletions

View file

@ -349,10 +349,11 @@ Working copy now at: 619f58d8a988
added 0 files, modified 1 files, removed 0 files
$ cat file1
<<<<<<<
a
|||||||
b1
=======
-------
+++++++
-b1
+a
+++++++
b2
>>>>>>>
$ echo resolved > file1

View file

@ -12,11 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::cmp::min;
use std::io::{Cursor, Write};
use itertools::Itertools;
use crate::diff::{find_line_ranges, Diff, DiffHunk};
use crate::files;
use crate::files::{MergeHunk, MergeResult};
use crate::repo_path::RepoPath;
use crate::store::{Conflict, ConflictPart, TreeValue};
use crate::store_wrapper::StoreWrapper;
@ -76,6 +79,49 @@ fn file_parts(parts: &[ConflictPart]) -> Vec<&ConflictPart> {
.collect_vec()
}
fn get_file_contents(store: &StoreWrapper, path: &RepoPath, part: &ConflictPart) -> Vec<u8> {
if let TreeValue::Normal {
id,
executable: false,
} = &part.value
{
let mut content: Vec<u8> = vec![];
store
.read_file(path, id)
.unwrap()
.read_to_end(&mut content)
.unwrap();
content
} else {
panic!("unexpectedly got a non-file conflict part");
}
}
fn write_diff_hunks(left: &[u8], right: &[u8], file: &mut dyn Write) -> std::io::Result<()> {
let diff = Diff::for_tokenizer(&[left, right], &find_line_ranges);
for hunk in diff.hunks() {
match hunk {
DiffHunk::Matching(content) => {
for line in content.split_inclusive(|b| *b == b'\n') {
file.write_all(b" ")?;
file.write_all(line)?;
}
}
DiffHunk::Different(content) => {
for line in content[0].split_inclusive(|b| *b == b'\n') {
file.write_all(b"-")?;
file.write_all(line)?;
}
for line in content[1].split_inclusive(|b| *b == b'\n') {
file.write_all(b"+")?;
file.write_all(line)?;
}
}
}
}
Ok(())
}
pub fn materialize_conflict(
store: &StoreWrapper,
path: &RepoPath,
@ -91,78 +137,55 @@ pub fn materialize_conflict(
return;
}
match conflict.to_three_way() {
None => {
file.write_all(b"Unresolved complex conflict.\n").unwrap();
let added_content = file_adds
.iter()
.map(|part| get_file_contents(store, path, part))
.collect_vec();
let removed_content = file_removes
.iter()
.map(|part| get_file_contents(store, path, part))
.collect_vec();
let removed_slices = removed_content
.iter()
.map(|vec| vec.as_slice())
.collect_vec();
let added_slices = added_content.iter().map(|vec| vec.as_slice()).collect_vec();
let merge_result = files::merge(&removed_slices, &added_slices);
match merge_result {
MergeResult::Resolved(content) => {
file.write_all(&content).unwrap();
}
Some((Some(left), Some(base), Some(right))) => {
match (left.value, base.value, right.value) {
(
TreeValue::Normal {
id: left_id,
executable: false,
},
TreeValue::Normal {
id: base_id,
executable: false,
},
TreeValue::Normal {
id: right_id,
executable: false,
},
) => {
let mut left_contents: Vec<u8> = vec![];
let mut base_contents: Vec<u8> = vec![];
let mut right_contents: Vec<u8> = vec![];
store
.read_file(path, &left_id)
.unwrap()
.read_to_end(&mut left_contents)
.unwrap();
store
.read_file(path, &base_id)
.unwrap()
.read_to_end(&mut base_contents)
.unwrap();
store
.read_file(path, &right_id)
.unwrap()
.read_to_end(&mut right_contents)
.unwrap();
let merge_result =
files::merge(&[&base_contents], &[&left_contents, &right_contents]);
match merge_result {
files::MergeResult::Resolved(contents) => {
file.write_all(&contents).unwrap();
MergeResult::Conflict(hunks) => {
for hunk in hunks {
match hunk {
MergeHunk::Resolved(content) => {
file.write_all(&content).unwrap();
}
MergeHunk::Conflict { removes, adds } => {
let num_diffs = min(removes.len(), adds.len());
// TODO: Pair up a remove with an add in a way that minimizes the size of
// the diff
file.write_all(b"<<<<<<<\n").unwrap();
for i in 0..num_diffs {
file.write_all(b"-------\n").unwrap();
file.write_all(b"+++++++\n").unwrap();
write_diff_hunks(&removes[i], &adds[i], file).unwrap();
}
files::MergeResult::Conflict(hunks) => {
for hunk in hunks {
match hunk {
files::MergeHunk::Resolved(contents) => {
file.write_all(&contents).unwrap();
}
files::MergeHunk::Conflict { removes, adds } => {
file.write_all(b"<<<<<<<\n").unwrap();
file.write_all(&adds[0]).unwrap();
file.write_all(b"|||||||\n").unwrap();
file.write_all(&removes[0]).unwrap();
file.write_all(b"=======\n").unwrap();
file.write_all(&adds[1]).unwrap();
file.write_all(b">>>>>>>\n").unwrap();
}
}
}
for slice in removes.iter().skip(num_diffs) {
file.write_all(b"-------\n").unwrap();
file.write_all(slice).unwrap();
}
for slice in adds.iter().skip(num_diffs) {
file.write_all(b"+++++++\n").unwrap();
file.write_all(slice).unwrap();
}
file.write_all(b">>>>>>>\n").unwrap();
}
}
_ => {
file.write_all(b"Unresolved 3-way conflict.\n").unwrap();
}
}
}
Some(_) => {
file.write_all(b"Unresolved complex conflict.\n").unwrap();
}
}
}

View file

@ -182,38 +182,6 @@ pub struct Conflict {
pub adds: Vec<ConflictPart>,
}
impl Conflict {
// Returns (left,base,right) if this conflict is a 3-way conflict
pub fn to_three_way(
&self,
) -> Option<(
Option<ConflictPart>,
Option<ConflictPart>,
Option<ConflictPart>,
)> {
if self.removes.len() == 1 && self.adds.len() == 2 {
// Regular (modify/modify) 3-way conflict
Some((
Some(self.adds[0].clone()),
Some(self.removes[0].clone()),
Some(self.adds[1].clone()),
))
} else if self.removes.is_empty() && self.adds.len() == 2 {
// Add/add conflict
Some((Some(self.adds[0].clone()), None, Some(self.adds[1].clone())))
} else if self.removes.len() == 1 && self.adds.len() == 1 {
// Modify/delete conflict
Some((
Some(self.adds[0].clone()),
Some(self.removes[0].clone()),
None,
))
} else {
None
}
}
}
impl Default for Conflict {
fn default() -> Self {
Conflict {