@@ -397,33 +397,4 @@ public abstract class RepositoryTestCase extends LocalDiskRepositoryTestCase { | |||
RefUpdate refUpdate = db.updateRef(Constants.HEAD); | |||
refUpdate.link(branchName); | |||
} | |||
/** | |||
* Writes a number of files in the working tree. The first content specified | |||
* will be written into a file named '0', the second into a file named "1" | |||
* and so on. If <code>null</code> is specified as content then this file is | |||
* skipped. | |||
* | |||
* @param ensureDistinctTimestamps | |||
* if set to <code>true</code> then between two write operations | |||
* this method will wait to ensure that the second file will get | |||
* a different lastmodification timestamp than the first file. | |||
* @param contents | |||
* the contents which should be written into the files | |||
* @return the File object associated to the last written file. | |||
* @throws IOException | |||
* @throws InterruptedException | |||
*/ | |||
protected File writeTrashFiles(boolean ensureDistinctTimestamps, | |||
String... contents) | |||
throws IOException, InterruptedException { | |||
File f = null; | |||
for (int i = 0; i < contents.length; i++) | |||
if (contents[i] != null) { | |||
if (ensureDistinctTimestamps && (f != null)) | |||
fsTick(f); | |||
f = writeTrashFile(Integer.toString(i), contents[i]); | |||
} | |||
return f; | |||
} | |||
} |
@@ -42,18 +42,12 @@ | |||
*/ | |||
package org.eclipse.jgit.merge; | |||
import static org.junit.Assert.*; | |||
import static org.junit.Assert.assertFalse; | |||
import java.io.File; | |||
import java.io.FileInputStream; | |||
import java.io.IOException; | |||
import org.eclipse.jgit.api.Git; | |||
import org.eclipse.jgit.api.MergeResult; | |||
import org.eclipse.jgit.api.MergeResult.MergeStatus; | |||
import org.eclipse.jgit.dircache.DirCache; | |||
import org.eclipse.jgit.lib.RepositoryTestCase; | |||
import org.eclipse.jgit.merge.ResolveMerger.MergeFailureReason; | |||
import org.eclipse.jgit.revwalk.RevCommit; | |||
import org.eclipse.jgit.treewalk.FileTreeIterator; | |||
import org.eclipse.jgit.util.FileUtils; | |||
@@ -101,174 +95,4 @@ public class ResolveMergerTest extends RepositoryTestCase { | |||
assertFalse(ok); | |||
} | |||
@Test | |||
public void checkLockedFilesToBeDeleted() throws Exception { | |||
Git git = Git.wrap(db); | |||
writeTrashFile("a.txt", "orig"); | |||
writeTrashFile("b.txt", "orig"); | |||
git.add().addFilepattern("a.txt").addFilepattern("b.txt").call(); | |||
RevCommit first = git.commit().setMessage("added a.txt, b.txt").call(); | |||
// modify and delete files on the master branch | |||
writeTrashFile("a.txt", "master"); | |||
git.rm().addFilepattern("b.txt").call(); | |||
RevCommit masterCommit = git.commit() | |||
.setMessage("modified a.txt, deleted b.txt").setAll(true) | |||
.call(); | |||
// switch back to a side branch | |||
git.checkout().setCreateBranch(true).setStartPoint(first) | |||
.setName("side").call(); | |||
writeTrashFile("c.txt", "side"); | |||
git.add().addFilepattern("c.txt").call(); | |||
git.commit().setMessage("added c.txt").call(); | |||
// Get a handle to the the file so on windows it can't be deleted. | |||
FileInputStream fis = new FileInputStream(new File(db.getWorkTree(), | |||
"b.txt")); | |||
MergeResult mergeRes = git.merge().include(masterCommit).call(); | |||
if (mergeRes.getMergeStatus().equals(MergeStatus.FAILED)) { | |||
// probably windows | |||
assertEquals(1, mergeRes.getFailingPaths().size()); | |||
assertEquals(MergeFailureReason.COULD_NOT_DELETE, mergeRes | |||
.getFailingPaths().get("b.txt")); | |||
} | |||
assertEquals("[a.txt, mode:100644, content:master]" | |||
+ "[c.txt, mode:100644, content:side]", indexState(CONTENT)); | |||
fis.close(); | |||
} | |||
@Test | |||
public void checkForCorrectIndex() throws Exception { | |||
File f; | |||
long lastTs4, lastTsIndex; | |||
Git git = Git.wrap(db); | |||
File indexFile = db.getIndexFile(); | |||
// Create initial content and remember when the last file was written. | |||
f = writeTrashFiles(false, "orig", "orig", "1\n2\n3", "orig", "orig"); | |||
lastTs4 = f.lastModified(); | |||
// add all files, commit and check this doesn't update any working tree | |||
// files and that the index is in a new file system timer tick. Make | |||
// sure to wait long enough before adding so the index doesn't contain | |||
// racily clean entries | |||
fsTick(f); | |||
git.add().addFilepattern(".").call(); | |||
RevCommit firstCommit = git.commit().setMessage("initial commit") | |||
.call(); | |||
checkConsistentLastModified("0", "1", "2", "3", "4"); | |||
checkModificationTimeStampOrder("1", "2", "3", "4", "<.git/index"); | |||
assertEquals("Commit should not touch working tree file 4", lastTs4, | |||
new File(db.getWorkTree(), "4").lastModified()); | |||
lastTsIndex = indexFile.lastModified(); | |||
// Do modifications on the master branch. Then add and commit. This | |||
// should touch only "0", "2 and "3" | |||
fsTick(indexFile); | |||
f = writeTrashFiles(false, "master", null, "1master\n2\n3", "master", | |||
null); | |||
fsTick(f); | |||
git.add().addFilepattern(".").call(); | |||
RevCommit masterCommit = git.commit().setMessage("master commit") | |||
.call(); | |||
checkConsistentLastModified("0", "1", "2", "3", "4"); | |||
checkModificationTimeStampOrder("1", "4", "*" + lastTs4, "<*" | |||
+ lastTsIndex, "<0", | |||
"2", "3", "<.git/index"); | |||
lastTsIndex = indexFile.lastModified(); | |||
// Checkout a side branch. This should touch only "0", "2 and "3" | |||
fsTick(indexFile); | |||
git.checkout().setCreateBranch(true).setStartPoint(firstCommit) | |||
.setName("side").call(); | |||
checkConsistentLastModified("0", "1", "2", "3", "4"); | |||
checkModificationTimeStampOrder("1", "4", "*" + lastTs4, "<*" | |||
+ lastTsIndex, "<0", | |||
"2", "3", ".git/index"); | |||
lastTsIndex = indexFile.lastModified(); | |||
// This checkout may have populated worktree and index so fast that we | |||
// may have smudged entries now. Check that we have the right content | |||
// and then rewrite the index to get rid of smudged state | |||
assertEquals("[0, mode:100644, content:orig]" // | |||
+ "[1, mode:100644, content:orig]" // | |||
+ "[2, mode:100644, content:1\n2\n3]" // | |||
+ "[3, mode:100644, content:orig]" // | |||
+ "[4, mode:100644, content:orig]", // | |||
indexState(CONTENT)); | |||
fsTick(indexFile); | |||
f = writeTrashFiles(false, "orig", "orig", "1\n2\n3", "orig", "orig"); | |||
lastTs4 = f.lastModified(); | |||
fsTick(f); | |||
git.add().addFilepattern(".").call(); | |||
checkConsistentLastModified("0", "1", "2", "3", "4"); | |||
checkModificationTimeStampOrder("*" + lastTsIndex, "<0", "1", "2", "3", | |||
"4", "<.git/index"); | |||
lastTsIndex = indexFile.lastModified(); | |||
// Do modifications on the side branch. Touch only "1", "2 and "3" | |||
fsTick(indexFile); | |||
f = writeTrashFiles(false, null, "side", "1\n2\n3side", "side", null); | |||
fsTick(f); | |||
git.add().addFilepattern(".").call(); | |||
git.commit().setMessage("side commit").call(); | |||
checkConsistentLastModified("0", "1", "2", "3", "4"); | |||
checkModificationTimeStampOrder("0", "4", "*" + lastTs4, "<*" | |||
+ lastTsIndex, "<1", "2", "3", "<.git/index"); | |||
lastTsIndex = indexFile.lastModified(); | |||
// merge master and side. Should only touch "0," "2" and "3" | |||
fsTick(indexFile); | |||
git.merge().include(masterCommit).call(); | |||
checkConsistentLastModified("0", "1", "2", "4"); | |||
checkModificationTimeStampOrder("4", "*" + lastTs4, "<1", "<*" | |||
+ lastTsIndex, "<0", "2", "3", ".git/index"); | |||
assertEquals( | |||
"[0, mode:100644, content:master]" // | |||
+ "[1, mode:100644, content:side]" // | |||
+ "[2, mode:100644, content:1master\n2\n3side\n]" // | |||
+ "[3, mode:100644, stage:1, content:orig][3, mode:100644, stage:2, content:side][3, mode:100644, stage:3, content:master]" // | |||
+ "[4, mode:100644, content:orig]", // | |||
indexState(CONTENT)); | |||
} | |||
// Assert that every specified index entry has the same last modification | |||
// timestamp as the associated file | |||
private void checkConsistentLastModified(String... pathes) | |||
throws IOException { | |||
DirCache dc = db.readDirCache(); | |||
File workTree = db.getWorkTree(); | |||
for (String path : pathes) | |||
assertEquals( | |||
"IndexEntry with path " | |||
+ path | |||
+ " has lastmodified with is different from the worktree file", | |||
new File(workTree, path).lastModified(), dc.getEntry(path) | |||
.getLastModified()); | |||
} | |||
// Assert that modification timestamps of working tree files are as | |||
// expected. You may specify n files. It is asserted that every file | |||
// i+1 is not older than file i. If a path of file i+1 is prefixed with "<" | |||
// then this file must be younger then file i. A path "*<modtime>" | |||
// represents a file with a modification time of <modtime> | |||
// E.g. ("a", "b", "<c", "f/a.txt") means: a<=b<c<=f/a.txt | |||
private void checkModificationTimeStampOrder(String... pathes) { | |||
long lastMod = Long.MIN_VALUE; | |||
for (String p : pathes) { | |||
boolean strong = p.startsWith("<"); | |||
boolean fixed = p.charAt(strong ? 1 : 0) == '*'; | |||
p = p.substring((strong ? 1 : 0) + (fixed ? 1 : 0)); | |||
long curMod = fixed ? Long.valueOf(p).longValue() : new File( | |||
db.getWorkTree(), p).lastModified(); | |||
if (strong) | |||
assertTrue("path " + p + " is not younger than predecesssor", | |||
curMod > lastMod); | |||
else | |||
assertTrue("path " + p + " is older than predecesssor", | |||
curMod >= lastMod); | |||
} | |||
} | |||
} |
@@ -204,13 +204,6 @@ public class ResolveMerger extends ThreeWayMerger { | |||
} | |||
if (!inCore) { | |||
// No problem found. The only thing left to be done is to | |||
// checkout | |||
// all files from "theirs" which have been selected to go into | |||
// the | |||
// new index. | |||
checkout(); | |||
// All content-merges are successfully done. If we can now write the | |||
// new index we are on quite safe ground. Even if the checkout of | |||
// files coming from "theirs" fails the user can work around such | |||
@@ -221,6 +214,10 @@ public class ResolveMerger extends ThreeWayMerger { | |||
} | |||
builder = null; | |||
// No problem found. The only thing left to be done is to checkout | |||
// all files from "theirs" which have been selected to go into the | |||
// new index. | |||
checkout(); | |||
} else { | |||
builder.finish(); | |||
builder = null; | |||
@@ -316,43 +313,19 @@ public class ResolveMerger extends ThreeWayMerger { | |||
* @param path | |||
* @param p | |||
* @param stage | |||
* @param lastMod | |||
* @param len | |||
* @return the entry which was added to the index | |||
*/ | |||
private DirCacheEntry add(byte[] path, CanonicalTreeParser p, int stage, | |||
long lastMod, long len) { | |||
private DirCacheEntry add(byte[] path, CanonicalTreeParser p, int stage) { | |||
if (p != null && !p.getEntryFileMode().equals(FileMode.TREE)) { | |||
DirCacheEntry e = new DirCacheEntry(path, stage); | |||
e.setFileMode(p.getEntryFileMode()); | |||
e.setObjectId(p.getEntryObjectId()); | |||
e.setLastModified(lastMod); | |||
e.setLength(len); | |||
builder.add(e); | |||
return e; | |||
} | |||
return null; | |||
} | |||
/** | |||
* adds a entry to the index builder which is a copy of the specified | |||
* DirCacheEntry | |||
* | |||
* @param e | |||
* the entry which should be copied | |||
* | |||
* @return the entry which was added to the index | |||
*/ | |||
private DirCacheEntry keep(DirCacheEntry e) { | |||
DirCacheEntry newEntry = new DirCacheEntry(e.getPathString(), e.getStage()); | |||
newEntry.setFileMode(e.getFileMode()); | |||
newEntry.setObjectId(e.getObjectId()); | |||
newEntry.setLastModified(e.getLastModified()); | |||
newEntry.setLength(e.getLength()); | |||
builder.add(newEntry); | |||
return newEntry; | |||
} | |||
/** | |||
* Processes one path and tries to merge. This method will do all do all | |||
* trivial (not content) merges and will also detect if a merge will fail. | |||
@@ -409,27 +382,12 @@ public class ResolveMerger extends ThreeWayMerger { | |||
if (isIndexDirty()) | |||
return false; | |||
DirCacheEntry ourDce = null; | |||
if (index == null) { | |||
// create a fake DCE, but only if ours is valid. ours is kept only | |||
// in case it is valid, so a null ourDce is ok in all other cases. | |||
if (modeO != 0) { | |||
ourDce = new DirCacheEntry(tw.getRawPath()); | |||
ourDce.setObjectId(tw.getObjectId(T_OURS)); | |||
ourDce.setFileMode(tw.getFileMode(T_OURS)); | |||
} | |||
} else { | |||
ourDce = index.getDirCacheEntry(); | |||
} | |||
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. Since the index | |||
// is clean (the index matches already OURS) we can keep the existing one | |||
keep(ourDce); | |||
// matter which one we choose. OURS is chosen. | |||
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_0); | |||
// no checkout needed! | |||
return true; | |||
} else { | |||
@@ -440,25 +398,22 @@ public class ResolveMerger extends ThreeWayMerger { | |||
if (newMode != FileMode.MISSING.getBits()) { | |||
if (newMode == modeO) | |||
// ours version is preferred | |||
keep(ourDce); | |||
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_0); | |||
else { | |||
// the preferred version THEIRS has a different mode | |||
// than ours. Check it out! | |||
if (isWorktreeDirty(work)) | |||
return false; | |||
// we know about length and lastMod only after we have written the new content. | |||
// This will happen later. Set these values to 0 for know. | |||
DirCacheEntry e = add(tw.getRawPath(), theirs, | |||
DirCacheEntry.STAGE_0, 0, 0); | |||
DirCacheEntry.STAGE_0); | |||
toBeCheckedOut.put(tw.getPathString(), e); | |||
} | |||
return true; | |||
} else { | |||
// FileModes are not mergeable. We found a conflict on modes. | |||
// For conflicting entries we don't know lastModified and length. | |||
add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, 0, 0); | |||
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, 0, 0); | |||
add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, 0, 0); | |||
// 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(), | |||
@@ -471,8 +426,8 @@ public class ResolveMerger extends ThreeWayMerger { | |||
if (nonTree(modeO) && modeB == modeT && tw.idEqual(T_BASE, T_THEIRS)) { | |||
// THEIRS was not changed compared to BASE. All changes must be in | |||
// OURS. OURS is chosen. We can keep the existing entry. | |||
keep(ourDce); | |||
// OURS. OURS is chosen. | |||
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_0); | |||
// no checkout needed! | |||
return true; | |||
} | |||
@@ -485,11 +440,8 @@ public class ResolveMerger extends ThreeWayMerger { | |||
if (isWorktreeDirty(work)) | |||
return false; | |||
if (nonTree(modeT)) { | |||
// we know about length and lastMod only after we have written | |||
// the new content. | |||
// This will happen later. Set these values to 0 for know. | |||
DirCacheEntry e = add(tw.getRawPath(), theirs, | |||
DirCacheEntry.STAGE_0, 0, 0); | |||
DirCacheEntry.STAGE_0); | |||
if (e != null) | |||
toBeCheckedOut.put(tw.getPathString(), e); | |||
return true; | |||
@@ -508,16 +460,16 @@ public class ResolveMerger extends ThreeWayMerger { | |||
// detected later | |||
if (nonTree(modeO) && !nonTree(modeT)) { | |||
if (nonTree(modeB)) | |||
add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, 0, 0); | |||
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, 0, 0); | |||
add(tw.getRawPath(), base, DirCacheEntry.STAGE_1); | |||
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2); | |||
unmergedPaths.add(tw.getPathString()); | |||
enterSubtree = false; | |||
return true; | |||
} | |||
if (nonTree(modeT) && !nonTree(modeO)) { | |||
if (nonTree(modeB)) | |||
add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, 0, 0); | |||
add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, 0, 0); | |||
add(tw.getRawPath(), base, DirCacheEntry.STAGE_1); | |||
add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3); | |||
unmergedPaths.add(tw.getPathString()); | |||
enterSubtree = false; | |||
return true; | |||
@@ -550,10 +502,10 @@ public class ResolveMerger extends ThreeWayMerger { | |||
if (((modeO != 0 && !tw.idEqual(T_BASE, T_OURS)) || (modeT != 0 && !tw | |||
.idEqual(T_BASE, T_THEIRS)))) { | |||
add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, 0, 0); | |||
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, 0, 0); | |||
add(tw.getRawPath(), base, DirCacheEntry.STAGE_1); | |||
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2); | |||
DirCacheEntry e = add(tw.getRawPath(), theirs, | |||
DirCacheEntry.STAGE_3, 0, 0); | |||
DirCacheEntry.STAGE_3); | |||
// OURS was deleted checkout THEIRS | |||
if (modeO == 0) { | |||
@@ -657,9 +609,9 @@ public class ResolveMerger extends ThreeWayMerger { | |||
// a conflict occurred, the file will contain conflict markers | |||
// the index will be populated with the three stages and only the | |||
// workdir (if used) contains the halfways merged content | |||
add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, 0, 0); | |||
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, 0, 0); | |||
add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, 0, 0); | |||
add(tw.getRawPath(), base, DirCacheEntry.STAGE_1); | |||
add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2); | |||
add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3); | |||
mergeResults.put(tw.getPathString(), result); | |||
} else { | |||
// no conflict occurred, the file will contain fully merged content. |