Skip to content

Commit

Permalink
conflicts: use new multi-way diff for materialized multi-way conflicts
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
martinvonz committed Jun 30, 2021
1 parent 1390a04 commit 4435281
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 101 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
153 changes: 88 additions & 65 deletions lib/src/conflicts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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();
}
for slice in removes.iter().skip(num_diffs) {
file.write_all(b"-------\n").unwrap();
file.write_all(slice).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 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();
}
}
}

Expand Down
32 changes: 0 additions & 32 deletions lib/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 4435281

Please sign in to comment.