Browse Source

Allow dir/file conflicts in virtual base commit on recursive merge.

If RecursiveMerger finds multiple base commits, it tries to compute
the virtual ancestor to use as a base for the three way merge.
Currently, the content conflicts between ancestors are ignored (file
staged with the conflict markers). If the path is a file in one ancestor
and a dir in the other, it results in NoMergeBaseException
(CONFLICTS_DURING_MERGE_BASE_CALCULATION).

Allow these conflicts by ignoring this unmerged path in the virtual
base. The merger will compute diff in the children instead and it
can be further fixed manually if needed.

Change-Id: Id59648ae1d6bdf300b26fff513c3204317b755ab
Signed-off-by: Marija Savtchouk <mariasavtchouk@google.com>
tags/v5.11.0.202102240950-m3
Marija Savtchouk 3 years ago
parent
commit
1b9911d9ae

+ 264
- 0
org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java View File

@@ -1384,6 +1384,270 @@ public class MergerTest extends RepositoryTestCase {
git.merge().include(commitB).call();
}

/**
* Merging two commits with a file/dir conflict in the virtual ancestor.
*
* <p>
* Those conflicts should be ignored, otherwise the found base can not be used by the
* RecursiveMerger.
* <pre>
* --------------
* | \
* | C1 - C4 --- ? master
* | / /
* | I - A1 - C2 - C3 second-branch
* | \ /
* \ \ /
* ----A2-------- branch-to-merge
* </pre>
* <p>
* <p>
* Path "a" is initially a file in I and A1. It is changed to a directory in A2
* ("branch-to-merge").
* <p>
* A2 is merged into "master" and "second-branch". The dir/file merge conflict is resolved
* manually, results in C4 and C3.
* <p>
* While merging C3 and C4, A1 and A2 are the base commits found by the recursive merge that
* have the dir/file conflict.
*/
@Theory
public void checkFileDirMergeConflictInVirtualAncestor_NoConflictInChildren(
MergeStrategy strategy)
throws Exception {
if (!strategy.equals(MergeStrategy.RECURSIVE)) {
return;
}

Git git = Git.wrap(db);

// master
writeTrashFile("a", "initial content");
git.add().addFilepattern("a").call();
RevCommit commitI = git.commit().setMessage("Initial commit").call();

writeTrashFile("a", "content in Ancestor 1");
git.add().addFilepattern("a").call();
RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call();

writeTrashFile("a", "content in Child 1 (commited on master)");
git.add().addFilepattern("a").call();
// commit C1M
git.commit().setMessage("Child 1 on master").call();

git.checkout().setCreateBranch(true).setStartPoint(commitI).setName("branch-to-merge").call();
// "a" becomes a directory in A2
git.rm().addFilepattern("a").call();
writeTrashFile("a/content", "content in Ancestor 2 (commited on branch-to-merge)");
git.add().addFilepattern("a/content").call();
RevCommit commitA2 = git.commit().setMessage("Ancestor 2").call();

// second branch
git.checkout().setCreateBranch(true).setStartPoint(commitA1).setName("second-branch").call();
writeTrashFile("a", "content in Child 2 (commited on second-branch)");
git.add().addFilepattern("a").call();
// commit C2S
git.commit().setMessage("Child 2 on second-branch").call();

// Merge branch-to-merge into second-branch
MergeResult mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
assertEquals(mergeResult.getNewHead(), null);
assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
// Resolve the conflict manually, merge "a" as a file
git.rm().addFilepattern("a").call();
git.rm().addFilepattern("a/content").call();
writeTrashFile("a", "merge conflict resolution");
git.add().addFilepattern("a").call();
RevCommit commitC3S = git.commit().setMessage("Child 3 on second bug - resolve merge conflict")
.call();

// Merge branch-to-merge into master
git.checkout().setName("master").call();
mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
assertEquals(mergeResult.getNewHead(), null);
assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);

// Resolve the conflict manually - merge "a" as a file
git.rm().addFilepattern("a").call();
git.rm().addFilepattern("a/content").call();
writeTrashFile("a", "merge conflict resolution");
git.add().addFilepattern("a").call();
// commit C4M
git.commit().setMessage("Child 4 on master - resolve merge conflict").call();

