Fix a number of failing conflict situations

Adds further tests where the working tree is dirty (differs from
index) and where we have staged but uncommitted changes.

Fixed the test case 9 for file/directory conflicts.

Bug: 428819
Change-Id: Ie44a288b052abe936ebb74272d0fefef3b218a7a
Signed-off-by: Axel Richard <axel.richard@obeo.fr>
Signed-off-by: Robin Rosenberg <robin.rosenberg@dewire.com>
Signed-off-by: Christian Halstrick <christian.halstrick@sap.com>
This commit is contained in:
Robin Rosenberg 2014-03-30 22:04:53 +02:00
parent f7ac527ca7
commit 1a9f122773
5 changed files with 486 additions and 46 deletions

View File

@ -8,6 +8,8 @@ Bundle-Localization: plugin
Bundle-ActivationPolicy: lazy
Bundle-RequiredExecutionEnvironment: J2SE-1.5
Import-Package: org.eclipse.jgit.api;version="[3.4.0,3.5.0)",
org.eclipse.jgit.api.errors;version="[3.4.0,3.5.0)",
org.eclipse.jgit.diff;version="[3.4.0,3.5.0)",
org.eclipse.jgit.dircache;version="[3.4.0,3.5.0)",
org.eclipse.jgit.junit;version="[3.4.0,3.5.0)",
org.eclipse.jgit.lib;version="[3.4.0,3.5.0)",

View File

