ok/jj
1
0
Fork 0
forked from mirrors/jj

merge: fix modify/delete conflict to not resolve

This commit is contained in:
Martin von Zweigbergk 2022-04-06 12:18:24 -07:00 committed by Martin von Zweigbergk
parent 5a77e88b94
commit 97a1a3e20b
2 changed files with 82 additions and 25 deletions

View file

@ -28,6 +28,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed bugs ### Fixed bugs
* When rebasing a conflict where one side modified a file and the other side
deleted it, we no longer automatically resolve it in favor of the modified
content (this was a regression from commit c0ae4b16e8c4).
* Errors are now printed to stderr (they used to be printed to stdout). * Errors are now printed to stderr (they used to be printed to stdout).
* `jj untrack` now requires at least one path (allowing no arguments was a UX * `jj untrack` now requires at least one path (allowing no arguments was a UX

View file

@ -218,6 +218,10 @@ struct SyncRegion {
right: Range<usize>, right: Range<usize>,
} }
// TODO: Should we require `add.len() == removes.len() + 1`? If that condition
// is false, it effectively means that we should pretend that there are empty
// strings in `removes` or `adds` to make it true. Maybe we should have to
// caller make it explicitly that way.
pub fn merge(removes: &[&[u8]], adds: &[&[u8]]) -> MergeResult { pub fn merge(removes: &[&[u8]], adds: &[&[u8]]) -> MergeResult {
let num_removes = removes.len(); let num_removes = removes.len();
// TODO: Using the first remove as base (first in the inputs) is how it's // TODO: Using the first remove as base (first in the inputs) is how it's
@ -232,7 +236,9 @@ pub fn merge(removes: &[&[u8]], adds: &[&[u8]]) -> MergeResult {
for diff_hunk in diff.hunks() { for diff_hunk in diff.hunks() {
match diff_hunk { match diff_hunk {
DiffHunk::Matching(content) => { DiffHunk::Matching(content) => {
resolved_hunk.extend(content); if adds.len() > removes.len() {
resolved_hunk.extend(content);
}
} }
DiffHunk::Different(parts) => { DiffHunk::Different(parts) => {
let mut removed_parts = parts[..num_removes].to_vec(); let mut removed_parts = parts[..num_removes].to_vec();
@ -256,12 +262,17 @@ pub fn merge(removes: &[&[u8]], adds: &[&[u8]]) -> MergeResult {
if removed_parts.is_empty() && added_parts.is_empty() { if removed_parts.is_empty() && added_parts.is_empty() {
// The same content was added and removed, so there's // The same content was added and removed, so there's
// nothing left. // nothing left.
} else if distinct_removes.len() <= 1 && distinct_adds.len() == 1 { } else if distinct_removes.is_empty() && distinct_adds.len() == 1 {
// All sides made the same change. This includes: // All sides added the same content
// removes: [A], adds: [B, B, B] resolved_hunk.extend(added_parts[0]);
// removes: [], adds: [B, B] } else if distinct_removes.len() == 1 && distinct_adds.is_empty() {
// removes: [A, A], adds: [] // All sides removed the same content
// We consider all these to not be conflicts. } else if distinct_removes.len() == 1
&& distinct_adds.len() == 1
&& added_parts.len() == removed_parts.len() + 1
{
// All sides made the same change, and there's a matching extra base to apply it
// to
resolved_hunk.extend(added_parts[0]); resolved_hunk.extend(added_parts[0]);
} else { } else {
if !resolved_hunk.is_empty() { if !resolved_hunk.is_empty() {
@ -301,34 +312,81 @@ mod tests {
#[test] #[test]
fn test_merge() { fn test_merge() {
// Unchanged and empty on all sides
assert_eq!( assert_eq!(
merge(&[b""], &[b"", b""]), merge(&[b""], &[b"", b""]),
MergeResult::Resolved(b"".to_vec()) MergeResult::Resolved(b"".to_vec())
); );
// Unchanged on all sides
assert_eq!( assert_eq!(
merge(&[b"a"], &[b"a", b"a"]), merge(&[b"a"], &[b"a", b"a"]),
MergeResult::Resolved(b"a".to_vec()) MergeResult::Resolved(b"a".to_vec())
); );
// One side removed, one side unchanged
assert_eq!( assert_eq!(
merge(&[b"a"], &[b"", b"a"]), merge(&[b"a\n"], &[b"", b"a\n"]),
MergeResult::Resolved(b"".to_vec()) MergeResult::Resolved(b"".to_vec())
); );
// One side unchanged, one side removed
assert_eq!( assert_eq!(
merge(&[b"a"], &[b"a", b""]), merge(&[b"a\n"], &[b"a\n", b""]),
MergeResult::Resolved(b"".to_vec()) MergeResult::Resolved(b"".to_vec())
); );
// Both sides removed same line
assert_eq!( assert_eq!(
merge(&[b"a"], &[b"", b""]), merge(&[b"a\n"], &[b"", b""]),
MergeResult::Resolved(b"".to_vec()) MergeResult::Resolved(b"".to_vec())
); );
// One side modified, one side unchanged
assert_eq!( assert_eq!(
merge(&[b"a"], &[b"a b", b"a"]), merge(&[b"a"], &[b"a b", b"a"]),
MergeResult::Resolved(b"a b".to_vec()) MergeResult::Resolved(b"a b".to_vec())
); );
// One side unchanged, one side modified
assert_eq!( assert_eq!(
merge(&[b"a"], &[b"a", b"a b"]), merge(&[b"a"], &[b"a", b"a b"]),
MergeResult::Resolved(b"a b".to_vec()) MergeResult::Resolved(b"a b".to_vec())
); );
// All sides added same content
assert_eq!(
merge(&[], &[b"a\n", b"a\n", b"a\n"]),
MergeResult::Resolved(b"a\n".to_vec())
);
// One side modified, two sides added
assert_eq!(
merge(&[b"a"], &[b"b", b"b", b"b"]),
MergeResult::Conflict(vec![MergeHunk::Conflict {
removes: vec![b"a".to_vec()],
adds: vec![b"b".to_vec(), b"b".to_vec(), b"b".to_vec()]
}])
);
// All sides removed same content
assert_eq!(
merge(&[b"a\n", b"a\n", b"a\n"], &[]),
MergeResult::Resolved(b"".to_vec())
);
// One side modified, two sides removed
assert_eq!(
merge(&[b"a\n", b"a\n", b"a\n"], &[b""]),
MergeResult::Conflict(vec![MergeHunk::Conflict {
removes: vec![b"a\n".to_vec(), b"a\n".to_vec(), b"a\n".to_vec()],
adds: vec![b"".to_vec()]
}])
);
// Three sides made the same change
assert_eq!(
merge(&[b"a", b"a"], &[b"b", b"b", b"b"]),
MergeResult::Resolved(b"b".to_vec())
);
// One side unchanged, one side added
assert_eq!(
merge(&[b"a\n"], &[b"a\nb\n"]),
MergeResult::Conflict(vec![MergeHunk::Conflict {
removes: vec![b"".to_vec()],
adds: vec![b"b\n".to_vec()]
}])
);
// Two sides left one line unchanged, and added conflicting additional lines
assert_eq!( assert_eq!(
merge(&[b"a\n"], &[b"a\nb\n", b"a\nc\n"]), merge(&[b"a\n"], &[b"a\nb\n", b"a\nc\n"]),
MergeResult::Conflict(vec![ MergeResult::Conflict(vec![
@ -339,28 +397,23 @@ mod tests {
} }
]) ])
); );
// One side removed, one side modified
assert_eq!( assert_eq!(
merge(&[b"a"], &[b"b", b"a"]), merge(&[b"a\n"], &[b"", b"b\n"]),
MergeResult::Resolved(b"b".to_vec())
);
assert_eq!(
merge(&[b"a"], &[b"a", b"b"]),
MergeResult::Resolved(b"b".to_vec())
);
assert_eq!(
merge(&[b"a"], &[b"", b"b"]),
MergeResult::Conflict(vec![MergeHunk::Conflict { MergeResult::Conflict(vec![MergeHunk::Conflict {
removes: vec![b"a".to_vec()], removes: vec![b"a\n".to_vec()],
adds: vec![b"".to_vec(), b"b".to_vec()] adds: vec![b"".to_vec(), b"b\n".to_vec()]
}]) }])
); );
// One side modified, one side removed
assert_eq!( assert_eq!(
merge(&[b"a"], &[b"b", b""]), merge(&[b"a\n"], &[b"b\n", b""]),
MergeResult::Conflict(vec![MergeHunk::Conflict { MergeResult::Conflict(vec![MergeHunk::Conflict {
removes: vec![b"a".to_vec()], removes: vec![b"a\n".to_vec()],
adds: vec![b"b".to_vec(), b"".to_vec()] adds: vec![b"b\n".to_vec(), b"".to_vec()]
}]) }])
); );
// Two sides modified in different ways
assert_eq!( assert_eq!(
merge(&[b"a"], &[b"b", b"c"]), merge(&[b"a"], &[b"b", b"c"]),
MergeResult::Conflict(vec![MergeHunk::Conflict { MergeResult::Conflict(vec![MergeHunk::Conflict {
@ -368,7 +421,7 @@ mod tests {
adds: vec![b"b".to_vec(), b"c".to_vec()] adds: vec![b"b".to_vec(), b"c".to_vec()]
}]) }])
); );
// Two of three sides don't change // Two of three sides don't change, third side changes
assert_eq!( assert_eq!(
merge(&[b"a", b"a"], &[b"a", b"", b"a"]), merge(&[b"a", b"a"], &[b"a", b"", b"a"]),
MergeResult::Resolved(b"".to_vec()) MergeResult::Resolved(b"".to_vec())