diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/RecursiveMergerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/RecursiveMergerTest.java index 4e39daaf4..37cc88be1 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/RecursiveMergerTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/RecursiveMergerTest.java @@ -343,6 +343,89 @@ else if (worktreeState == WorktreeState.DifferentFromHeadAndOther } } + @Theory + /** + * Merging m2,s2 from the following topology. m1 and s1 are not mergeable + * without conflicts. The same file is modified in both branches. The + * modifications should be mergeable but only if the merge result of + * merging m1 and s1 is choosen as parent (including the conflict markers). + * + *
+	 * m0--m1--m2
+	 *   \   \/
+	 *    \  /\
+	 *     s1--s2
+	 * 
+ */ + public void crissCrossMerge_ParentsNotMergeable(MergeStrategy strategy, + IndexState indexState, WorktreeState worktreeState) + throws Exception { + if (!validateStates(indexState, worktreeState)) + return; + + BranchBuilder master = db_t.branch("master"); + RevCommit m0 = master.commit().add("f", "1\n2\n3\n").message("m0") + .create(); + RevCommit m1 = master.commit().add("f", "1\nx(master)\n2\n3\n") + .message("m1").create(); + db_t.getRevWalk().parseCommit(m1); + + BranchBuilder side = db_t.branch("side"); + RevCommit s1 = side.commit().parent(m0) + .add("f", "1\nx(side)\n2\n3\ny(side)\n") + .message("s1").create(); + RevCommit s2 = side.commit().parent(m1) + .add("f", "1\nx(side)\n2\n3\ny(side-again)\n") + .message("s2(merge)") + .create(); + RevCommit m2 = master.commit().parent(s1) + .add("f", "1\nx(side)\n2\n3\ny(side)\n").message("m2(merge)") + .create(); + + Git git = Git.wrap(db); + git.checkout().setName("master").call(); + modifyWorktree(worktreeState, "f", "side"); + modifyIndex(indexState, "f", "side"); + + ResolveMerger merger = (ResolveMerger) strategy.newMerger(db, + worktreeState == WorktreeState.Bare); + if (worktreeState != WorktreeState.Bare) + merger.setWorkingTreeIterator(new FileTreeIterator(db)); + try { + boolean expectSuccess = true; + if (!(indexState == IndexState.Bare + || indexState == IndexState.Missing || indexState == IndexState.SameAsHead)) + // index is dirty + expectSuccess = false; + else if (worktreeState == WorktreeState.DifferentFromHeadAndOther + || worktreeState == WorktreeState.SameAsOther) + expectSuccess = false; + assertEquals("Merge didn't return as expected: strategy:" + + strategy.getName() + ", indexState:" + indexState + + ", worktreeState:" + worktreeState + " . ", + Boolean.valueOf(expectSuccess), + Boolean.valueOf(merger.merge(new RevCommit[] { m2, s2 }))); + assertEquals(MergeStrategy.RECURSIVE, strategy); + if (!expectSuccess) + // if the merge was not successful skip testing the state of + // index and workingtree + return; + assertEquals("1\nx(side)\n2\n3\ny(side-again)", + contentAsString(db, merger.getResultTreeId(), "f")); + if (indexState != IndexState.Bare) + assertEquals( + "[f, mode:100644, content:1\nx(side)\n2\n3\ny(side-again)\n]", + indexState(RepositoryTestCase.CONTENT)); + if (worktreeState != WorktreeState.Bare + && worktreeState != WorktreeState.Missing) + assertEquals("1\nx(side)\n2\n3\ny(side-again)\n", read("f")); + } catch (NoMergeBaseException e) { + assertEquals(MergeStrategy.RESOLVE, strategy); + assertEquals(e.getReason(), + MergeBaseFailureReason.MULTIPLE_MERGE_BASES_NOT_SUPPORTED); + } + } + @Theory /** * Merging m2,s2 from the following topology. The same file is modified diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeResult.java index 2dd4426e1..bac2b4fa7 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeResult.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeResult.java @@ -161,4 +161,16 @@ public void remove() { public boolean containsConflicts() { return containsConflicts; } + + /** + * Sets explicitly whether this merge should be seen as containing a + * conflict or not. Needed because during RecursiveMerger we want to do + * content-merges and take the resulting content (even with conflict + * markers!) as new conflict-free content + * + * @param containsConflicts + */ + protected void setContainsConflicts(boolean containsConflicts) { + this.containsConflicts = containsConflicts; + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java index 885d88e5c..af6c1f964 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/RecursiveMerger.java @@ -196,8 +196,7 @@ protected RevCommit getBaseCommit(RevCommit a, RevCommit b, int callDepth) if (mergeTrees( openTree(getBaseCommit(currentBase, nextBase, callDepth + 1).getTree()), - currentBase.getTree(), - nextBase.getTree())) + currentBase.getTree(), nextBase.getTree(), true)) currentBase = createCommitForTree(resultTree, parents); else throw new NoMergeBaseException( 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 28d42a616..d9cd50def 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java @@ -297,7 +297,8 @@ protected boolean mergeImpl() throws IOException { dircache = getRepository().lockDirCache(); try { - return mergeTrees(mergeBase(), sourceTrees[0], sourceTrees[1]); + return mergeTrees(mergeBase(), sourceTrees[0], sourceTrees[1], + false); } finally { if (implicitDirCache) dircache.unlock(); @@ -457,6 +458,9 @@ private DirCacheEntry keep(DirCacheEntry e) { * the index entry * @param work * the file in the working tree + * @param ignoreConflicts + * see + * {@link ResolveMerger#mergeTrees(AbstractTreeIterator, RevTree, RevTree, boolean)} * @return false if the merge will fail because the index entry * didn't match ours or the working-dir file was dirty and a * conflict occurred @@ -468,7 +472,8 @@ private DirCacheEntry keep(DirCacheEntry e) { */ protected boolean processEntry(CanonicalTreeParser base, CanonicalTreeParser ours, CanonicalTreeParser theirs, - DirCacheBuildIterator index, WorkingTreeIterator work) + DirCacheBuildIterator index, WorkingTreeIterator work, + boolean ignoreConflicts) throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { enterSubtree = true; @@ -627,9 +632,11 @@ protected boolean processEntry(CanonicalTreeParser base, } MergeResult result = contentMerge(base, ours, theirs); + if (ignoreConflicts) + result.setContainsConflicts(false); File of = writeMergedFile(result); updateIndex(base, ours, theirs, result, of); - if (result.containsConflicts()) + if (result.containsConflicts() && !ignoreConflicts) unmergedPaths.add(tw.getPathString()); modifiedFiles.add(tw.getPathString()); } else if (modeO != modeT) { @@ -993,12 +1000,32 @@ public void setWorkingTreeIterator(WorkingTreeIterator workingTreeIterator) { * @param baseTree * @param headTree * @param mergeTree + * @param ignoreConflicts + * Controls what to do in case a content-merge is done and a + * conflict is detected. The default setting for this should be + * false. In this case the working tree file is + * filled with new content (containing conflict markers) and the + * index is filled with multiple stages containing BASE, OURS and + * THEIRS content. Having such non-0 stages is the sign to git + * tools that there are still conflicts for that path. + *

+ * If true is specified the behavior is different. + * In case a conflict is detected the working tree file is again + * filled with new content (containing conflict markers). But + * also stage 0 of the index is filled with that content. No + * other stages are filled. Means: there is no conflict on that + * path but the new content (including conflict markers) is + * stored as successful merge result. This is needed in the + * context of {@link RecursiveMerger} where when determining + * merge bases we don't want to deal with content-merge + * conflicts. * @return whether the trees merged cleanly * @throws IOException * @since 3.0 */ protected boolean mergeTrees(AbstractTreeIterator baseTree, - RevTree headTree, RevTree mergeTree) throws IOException { + RevTree headTree, RevTree mergeTree, boolean ignoreConflicts) + throws IOException { builder = dircache.builder(); DirCacheBuildIterator buildIt = new DirCacheBuildIterator(builder); @@ -1011,7 +1038,7 @@ protected boolean mergeTrees(AbstractTreeIterator baseTree, if (workingTreeIterator != null) tw.addTree(workingTreeIterator); - if (!mergeTreeWalk(tw)) { + if (!mergeTreeWalk(tw, ignoreConflicts)) { return false; } @@ -1050,11 +1077,15 @@ protected boolean mergeTrees(AbstractTreeIterator baseTree, * * @param treeWalk * The walk to iterate over. + * @param ignoreConflicts + * see + * {@link ResolveMerger#mergeTrees(AbstractTreeIterator, RevTree, RevTree, boolean)} * @return Whether the trees merged cleanly. * @throws IOException * @since 3.4 */ - protected boolean mergeTreeWalk(TreeWalk treeWalk) throws IOException { + protected boolean mergeTreeWalk(TreeWalk treeWalk, boolean ignoreConflicts) + throws IOException { boolean hasWorkingTreeIterator = tw.getTreeCount() > T_FILE; while (treeWalk.next()) { if (!processEntry( @@ -1063,7 +1094,7 @@ protected boolean mergeTreeWalk(TreeWalk treeWalk) throws IOException { treeWalk.getTree(T_THEIRS, CanonicalTreeParser.class), treeWalk.getTree(T_INDEX, DirCacheBuildIterator.class), hasWorkingTreeIterator ? treeWalk.getTree(T_FILE, - WorkingTreeIterator.class) : null)) { + WorkingTreeIterator.class) : null, ignoreConflicts)) { cleanUp(); return false; }