@ -43,15 +43,21 @@
package org.eclipse.jgit.pgm;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertNotNull;
import java.io.File;
import java.util.List;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.CheckoutConflictException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.lib.CLIRepositoryTestCase;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.FileTreeIterator.FileEntry;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.FileUtils;
import org.junit.Assert;
import org.junit.Test;
@ -150,6 +156,7 @@ public void testCheckoutExistingBranchWithConflict() throws Exception {
* <li>Delete file 'a' in the working tree
* <li>Checkout branch '1'
* </ol>
* <p>
* The working tree should contain 'a' with FileMode.REGULAR_FILE after the
* checkout.
*
@ -181,6 +188,359 @@ public void testCheckoutWithMissingWorkingTreeFile() throws Exception {
assertEquals("Hello world a", read(fileA));
}
/**
* Steps:
* <ol>
* <li>Add file 'b'
* <li>Commit
* <li>Create branch '1'
* <li>Add folder 'a'
* <li>Commit
* <li>Replace folder 'a' by file 'a' in the working tree
* <li>Checkout branch '1'
* </ol>
* <p>
* The working tree should contain 'a' with FileMode.REGULAR_FILE after the
* checkout.
*
* @throws Exception
*/
@Test
public void fileModeTestMissingThenFolderWithFileInWorkingTree()
throws Exception {
Git git = new Git(db);
writeTrashFile("b", "Hello world b");
git.add().addFilepattern(".").call();
git.commit().setMessage("add file b").call();
Ref branch_1 = git.branchCreate().setName("branch_1").call();
File folderA = new File(db.getWorkTree(), "a");
FileUtils.mkdirs(folderA);
writeTrashFile("a/c", "Hello world c");
git.add().addFilepattern(".").call();
git.commit().setMessage("add folder a").call();
FileEntry entry = new FileTreeIterator.FileEntry(new File(
db.getWorkTree(), "a"), db.getFS());
assertEquals(FileMode.TREE, entry.getMode());
FileUtils.delete(folderA, FileUtils.RECURSIVE);
writeTrashFile("a", "b");
entry = new FileTreeIterator.FileEntry(new File(db.getWorkTree(), "a"),
db.getFS());
assertEquals(FileMode.REGULAR_FILE, entry.getMode());
git.checkout().setName(branch_1.getName()).call();
entry = new FileTreeIterator.FileEntry(new File(db.getWorkTree(), "a"),
db.getFS());
assertEquals(FileMode.REGULAR_FILE, entry.getMode());
}
/**
* Steps:
* <ol>
* <li>Add file 'a'
* <li>Commit
* <li>Create branch '1'
* <li>Replace file 'a' by folder 'a'
* <li>Commit
* <li>Delete folder 'a' in the working tree
* <li>Checkout branch '1'
* </ol>
* <p>
* The working tree should contain 'a' with FileMode.REGULAR_FILE after the
* checkout.
*
* @throws Exception
*/
@Test
public void fileModeTestFolderWithMissingInWorkingTree() throws Exception {
Git git = new Git(db);
writeTrashFile("b", "Hello world b");
writeTrashFile("a", "b");
git.add().addFilepattern(".").call();
git.commit().setMessage("add file b & file a").call();
Ref branch_1 = git.branchCreate().setName("branch_1").call();
git.rm().addFilepattern("a").call();
File folderA = new File(db.getWorkTree(), "a");
FileUtils.mkdirs(folderA);
writeTrashFile("a/c", "Hello world c");
git.add().addFilepattern(".").call();
git.commit().setMessage("add folder a").call();
FileEntry entry = new FileTreeIterator.FileEntry(new File(
db.getWorkTree(), "a"), db.getFS());
assertEquals(FileMode.TREE, entry.getMode());
FileUtils.delete(folderA, FileUtils.RECURSIVE);
git.checkout().setName(branch_1.getName()).call();
entry = new FileTreeIterator.FileEntry(new File(db.getWorkTree(), "a"),
db.getFS());
assertEquals(FileMode.REGULAR_FILE, entry.getMode());
}
/**
* Steps:
* <ol>
* <li>Add file 'a'
* <li>Commit
* <li>Create branch '1'
* <li>Delete file 'a'
* <li>Commit
* <li>Add folder 'a' in the working tree
* <li>Checkout branch '1'
* </ol>
* <p>
* The checkout command should raise an error. The conflicting paths are 'a'
* and 'a/c'.
*
* @throws Exception
*/
@Test
public void fileModeTestMissingWithFolderInWorkingTree() throws Exception {
Git git = new Git(db);
writeTrashFile("b", "Hello world b");
writeTrashFile("a", "b");
git.add().addFilepattern(".").call();
git.commit().setMessage("add file b & file a").call();
Ref branch_1 = git.branchCreate().setName("branch_1").call();
git.rm().addFilepattern("a").call();
git.commit().setMessage("delete file a").call();
FileUtils.mkdirs(new File(db.getWorkTree(), "a"));
writeTrashFile("a/c", "Hello world c");
FileEntry entry = new FileTreeIterator.FileEntry(new File(
db.getWorkTree(), "a"), db.getFS());
assertEquals(FileMode.TREE, entry.getMode());
CheckoutConflictException exception = null;
try {
git.checkout().setName(branch_1.getName()).call();
} catch (CheckoutConflictException e) {
exception = e;
}
assertNotNull(exception);
assertEquals(2, exception.getConflictingPaths().size());
assertEquals("a", exception.getConflictingPaths().get(0));
assertEquals("a/c", exception.getConflictingPaths().get(1));
}
/**
* Steps:
* <ol>
* <li>Add folder 'a'
* <li>Commit
* <li>Create branch '1'
* <li>Delete folder 'a'
* <li>Commit
* <li>Add file 'a' in the working tree
* <li>Checkout branch '1'
* </ol>
* <p>
* The checkout command should raise an error. The conflicting path is 'a'.
*
* @throws Exception
*/
@Test
public void fileModeTestFolderThenMissingWithFileInWorkingTree()
throws Exception {
Git git = new Git(db);
FileUtils.mkdirs(new File(db.getWorkTree(), "a"));
writeTrashFile("a/c", "Hello world c");
writeTrashFile("b", "Hello world b");
git.add().addFilepattern(".").call();
RevCommit commit1 = git.commit().setMessage("add folder a & file b")
.call();
Ref branch_1 = git.branchCreate().setName("branch_1").call();
git.rm().addFilepattern("a").call();
RevCommit commit2 = git.commit().setMessage("delete folder a").call();
TreeWalk tw = new TreeWalk(db);
tw.addTree(commit1.getTree());
tw.addTree(commit2.getTree());
List<DiffEntry> scan = DiffEntry.scan(tw);
assertEquals(1, scan.size());
assertEquals(FileMode.MISSING, scan.get(0).getNewMode());
assertEquals(FileMode.TREE, scan.get(0).getOldMode());
writeTrashFile("a", "b");
FileEntry entry = new FileTreeIterator.FileEntry(new File(
db.getWorkTree(), "a"), db.getFS());
assertEquals(FileMode.REGULAR_FILE, entry.getMode());
CheckoutConflictException exception = null;
try {
git.checkout().setName(branch_1.getName()).call();
} catch (CheckoutConflictException e) {
exception = e;
}
assertNotNull(exception);
assertEquals(1, exception.getConflictingPaths().size());
assertEquals("a", exception.getConflictingPaths().get(0));
}
/**
* Steps:
* <ol>
* <li>Add folder 'a'
* <li>Commit
* <li>Create branch '1'
* <li>Replace folder 'a'by file 'a'
* <li>Commit
* <li>Delete file 'a' in the working tree
* <li>Checkout branch '1'
* </ol>
* <p>
* The working tree should contain 'a' with FileMode.TREE after the
* checkout.
*
* @throws Exception
*/
@Test
public void fileModeTestFolderThenFileWithMissingInWorkingTree()
throws Exception {
Git git = new Git(db);
FileUtils.mkdirs(new File(db.getWorkTree(), "a"));
writeTrashFile("a/c", "Hello world c");
writeTrashFile("b", "Hello world b");
git.add().addFilepattern(".").call();
git.commit().setMessage("add folder a & file b").call();
Ref branch_1 = git.branchCreate().setName("branch_1").call();
git.rm().addFilepattern("a").call();
File fileA = new File(db.getWorkTree(), "a");
writeTrashFile("a", "b");
git.add().addFilepattern("a").call();
git.commit().setMessage("add file a").call();
FileEntry entry = new FileTreeIterator.FileEntry(new File(
db.getWorkTree(), "a"), db.getFS());
assertEquals(FileMode.REGULAR_FILE, entry.getMode());
FileUtils.delete(fileA);
git.checkout().setName(branch_1.getName()).call();
entry = new FileTreeIterator.FileEntry(new File(db.getWorkTree(), "a"),
db.getFS());
assertEquals(FileMode.TREE, entry.getMode());
}
/**
* Steps:
* <ol>
* <li>Add file 'a'
* <li>Commit
* <li>Create branch '1'
* <li>Modify file 'a'
* <li>Commit
* <li>Delete file 'a' & replace by folder 'a' in the working tree & index
* <li>Checkout branch '1'
* </ol>
* <p>
* The checkout command should raise an error. The conflicting path is 'a'.
*
* @throws Exception
*/
@Test
public void fileModeTestFileThenFileWithFolderInIndex() throws Exception {
Git git = new Git(db);
writeTrashFile("a", "Hello world a");
writeTrashFile("b", "Hello world b");
git.add().addFilepattern(".").call();
git.commit().setMessage("add files a & b").call();
Ref branch_1 = git.branchCreate().setName("branch_1").call();
writeTrashFile("a", "b");
git.add().addFilepattern("a").call();
git.commit().setMessage("add file a").call();
FileEntry entry = new FileTreeIterator.FileEntry(new File(
db.getWorkTree(), "a"), db.getFS());
assertEquals(FileMode.REGULAR_FILE, entry.getMode());
git.rm().addFilepattern("a").call();
FileUtils.mkdirs(new File(db.getWorkTree(), "a"));
writeTrashFile("a/c", "Hello world c");
git.add().addFilepattern(".").call();
entry = new FileTreeIterator.FileEntry(new File(db.getWorkTree(), "a"),
db.getFS());
assertEquals(FileMode.TREE, entry.getMode());
CheckoutConflictException exception = null;
try {
git.checkout().setName(branch_1.getName()).call();
} catch (CheckoutConflictException e) {
exception = e;
}
assertNotNull(exception);
assertEquals(1, exception.getConflictingPaths().size());
assertEquals("a", exception.getConflictingPaths().get(0));
}
/**
* Steps:
* <ol>
* <li>Add file 'a'
* <li>Commit
* <li>Create branch '1'
* <li>Modify file 'a'
* <li>Commit
* <li>Delete file 'a' & replace by folder 'a' in the working tree & index
* <li>Checkout branch '1'
* </ol>
* <p>
* The checkout command should raise an error. The conflicting paths are 'a'
* and 'a/c'.
*
* @throws Exception
*/
@Test
public void fileModeTestFileWithFolderInIndex() throws Exception {
Git git = new Git(db);
writeTrashFile("b", "Hello world b");
writeTrashFile("a", "b");
git.add().addFilepattern(".").call();
git.commit().setMessage("add file b & file a").call();
Ref branch_1 = git.branchCreate().setName("branch_1").call();
git.rm().addFilepattern("a").call();
writeTrashFile("a", "Hello world a");
git.add().addFilepattern("a").call();
git.commit().setMessage("add file a").call();
FileEntry entry = new FileTreeIterator.FileEntry(new File(
db.getWorkTree(), "a"), db.getFS());
assertEquals(FileMode.REGULAR_FILE, entry.getMode());
git.rm().addFilepattern("a").call();
FileUtils.mkdirs(new File(db.getWorkTree(), "a"));
writeTrashFile("a/c", "Hello world c");
git.add().addFilepattern(".").call();
entry = new FileTreeIterator.FileEntry(new File(db.getWorkTree(), "a"),
db.getFS());
assertEquals(FileMode.TREE, entry.getMode());
CheckoutConflictException exception = null;
try {
git.checkout().setName(branch_1.getName()).call();
} catch (CheckoutConflictException e) {
exception = e;
}
assertNotNull(exception);
assertEquals(1, exception.getConflictingPaths().size());
assertEquals("a", exception.getConflictingPaths().get(0));
// TODO: ideally we'd like to get two paths from this exception
// assertEquals(2, exception.getConflictingPaths().size());
// assertEquals("a", exception.getConflictingPaths().get(0));
// assertEquals("a/c", exception.getConflictingPaths().get(1));
}
static private void assertEquals(Object expected, Object actual) {
Assert.assertEquals(expected, actual);
}

View File

@ -619,7 +619,7 @@ public void testDirectoryFileConflicts_8() throws Exception {
@Test
public void testDirectoryFileConflicts_9() throws Exception {
// 9
doit(mk("DF"), mkmap("DF", "QP"), mk("DF/DF"));
doit(mkmap("DF", "QP"), mkmap("DF", "QP"), mkmap("DF/DF", "DF/DF"));
assertRemoved("DF/DF");
assertUpdated("DF");
}

View File

@ -562,7 +562,7 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m,
* 6b D F F N N N N Conflict
* 7 F D F Y Y N N Update
* 8 F D F N Y N N Conflict
* 9 F D F Y N N N Update
* 9 F D F N N N Conflict
* 10 F D D N N Y Keep
* 11 F D D N N N Conflict
* 12 F F D Y N Y N Update
@ -610,7 +610,7 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m,
// switch processes all relevant cases.
switch (ffMask) {
case 0xDDF: // 1 2
if (isModified(name)) {
if (f != null && isModifiedSubtree_IndexWorkingtree(name)) {
conflict(name, dce, h, m); // 1
} else {
update(name, mId, mMode); // 2
@ -647,32 +647,29 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m,
break;
case 0xFDF: // 7 8 9
if (equalIdAndMode(hId, hMode, mId, mMode)) {
if (isModified(name))
if (isModifiedSubtree_IndexWorkingtree(name))
conflict(name, dce, h, m); // 8
else
update(name, mId, mMode); // 7
} else if (!isModified(name))
update(name, mId, mMode); // 9
else
// To be confirmed - this case is not in the table.
conflict(name, dce, h, m);
} else
conflict(name, dce, h, m); // 9
break;
case 0xFD0: // keep without a rule
keep(dce);
break;
case 0xFFD: // 12 13 14
if (equalIdAndMode(hId, hMode, iId, iMode))
if (f == null
|| f.isModified(dce, true,
if (f != null
&& f.isModified(dce, true,
this.walk.getObjectReader()))
conflict(name, dce, h, m);
conflict(name, dce, h, m); // 13
else
remove(name);
remove(name); // 12
else
conflict(name, dce, h, m);
conflict(name, dce, h, m); // 14
break;
case 0x0DF: // 16 17
if (!isModified(name))
if (!isModifiedSubtree_IndexWorkingtree(name))
update(name, mId, mMode);
else
conflict(name, dce, h, m);
@ -684,12 +681,14 @@ void processEntry(CanonicalTreeParser h, CanonicalTreeParser m,
}
// if we have no file at all then there is nothing to do
if ((ffMask & 0x222) == 0)
if ((ffMask & 0x222) == 0
&& (f == null || FileMode.TREE.equals(f.getEntryFileMode())))
return;
if ((ffMask == 0x00F) && f != null && FileMode.TREE.equals(f.getEntryFileMode())) {
// File/Directory conflict case #20
conflict(name, null, h, m);
return;
}
if (i == null) {
@ -768,7 +767,9 @@ else if (m == null)
* </pre>
*/
if (m == null || equalIdAndMode(mId, mMode, iId, iMode)) {
if (m == null
|| !isModified_IndexTree(name, iId, iMode, mId, mMode,
mergeCommitTree)) {
// Merge contains nothing or the same as Index
// Nothing in Head
// Something in Index
@ -824,7 +825,7 @@ else if (m == null)
* clean I==H I==M H M Result
* -----------------------------------------------------
* 10 yes yes N/A exists nothing remove path from index
* 11 no yes N/A exists nothing fail
* 11 no yes N/A exists nothing keep file
* 12 yes no N/A exists nothing fail
* 13 no no N/A exists nothing fail
* </pre>
@ -841,23 +842,31 @@ else if (m == null)
// Something different from a submodule in Index
// Nothing in Merge
// Something in Head
if (equalIdAndMode(hId, hMode, iId, iMode)) {
if (!isModified_IndexTree(name, iId, iMode, hId, hMode,
headCommitTree)) {
// Index contains the same as Head
// Something different from a submodule in Index
// Nothing in Merge
// Something in Head
if (f == null
|| f.isModified(dce, true,
this.walk.getObjectReader()))
if (f != null
&& f.isModified(dce, true,
this.walk.getObjectReader())) {
// file is dirty
// Index contains the same as Head
// Something different from a submodule in Index
// Nothing in Merge
// Something in Head
// -> file is dirty but is should be removed. That's
// a conflict
conflict(name, dce, h, m);
else
if (!FileMode.TREE.equals(f.getEntryFileMode())
&& FileMode.TREE.equals(iMode))
// The workingtree contains a file and the index semantically contains a folder.
// Git considers the workingtree file as untracked. Just keep the untracked file.
return;
else
// -> file is dirty and tracked but is should be
// removed. That's a conflict
conflict(name, dce, h, m);
} else
// file doesn't exist or is clean
// Index contains the same as Head
// Something different from a submodule in Index
@ -880,8 +889,10 @@ else if (m == null)
// Something in Head
// Something in Index
if (!equalIdAndMode(hId, hMode, mId, mMode)
&& !equalIdAndMode(hId, hMode, iId, iMode)
&& !equalIdAndMode(mId, mMode, iId, iMode))
&& isModified_IndexTree(name, iId, iMode, hId, hMode,
headCommitTree)
&& isModified_IndexTree(name, iId, iMode, mId, mMode,
mergeCommitTree))
// All three contents in Head, Merge, Index differ from each
// other
// -> All contents differ. Report a conflict.
@ -893,8 +904,10 @@ else if (m == null)
// Something in Head
// Something in Index
if (equalIdAndMode(hId, hMode, iId, iMode)
&& !equalIdAndMode(mId, mMode, iId, iMode)) {
if (!isModified_IndexTree(name, iId, iMode, hId, hMode,
headCommitTree)
&& isModified_IndexTree(name, iId, iMode, mId, mMode,
mergeCommitTree)) {
// Head contains the same as Index. Merge differs
// Something in Merge
@ -1036,25 +1049,88 @@ private void cleanUpConflicts() throws CheckoutConflictException {
}
}
private boolean isModified(String path) throws CorruptObjectException, IOException {
/**
* Checks whether the subtree starting at a given path differs between Index and
* workingtree.
*
* @param path
* @return true if the subtrees differ
* @throws CorruptObjectException
* @throws IOException
*/
private boolean isModifiedSubtree_IndexWorkingtree(String path)
throws CorruptObjectException, IOException {
NameConflictTreeWalk tw = new NameConflictTreeWalk(repo);
tw.addTree(new DirCacheIterator(dc));
tw.addTree(new FileTreeIterator(repo));
tw.setRecursive(true);
tw.setFilter(PathFilter.create(path));
DirCacheIterator dcIt;
WorkingTreeIterator wtIt;
while(tw.next()) {
dcIt = tw.getTree(0, DirCacheIterator.class);
wtIt = tw.getTree(1, WorkingTreeIterator.class);
if (dcIt == null || wtIt == null)
return true;
if (wtIt.isModified(dcIt.getDirCacheEntry(), true,
this.walk.getObjectReader())) {
return true;
try {
tw.addTree(new DirCacheIterator(dc));
tw.addTree(new FileTreeIterator(repo));
tw.setRecursive(true);
tw.setFilter(PathFilter.create(path));
DirCacheIterator dcIt;
WorkingTreeIterator wtIt;
while (tw.next()) {
dcIt = tw.getTree(0, DirCacheIterator.class);
wtIt = tw.getTree(1, WorkingTreeIterator.class);
if (dcIt == null || wtIt == null)
return true;
if (wtIt.isModified(dcIt.getDirCacheEntry(), true,
this.walk.getObjectReader())) {
return true;
}
}
return false;
} finally {
tw.release();
}
}
private boolean isModified_IndexTree(String path, ObjectId iId,
FileMode iMode, ObjectId tId, FileMode tMode, ObjectId rootTree)
throws CorruptObjectException, IOException {
if (iMode != tMode)
return true;
if (FileMode.TREE.equals(iMode)
&& (iId == null || ObjectId.zeroId().equals(iId)))
return isModifiedSubtree_IndexTree(path, rootTree);
else
return !equalIdAndMode(iId, iMode, tId, tMode);
}
/**
* Checks whether the subtree starting at a given path differs between Index and
* some tree.
*
* @param path
* @param tree
* the tree to compare
* @return true if the subtrees differ
* @throws CorruptObjectException
* @throws IOException
*/
private boolean isModifiedSubtree_IndexTree(String path, ObjectId tree)
throws CorruptObjectException, IOException {
NameConflictTreeWalk tw = new NameConflictTreeWalk(repo);
try {
tw.addTree(new DirCacheIterator(dc));
tw.addTree(tree);
tw.setRecursive(true);
tw.setFilter(PathFilter.create(path));
while (tw.next()) {
AbstractTreeIterator dcIt = tw.getTree(0,
DirCacheIterator.class);
AbstractTreeIterator treeIt = tw.getTree(1,
AbstractTreeIterator.class);
if (dcIt == null || treeIt == null)
return true;
if (dcIt.getEntryRawMode() != treeIt.getEntryRawMode())
return true;
if (!dcIt.getEntryObjectId().equals(treeIt.getEntryObjectId()))
return true;
}
return false;
} finally {
tw.release();
}
return false;
}
/**

View File

@ -832,6 +832,8 @@ public boolean isModified(DirCacheEntry entry, boolean forceContentCheck) {
*/
public boolean isModified(DirCacheEntry entry, boolean forceContentCheck,
ObjectReader reader) throws IOException {
if (entry == null)
return !FileMode.MISSING.equals(getEntryFileMode());
MetadataDiff diff = compareMetadata(entry);
switch (diff) {
case DIFFER_BY_TIMESTAMP: