Previously when RecursiveMerger was trying to create a single virtual common base for the merge it was failing when this lead to content-merge conflicts. This is different from what native git is doing. When native git's recursive merge algorithm creates a new common base it will merge the multiple parents and simply take the merge result (potentially including conflict markers) as common base. See my discussion with Shawn here: http://www.spinics.net/lists/git/msg234959.html : > - How should workingtree, index (stage1,2,3) look like if during that > merge of common ancestors a conflict occurs? Will I see in stage2 and > stage3 really see content of X1 and X2? Its done entirely in memory and never touches the working tree or index. When a conflict exists in the X1-X2 merge the conflict is preserved into the new virtual base. There is still the possibility that the merge of parents lead to conflicts. File/Folder conclicts, conflicts on filemodes. This commit only fixes the situation for conflicts when merging content. Bug: 438203 Change-Id: If45bc3d078b3d3de87b758e71d7379059d709603tags/v3.5.0.201409071800-rc1
} | } | ||||
} | } | ||||
@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). | |||||
* | |||||
* <pre> | |||||
* m0--m1--m2 | |||||
* \ \/ | |||||
* \ /\ | |||||
* s1--s2 | |||||
* </pre> | |||||
*/ | |||||
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 | @Theory | ||||
/** | /** | ||||
* Merging m2,s2 from the following topology. The same file is modified | * Merging m2,s2 from the following topology. The same file is modified |
public boolean containsConflicts() { | public boolean containsConflicts() { | ||||
return 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; | |||||
} | |||||
} | } |
if (mergeTrees( | if (mergeTrees( | ||||
openTree(getBaseCommit(currentBase, nextBase, | openTree(getBaseCommit(currentBase, nextBase, | ||||
callDepth + 1).getTree()), | callDepth + 1).getTree()), | ||||
currentBase.getTree(), | |||||
nextBase.getTree())) | |||||
currentBase.getTree(), nextBase.getTree(), true)) | |||||
currentBase = createCommitForTree(resultTree, parents); | currentBase = createCommitForTree(resultTree, parents); | ||||
else | else | ||||
throw new NoMergeBaseException( | throw new NoMergeBaseException( |
dircache = getRepository().lockDirCache(); | dircache = getRepository().lockDirCache(); | ||||
try { | try { | ||||
return mergeTrees(mergeBase(), sourceTrees[0], sourceTrees[1]); | |||||
return mergeTrees(mergeBase(), sourceTrees[0], sourceTrees[1], | |||||
false); | |||||
} finally { | } finally { | ||||
if (implicitDirCache) | if (implicitDirCache) | ||||
dircache.unlock(); | dircache.unlock(); | ||||
* the index entry | * the index entry | ||||
* @param work | * @param work | ||||
* the file in the working tree | * the file in the working tree | ||||
* @param ignoreConflicts | |||||
* see | |||||
* {@link ResolveMerger#mergeTrees(AbstractTreeIterator, RevTree, RevTree, boolean)} | |||||
* @return <code>false</code> if the merge will fail because the index entry | * @return <code>false</code> if the merge will fail because the index entry | ||||
* didn't match ours or the working-dir file was dirty and a | * didn't match ours or the working-dir file was dirty and a | ||||
* conflict occurred | * conflict occurred | ||||
*/ | */ | ||||
protected boolean processEntry(CanonicalTreeParser base, | protected boolean processEntry(CanonicalTreeParser base, | ||||
CanonicalTreeParser ours, CanonicalTreeParser theirs, | CanonicalTreeParser ours, CanonicalTreeParser theirs, | ||||
DirCacheBuildIterator index, WorkingTreeIterator work) | |||||
DirCacheBuildIterator index, WorkingTreeIterator work, | |||||
boolean ignoreConflicts) | |||||
throws MissingObjectException, IncorrectObjectTypeException, | throws MissingObjectException, IncorrectObjectTypeException, | ||||
CorruptObjectException, IOException { | CorruptObjectException, IOException { | ||||
enterSubtree = true; | enterSubtree = true; | ||||
} | } | ||||
MergeResult<RawText> result = contentMerge(base, ours, theirs); | MergeResult<RawText> result = contentMerge(base, ours, theirs); | ||||
if (ignoreConflicts) | |||||
result.setContainsConflicts(false); | |||||
File of = writeMergedFile(result); | File of = writeMergedFile(result); | ||||
updateIndex(base, ours, theirs, result, of); | updateIndex(base, ours, theirs, result, of); | ||||
if (result.containsConflicts()) | |||||
if (result.containsConflicts() && !ignoreConflicts) | |||||
unmergedPaths.add(tw.getPathString()); | unmergedPaths.add(tw.getPathString()); | ||||
modifiedFiles.add(tw.getPathString()); | modifiedFiles.add(tw.getPathString()); | ||||
} else if (modeO != modeT) { | } else if (modeO != modeT) { | ||||
* @param baseTree | * @param baseTree | ||||
* @param headTree | * @param headTree | ||||
* @param mergeTree | * @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 | |||||
* <code>false</code>. 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. | |||||
* <p> | |||||
* If <code>true</code> 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 | * @return whether the trees merged cleanly | ||||
* @throws IOException | * @throws IOException | ||||
* @since 3.0 | * @since 3.0 | ||||
*/ | */ | ||||
protected boolean mergeTrees(AbstractTreeIterator baseTree, | protected boolean mergeTrees(AbstractTreeIterator baseTree, | ||||
RevTree headTree, RevTree mergeTree) throws IOException { | |||||
RevTree headTree, RevTree mergeTree, boolean ignoreConflicts) | |||||
throws IOException { | |||||
builder = dircache.builder(); | builder = dircache.builder(); | ||||
DirCacheBuildIterator buildIt = new DirCacheBuildIterator(builder); | DirCacheBuildIterator buildIt = new DirCacheBuildIterator(builder); | ||||
if (workingTreeIterator != null) | if (workingTreeIterator != null) | ||||
tw.addTree(workingTreeIterator); | tw.addTree(workingTreeIterator); | ||||
if (!mergeTreeWalk(tw)) { | |||||
if (!mergeTreeWalk(tw, ignoreConflicts)) { | |||||
return false; | return false; | ||||
} | } | ||||
* | * | ||||
* @param treeWalk | * @param treeWalk | ||||
* The walk to iterate over. | * The walk to iterate over. | ||||
* @param ignoreConflicts | |||||
* see | |||||
* {@link ResolveMerger#mergeTrees(AbstractTreeIterator, RevTree, RevTree, boolean)} | |||||
* @return Whether the trees merged cleanly. | * @return Whether the trees merged cleanly. | ||||
* @throws IOException | * @throws IOException | ||||
* @since 3.4 | * @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; | boolean hasWorkingTreeIterator = tw.getTreeCount() > T_FILE; | ||||
while (treeWalk.next()) { | while (treeWalk.next()) { | ||||
if (!processEntry( | if (!processEntry( | ||||
treeWalk.getTree(T_THEIRS, CanonicalTreeParser.class), | treeWalk.getTree(T_THEIRS, CanonicalTreeParser.class), | ||||
treeWalk.getTree(T_INDEX, DirCacheBuildIterator.class), | treeWalk.getTree(T_INDEX, DirCacheBuildIterator.class), | ||||
hasWorkingTreeIterator ? treeWalk.getTree(T_FILE, | hasWorkingTreeIterator ? treeWalk.getTree(T_FILE, | ||||
WorkingTreeIterator.class) : null)) { | |||||
WorkingTreeIterator.class) : null, ignoreConflicts)) { | |||||
cleanUp(); | cleanUp(); | ||||
return false; | return false; | ||||
} | } |