summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java264
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java23
2 files changed, 277 insertions, 10 deletions
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java
index e2ac89be90..eecf25be90 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/MergerTest.java
@@ -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);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
index 6c217fdf25..4bfb38d286 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
@@ -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;