Fix ResolveMerger not to add paths with FileMode 0

When ResolveMerger finds a path where it has to do a content merge it
will try the content merge and if that succeeds it'll add the newly
produced content to the index. For the FileMode of this new index entry
it blindly copies the FileMode it finds for that path in the common base
tree. If by chance the common base tree does not contain this path it'll
try to add FileMode 0 (MISSING) to the index.

One could argue that this can't happen: how can the ResolveMerger
successfully (with no conflicts) merge two contents if there is no
common base? This was due to another bug in ResolveMerger. It failed to
find out that for two files which differ only in the FileMode (e.g. 644
vs. 755) it should not try a content merge.

Change-Id: I7a00fe1a6c610679be475cab8a3f8aa4c08811a1
Signed-off-by: Christian Halstrick <christian.halstrick@sap.com>
Signed-off-by: Robin Rosenberg <robin.rosenberg@dewire.com>
This commit is contained in:
Christian Halstrick 2011-12-09 11:17:59 +01:00 committed by Robin Rosenberg
parent c1f352c100
commit 3c544647b7
2 changed files with 150 additions and 7 deletions

View File

@ -1023,6 +1023,81 @@ public void testMergeRemovingFolders() throws Exception {
assertFalse(folder2.exists());
}
@Test
public void testFileModeMerge() throws Exception {
Git git = new Git(db);
writeTrashFile("mergeableMode", "a");
setExecutable(git, "mergeableMode", false);
writeTrashFile("conflictingModeWithBase", "a");
setExecutable(git, "conflictingModeWithBase", false);
RevCommit initialCommit = addAllAndCommit(git);
// switch branch
createBranch(initialCommit, "refs/heads/side");
checkoutBranch("refs/heads/side");
setExecutable(git, "mergeableMode", true);
writeTrashFile("conflictingModeNoBase", "b");
setExecutable(git, "conflictingModeNoBase", true);
RevCommit sideCommit = addAllAndCommit(git);
// switch branch
createBranch(initialCommit, "refs/heads/side2");
checkoutBranch("refs/heads/side2");
setExecutable(git, "mergeableMode", false);
assertFalse(new File(git.getRepository().getWorkTree(),
"conflictingModeNoBase").exists());
writeTrashFile("conflictingModeNoBase", "b");
setExecutable(git, "conflictingModeNoBase", false);
addAllAndCommit(git);
// merge
MergeResult result = git.merge().include(sideCommit.getId())
.setStrategy(MergeStrategy.RESOLVE).call();
assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
assertTrue(canExecute(git, "mergeableMode"));
assertFalse(canExecute(git, "conflictingModeNoBase"));
}
@Test
public void testFileModeMergeWithDirtyWorkTree() throws Exception {
Git git = new Git(db);
writeTrashFile("mergeableButDirty", "a");
setExecutable(git, "mergeableButDirty", false);
RevCommit initialCommit = addAllAndCommit(git);
// switch branch
createBranch(initialCommit, "refs/heads/side");
checkoutBranch("refs/heads/side");
setExecutable(git, "mergeableButDirty", true);
RevCommit sideCommit = addAllAndCommit(git);
// switch branch
createBranch(initialCommit, "refs/heads/side2");
checkoutBranch("refs/heads/side2");
setExecutable(git, "mergeableButDirty", false);
addAllAndCommit(git);
writeTrashFile("mergeableButDirty", "b");
// merge
MergeResult result = git.merge().include(sideCommit.getId())
.setStrategy(MergeStrategy.RESOLVE).call();
assertEquals(MergeStatus.FAILED, result.getMergeStatus());
assertFalse(canExecute(git, "mergeableButDirty"));
}
private void setExecutable(Git git, String path, boolean executable) {
new File(git.getRepository().getWorkTree(), path)
.setExecutable(executable);
assertEquals(executable, canExecute(git, path));
}
private boolean canExecute(Git git, String path) {
return (new File(git.getRepository().getWorkTree(), path).canExecute());
}
private RevCommit addAllAndCommit(final Git git) throws Exception {
git.add().addFilepattern(".").call();
return git.commit().setMessage("message").call();

View File

@ -51,6 +51,7 @@
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
@ -378,12 +379,46 @@ private boolean processEntry(CanonicalTreeParser base,
if (isIndexDirty())
return false;
if (nonTree(modeO) && modeO == modeT && tw.idEqual(T_OURS, T_THEIRS)) {
// OURS and THEIRS are equal: it doesn't matter which one we choose.
// OURS is chosen.
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_0);
// no checkout needed!
return true;
if (nonTree(modeO) && nonTree(modeT) && tw.idEqual(T_OURS, T_THEIRS)) {
// OURS and THEIRS have equal content. Check the file mode
if (modeO == modeT) {
// content and mode of OURS and THEIRS are equal: it doesn't
// matter which one we choose. OURS is chosen.
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_0);
// no checkout needed!
return true;
} else {
// same content but different mode on OURS and THEIRS.
// Try to merge the mode and report an error if this is
// not possible.
int newMode = mergeFileModes(modeB, modeO, modeT);
if (newMode != FileMode.MISSING.getBits()) {
if (newMode == modeO)
// ours version is preferred
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_0);
else {
// the preferred version THEIRS has a different mode
// than ours. Check it out!
if (isWorktreeDirty())
return false;
DirCacheEntry e = add(tw.getRawPath(), theirs,
DirCacheEntry.STAGE_0);
toBeCheckedOut.put(tw.getPathString(), e);
}
return true;
} else {
// FileModes are not mergeable. We found a conflict on modes
add(tw.getRawPath(), base, DirCacheEntry.STAGE_1);
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2);
add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3);
unmergedPaths.add(tw.getPathString());
mergeResults.put(
tw.getPathString(),
new MergeResult<RawText>(Collections
.<RawText> emptyList()));
}
return true;
}
}
if (nonTree(modeO) && modeB == modeT && tw.idEqual(T_BASE, T_THEIRS)) {
@ -582,7 +617,12 @@ else if (!result.containsConflicts()) {
// no conflict occurred, the file will contain fully merged content.
// the index will be populated with the new merged version
DirCacheEntry dce = new DirCacheEntry(tw.getPathString());
dce.setFileMode(tw.getFileMode(0));
int newMode = mergeFileModes(tw.getRawMode(0), tw.getRawMode(1),
tw.getRawMode(2));
// set the mode for the new content. Fall back to REGULAR_FILE if
// you can't merge modes of OURS and THEIRS
dce.setFileMode((newMode == FileMode.MISSING.getBits()) ? FileMode.REGULAR_FILE
: FileMode.fromBits(newMode));
dce.setLastModified(of.lastModified());
dce.setLength((int) of.length());
InputStream is = new FileInputStream(of);
@ -599,6 +639,34 @@ else if (!result.containsConflicts()) {
}
}
/**
* Try to merge filemodes. If only ours or theirs have changed the mode
* (compared to base) we choose that one. If ours and theirs have equal
* modes return that one. If also that is not the case the modes are not
* mergeable. Return {@link FileMode#MISSING} int that case.
*
* @param modeB
* filemode found in BASE
* @param modeO
* filemode found in OURS
* @param modeT
* filemode found in THEIRS
*
* @return the merged filemode or {@link FileMode#MISSING} in case of a
* conflict
*/
private int mergeFileModes(int modeB, int modeO, int modeT) {
if (modeO == modeT)
return modeO;
if (modeB == modeO)
// Base equal to Ours -> chooses Theirs if that is not missing
return (modeT == FileMode.MISSING.getBits()) ? modeO : modeT;
if (modeB == modeT)
// Base equal to Theirs -> chooses Ours if that is not missing
return (modeO == FileMode.MISSING.getBits()) ? modeT : modeO;
return FileMode.MISSING.getBits();
}
private static RawText getRawText(ObjectId id, Repository db)
throws IOException {
if (id.equals(ObjectId.zeroId()))