// Merge C4M (second-branch) into master (C3S)
// Conflict in virtual base should be here, but there are no conflicts in
// children
mergeResult = git.merge().include(commitC3S).call();
assertEquals(mergeResult.getMergeStatus(), MergeStatus.MERGED);

}

@Theory
public void checkFileDirMergeConflictInVirtualAncestor_ConflictInChildren_FileDir(MergeStrategy strategy)
throws Exception {
if (!strategy.equals(MergeStrategy.RECURSIVE)) {
return;
}

Git git = Git.wrap(db);

// master
writeTrashFile("a", "initial content");
git.add().addFilepattern("a").call();
RevCommit commitI = git.commit().setMessage("Initial commit").call();

writeTrashFile("a", "content in Ancestor 1");
git.add().addFilepattern("a").call();
RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call();

writeTrashFile("a", "content in Child 1 (commited on master)");
git.add().addFilepattern("a").call();
// commit C1M
git.commit().setMessage("Child 1 on master").call();

git.checkout().setCreateBranch(true).setStartPoint(commitI).setName("branch-to-merge").call();

// "a" becomes a directory in A2
git.rm().addFilepattern("a").call();
writeTrashFile("a/content", "content in Ancestor 2 (commited on branch-to-merge)");
git.add().addFilepattern("a/content").call();
RevCommit commitA2 = git.commit().setMessage("Ancestor 2").call();

// second branch
git.checkout().setCreateBranch(true).setStartPoint(commitA1).setName("second-branch").call();
writeTrashFile("a", "content in Child 2 (commited on second-branch)");
git.add().addFilepattern("a").call();
// commit C2S
git.commit().setMessage("Child 2 on second-branch").call();

// Merge branch-to-merge into second-branch
MergeResult mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
assertEquals(mergeResult.getNewHead(), null);
assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
// Resolve the conflict manually - write a file
git.rm().addFilepattern("a").call();
git.rm().addFilepattern("a/content").call();
writeTrashFile("a",
"content in Child 3 (commited on second-branch) - merge conflict resolution");
git.add().addFilepattern("a").call();
RevCommit commitC3S = git.commit().setMessage("Child 3 on second bug - resolve merge conflict")
.call();

// Merge branch-to-merge into master
git.checkout().setName("master").call();
mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
assertEquals(mergeResult.getNewHead(), null);
assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);

// Resolve the conflict manually - write a file
git.rm().addFilepattern("a").call();
git.rm().addFilepattern("a/content").call();
writeTrashFile("a", "content in Child 4 (commited on master) - merge conflict resolution");
git.add().addFilepattern("a").call();
// commit C4M
git.commit().setMessage("Child 4 on master - resolve merge conflict").call();

// Merge C4M (second-branch) into master (C3S)
// Conflict in virtual base should be here
mergeResult = git.merge().include(commitC3S).call();
assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
String expected =
"<<<<<<< HEAD\n" + "content in Child 4 (commited on master) - merge conflict resolution\n"
+ "=======\n"
+ "content in Child 3 (commited on second-branch) - merge conflict resolution\n"
+ ">>>>>>> " + commitC3S.name() + "\n";
assertEquals(expected, read("a"));
// Nothing was populated from the ancestors.
assertEquals(
"[a, mode:100644, stage:2, content:content in Child 4 (commited on master) - merge conflict resolution][a, mode:100644, stage:3, content:content in Child 3 (commited on second-branch) - merge conflict resolution]",
indexState(CONTENT));
}

/**
* Same test as above, but "a" is a dir in A1 and a file in A2
*/
@Theory
public void checkFileDirMergeConflictInVirtualAncestor_ConflictInChildren_DirFile(MergeStrategy strategy)
throws Exception {
if (!strategy.equals(MergeStrategy.RECURSIVE)) {
return;
}

Git git = Git.wrap(db);

// master
writeTrashFile("a/content", "initial content");
git.add().addFilepattern("a/content").call();
RevCommit commitI = git.commit().setMessage("Initial commit").call();

writeTrashFile("a/content", "content in Ancestor 1");
git.add().addFilepattern("a/content").call();
RevCommit commitA1 = git.commit().setMessage("Ancestor 1").call();

writeTrashFile("a/content", "content in Child 1 (commited on master)");
git.add().addFilepattern("a/content").call();
// commit C1M
git.commit().setMessage("Child 1 on master").call();

git.checkout().setCreateBranch(true).setStartPoint(commitI).setName("branch-to-merge").call();

// "a" becomes a file in A2
git.rm().addFilepattern("a/content").call();
writeTrashFile("a", "content in Ancestor 2 (commited on branch-to-merge)");
git.add().addFilepattern("a").call();
RevCommit commitA2 = git.commit().setMessage("Ancestor 2").call();

// second branch
git.checkout().setCreateBranch(true).setStartPoint(commitA1).setName("second-branch").call();
writeTrashFile("a/content", "content in Child 2 (commited on second-branch)");
git.add().addFilepattern("a/content").call();
// commit C2S
git.commit().setMessage("Child 2 on second-branch").call();

// Merge branch-to-merge into second-branch
MergeResult mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
assertEquals(mergeResult.getNewHead(), null);
assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
// Resolve the conflict manually - write a file
git.rm().addFilepattern("a").call();
git.rm().addFilepattern("a/content").call();
deleteTrashFile("a/content");
deleteTrashFile("a");
writeTrashFile("a", "content in Child 3 (commited on second-branch) - merge conflict resolution");
git.add().addFilepattern("a").call();
RevCommit commitC3S = git.commit().setMessage("Child 3 on second bug - resolve merge conflict").call();

// Merge branch-to-merge into master
git.checkout().setName("master").call();
mergeResult = git.merge().include(commitA2).setStrategy(strategy).call();
assertEquals(mergeResult.getNewHead(), null);
assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);

// Resolve the conflict manually - write a file
git.rm().addFilepattern("a").call();
git.rm().addFilepattern("a/content").call();
deleteTrashFile("a/content");
deleteTrashFile("a");
writeTrashFile("a", "content in Child 4 (commited on master) - merge conflict resolution");
git.add().addFilepattern("a").call();
// commit C4M
git.commit().setMessage("Child 4 on master - resolve merge conflict").call();

// Merge C4M (second-branch) into master (C3S)
// Conflict in virtual base should be here
mergeResult = git.merge().include(commitC3S).call();
assertEquals(mergeResult.getMergeStatus(), MergeStatus.CONFLICTING);
String expected = "<<<<<<< HEAD\n" + "content in Child 4 (commited on master) - merge conflict resolution\n"
+ "=======\n" + "content in Child 3 (commited on second-branch) - merge conflict resolution\n"
+ ">>>>>>> " + commitC3S.name() + "\n";
assertEquals(expected, read("a"));
// Nothing was populated from the ancestors.
assertEquals(
"[a, mode:100644, stage:2, content:content in Child 4 (commited on master) - merge conflict resolution][a, mode:100644, stage:3, content:content in Child 3 (commited on second-branch) - merge conflict resolution]",
indexState(CONTENT));
}

private void writeSubmodule(String path, ObjectId commit)
throws IOException, ConfigInvalidException {
addSubmoduleToIndex(path, commit);

+ 13
- 10
org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java View File

@@ -703,18 +703,21 @@ public class ResolveMerger extends ThreeWayMerger {
// conflict between ours and theirs. file/folder conflicts between
// base/index/workingTree and something else are not relevant or
// detected later
if (nonTree(modeO) && !nonTree(modeT)) {
if (nonTree(modeB))
add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0);
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0);
unmergedPaths.add(tw.getPathString());
enterSubtree = false;
return true;
}
if (nonTree(modeT) && !nonTree(modeO)) {
if (nonTree(modeO) != nonTree(modeT)) {
if (ignoreConflicts) {
// In case of merge failures, ignore this path instead of reporting unmerged, so
// a caller can use virtual commit. This will not result in files with conflict
// markers in the index/working tree. The actual diff on the path will be
// computed directly on children.
enterSubtree = false;
return true;
}
if (nonTree(modeB))
add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, EPOCH, 0);
add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0);
if (nonTree(modeO))
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, EPOCH, 0);
if (nonTree(modeT))
add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, EPOCH, 0);
unmergedPaths.add(tw.getPathString());
enterSubtree = false;
return true;

Loading…
Cancel
Save