diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java index a5ad18d10..29fc3c3a0 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java @@ -43,6 +43,7 @@ */ package org.eclipse.jgit.api; +import static org.eclipse.jgit.util.FileUtils.RECURSIVE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -777,12 +778,107 @@ public void testAssumeUnchanged() throws Exception { assertEquals("[a.txt, mode:100644, content:more content," + " assume-unchanged:false][b.txt, mode:100644," - + "" + "" + " content:content, assume-unchanged:true]", indexState(CONTENT | ASSUME_UNCHANGED)); } + @Test + public void testReplaceFileWithDirectory() + throws IOException, NoFilepatternException, GitAPIException { + try (Git git = new Git(db)) { + writeTrashFile("df", "before replacement"); + git.add().addFilepattern("df").call(); + assertEquals("[df, mode:100644, content:before replacement]", + indexState(CONTENT)); + FileUtils.delete(new File(db.getWorkTree(), "df")); + writeTrashFile("df/f", "after replacement"); + git.add().addFilepattern("df").call(); + assertEquals("[df/f, mode:100644, content:after replacement]", + indexState(CONTENT)); + } + } + + @Test + public void testReplaceDirectoryWithFile() + throws IOException, NoFilepatternException, GitAPIException { + try (Git git = new Git(db)) { + writeTrashFile("df/f", "before replacement"); + git.add().addFilepattern("df").call(); + assertEquals("[df/f, mode:100644, content:before replacement]", + indexState(CONTENT)); + FileUtils.delete(new File(db.getWorkTree(), "df"), RECURSIVE); + writeTrashFile("df", "after replacement"); + git.add().addFilepattern("df").call(); + assertEquals("[df, mode:100644, content:after replacement]", + indexState(CONTENT)); + } + } + + @Test + public void testReplaceFileByPartOfDirectory() + throws IOException, NoFilepatternException, GitAPIException { + try (Git git = new Git(db)) { + writeTrashFile("src/main", "df", "before replacement"); + writeTrashFile("src/main", "z", "z"); + writeTrashFile("z", "z2"); + git.add().addFilepattern("src/main/df") + .addFilepattern("src/main/z") + .addFilepattern("z") + .call(); + assertEquals( + "[src/main/df, mode:100644, content:before replacement]" + + "[src/main/z, mode:100644, content:z]" + + "[z, mode:100644, content:z2]", + indexState(CONTENT)); + FileUtils.delete(new File(db.getWorkTree(), "src/main/df")); + writeTrashFile("src/main/df", "a", "after replacement"); + writeTrashFile("src/main/df", "b", "unrelated file"); + git.add().addFilepattern("src/main/df/a").call(); + assertEquals( + "[src/main/df/a, mode:100644, content:after replacement]" + + "[src/main/z, mode:100644, content:z]" + + "[z, mode:100644, content:z2]", + indexState(CONTENT)); + } + } + + @Test + public void testReplaceDirectoryConflictsWithFile() + throws IOException, NoFilepatternException, GitAPIException { + DirCache dc = db.lockDirCache(); + try (ObjectInserter oi = db.newObjectInserter()) { + DirCacheBuilder builder = dc.builder(); + File f = writeTrashFile("a", "df", "content"); + addEntryToBuilder("a", f, oi, builder, 1); + + f = writeTrashFile("a", "df", "other content"); + addEntryToBuilder("a/df", f, oi, builder, 3); + + f = writeTrashFile("a", "df", "our content"); + addEntryToBuilder("a/df", f, oi, builder, 2); + + f = writeTrashFile("z", "z"); + addEntryToBuilder("z", f, oi, builder, 0); + builder.commit(); + } + assertEquals( + "[a, mode:100644, stage:1, content:content]" + + "[a/df, mode:100644, stage:2, content:our content]" + + "[a/df, mode:100644, stage:3, content:other content]" + + "[z, mode:100644, content:z]", + indexState(CONTENT)); + + try (Git git = new Git(db)) { + FileUtils.delete(new File(db.getWorkTree(), "a"), RECURSIVE); + writeTrashFile("a", "merged"); + git.add().addFilepattern("a").call(); + assertEquals("[a, mode:100644, content:merged]" + + "[z, mode:100644, content:z]", + indexState(CONTENT)); + } + } + @Test public void testExecutableRetention() throws Exception { StoredConfig config = db.getConfig(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCachePathEditTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCachePathEditTest.java index 63ec85861..39a0cdac5 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCachePathEditTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCachePathEditTest.java @@ -43,11 +43,13 @@ package org.eclipse.jgit.dircache; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import java.util.ArrayList; import java.util.List; import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit; +import org.eclipse.jgit.errors.DirCacheNameConflictException; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; import org.junit.Test; @@ -154,6 +156,108 @@ public void testPathEditShouldBeCalledForEachStage() throws Exception { assertEquals(DirCacheEntry.STAGE_3, entries.get(2).getStage()); } + @Test + public void testFileReplacesTree() throws Exception { + DirCache dc = DirCache.newInCore(); + DirCacheEditor editor = dc.editor(); + editor.add(new AddEdit("a")); + editor.add(new AddEdit("b/c")); + editor.add(new AddEdit("b/d")); + editor.add(new AddEdit("e")); + editor.finish(); + + editor = dc.editor(); + editor.add(new AddEdit("b")); + editor.finish(); + + assertEquals(3, dc.getEntryCount()); + assertEquals("a", dc.getEntry(0).getPathString()); + assertEquals("b", dc.getEntry(1).getPathString()); + assertEquals("e", dc.getEntry(2).getPathString()); + + dc.clear(); + editor = dc.editor(); + editor.add(new AddEdit("A.c")); + editor.add(new AddEdit("A/c")); + editor.add(new AddEdit("A0c")); + editor.finish(); + + editor = dc.editor(); + editor.add(new AddEdit("A")); + editor.finish(); + assertEquals(3, dc.getEntryCount()); + assertEquals("A", dc.getEntry(0).getPathString()); + assertEquals("A.c", dc.getEntry(1).getPathString()); + assertEquals("A0c", dc.getEntry(2).getPathString()); + } + + @Test + public void testTreeReplacesFile() throws Exception { + DirCache dc = DirCache.newInCore(); + DirCacheEditor editor = dc.editor(); + editor.add(new AddEdit("a")); + editor.add(new AddEdit("ab")); + editor.add(new AddEdit("b")); + editor.add(new AddEdit("e")); + editor.finish(); + + editor = dc.editor(); + editor.add(new AddEdit("b/c/d/f")); + editor.add(new AddEdit("b/g/h/i")); + editor.finish(); + + assertEquals(5, dc.getEntryCount()); + assertEquals("a", dc.getEntry(0).getPathString()); + assertEquals("ab", dc.getEntry(1).getPathString()); + assertEquals("b/c/d/f", dc.getEntry(2).getPathString()); + assertEquals("b/g/h/i", dc.getEntry(3).getPathString()); + assertEquals("e", dc.getEntry(4).getPathString()); + } + + @Test + public void testFileOverlapsTree() throws Exception { + DirCache dc = DirCache.newInCore(); + DirCacheEditor editor = dc.editor(); + editor.add(new AddEdit("a")); + editor.add(new AddEdit("a/b").setReplace(false)); + try { + editor.finish(); + fail("Expected DirCacheNameConflictException to be thrown"); + } catch (DirCacheNameConflictException e) { + assertEquals("a a/b", e.getMessage()); + assertEquals("a", e.getPath1()); + assertEquals("a/b", e.getPath2()); + } + + editor = dc.editor(); + editor.add(new AddEdit("A.c")); + editor.add(new AddEdit("A/c").setReplace(false)); + editor.add(new AddEdit("A0c")); + editor.add(new AddEdit("A")); + try { + editor.finish(); + fail("Expected DirCacheNameConflictException to be thrown"); + } catch (DirCacheNameConflictException e) { + assertEquals("A A/c", e.getMessage()); + assertEquals("A", e.getPath1()); + assertEquals("A/c", e.getPath2()); + } + + editor = dc.editor(); + editor.add(new AddEdit("A.c")); + editor.add(new AddEdit("A/b/c/d").setReplace(false)); + editor.add(new AddEdit("A/b/c")); + editor.add(new AddEdit("A0c")); + try { + editor.finish(); + fail("Expected DirCacheNameConflictException to be thrown"); + } catch (DirCacheNameConflictException e) { + assertEquals("A/b/c A/b/c/d", e.getMessage()); + assertEquals("A/b/c", e.getPath1()); + assertEquals("A/b/c/d", e.getPath2()); + } + } + private static DirCacheEntry createEntry(String path, int stage) { DirCacheEntry entry = new DirCacheEntry(path, stage); entry.setFileMode(FileMode.REGULAR_FILE); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java index d768e0fa0..92901f826 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java @@ -1084,7 +1084,7 @@ public void testCheckoutChangeLinkToNonEmptyDirsAndNewIndexEntry() assertWorkDir(mkmap(linkName, "a", fname, "a")); Status st = git.status().call(); - assertFalse(st.isClean()); + assertTrue(st.isClean()); } @Test @@ -1213,9 +1213,7 @@ public void testCheckoutChangeFileToNonEmptyDirsAndNewIndexEntry() assertWorkDir(mkmap(fname, "a")); Status st = git.status().call(); - assertFalse(st.isClean()); - assertEquals(1, st.getAdded().size()); - assertTrue(st.getAdded().contains(fname + "/dir/file1")); + assertTrue(st.isClean()); } @Test diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java index 8a2c08c80..3b94f16f1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java @@ -45,6 +45,7 @@ import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; import static org.eclipse.jgit.lib.FileMode.GITLINK; +import static org.eclipse.jgit.lib.FileMode.TYPE_TREE; import java.io.IOException; import java.io.InputStream; @@ -66,7 +67,7 @@ import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.treewalk.FileTreeIterator; -import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.NameConflictTreeWalk; import org.eclipse.jgit.treewalk.TreeWalk.OperationType; import org.eclipse.jgit.treewalk.WorkingTreeIterator; import org.eclipse.jgit.treewalk.filter.PathFilterGroup; @@ -141,7 +142,7 @@ public DirCache call() throws GitAPIException, NoFilepatternException { boolean addAll = filepatterns.contains("."); //$NON-NLS-1$ try (ObjectInserter inserter = repo.newObjectInserter(); - final TreeWalk tw = new TreeWalk(repo)) { + NameConflictTreeWalk tw = new NameConflictTreeWalk(repo)) { tw.setOperationType(OperationType.CHECKIN_OP); dc = repo.lockDirCache(); @@ -151,7 +152,6 @@ public DirCache call() throws GitAPIException, NoFilepatternException { workingTreeIterator = new FileTreeIterator(repo); workingTreeIterator.setDirCacheIterator(tw, 0); tw.addTree(workingTreeIterator); - tw.setRecursive(true); if (!addAll) tw.setFilter(PathFilterGroup.createFromStrings(filepatterns)); @@ -180,9 +180,14 @@ public DirCache call() throws GitAPIException, NoFilepatternException { continue; } + if (tw.isSubtree() && !tw.isDirectoryFileConflict()) { + tw.enterSubtree(); + continue; + } + if (f == null) { // working tree file does not exist - if (c != null - && (!update || GITLINK == c.getEntryFileMode())) { + if (entry != null + && (!update || GITLINK == entry.getFileMode())) { builder.add(entry); } continue; @@ -196,6 +201,14 @@ public DirCache call() throws GitAPIException, NoFilepatternException { continue; } + if (f.getEntryRawMode() == TYPE_TREE) { + // Index entry exists and is symlink, gitlink or file, + // otherwise the tree would have been entered above. + // Replace the index entry by diving into tree of files. + tw.enterSubtree(); + continue; + } + byte[] path = tw.getRawPath(); if (entry == null || entry.getStage() > 0) { entry = new DirCacheEntry(path); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/BaseDirCacheEditor.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/BaseDirCacheEditor.java index 70f80aeb7..fc789b3d1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/BaseDirCacheEditor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/BaseDirCacheEditor.java @@ -44,8 +44,13 @@ package org.eclipse.jgit.dircache; +import static org.eclipse.jgit.lib.FileMode.TREE; +import static org.eclipse.jgit.lib.FileMode.TYPE_TREE; + import java.io.IOException; +import org.eclipse.jgit.errors.DirCacheNameConflictException; + /** * Generic update/editing support for {@link DirCache}. *
@@ -168,6 +173,7 @@ protected void fastKeep(final int pos, int cnt) { * {@link #finish()}, and only after {@link #entries} is sorted. */ protected void replace() { + checkNameConflicts(); if (entryCnt < entries.length / 2) { final DirCacheEntry[] n = new DirCacheEntry[entryCnt]; System.arraycopy(entries, 0, n, 0, entryCnt); @@ -176,6 +182,98 @@ protected void replace() { cache.replace(entries, entryCnt); } + private void checkNameConflicts() { + int end = entryCnt - 1; + for (int eIdx = 0; eIdx < end; eIdx++) { + DirCacheEntry e = entries[eIdx]; + if (e.getStage() != 0) { + continue; + } + + byte[] ePath = e.path; + int prefixLen = lastSlash(ePath) + 1; + + for (int nIdx = eIdx + 1; nIdx < entryCnt; nIdx++) { + DirCacheEntry n = entries[nIdx]; + if (n.getStage() != 0) { + continue; + } + + byte[] nPath = n.path; + if (!startsWith(ePath, nPath, prefixLen)) { + // Different prefix; this entry is in another directory. + break; + } + + int s = nextSlash(nPath, prefixLen); + int m = s < nPath.length ? TYPE_TREE : n.getRawMode(); + int cmp = pathCompare( + ePath, prefixLen, ePath.length, TYPE_TREE, + nPath, prefixLen, s, m); + if (cmp < 0) { + break; + } else if (cmp == 0) { + throw new DirCacheNameConflictException( + e.getPathString(), + n.getPathString()); + } + } + } + } + + private static int lastSlash(byte[] path) { + for (int i = path.length - 1; i >= 0; i--) { + if (path[i] == '/') { + return i; + } + } + return -1; + } + + private static int nextSlash(byte[] b, int p) { + final int n = b.length; + for (; p < n; p++) { + if (b[p] == '/') { + return p; + } + } + return n; + } + + private static boolean startsWith(byte[] a, byte[] b, int n) { + if (b.length < n) { + return false; + } + for (n--; n >= 0; n--) { + if (a[n] != b[n]) { + return false; + } + } + return true; + } + + static int pathCompare(byte[] aPath, int aPos, int aEnd, int aMode, + byte[] bPath, int bPos, int bEnd, int bMode) { + while (aPos < aEnd && bPos < bEnd) { + int cmp = (aPath[aPos++] & 0xff) - (bPath[bPos++] & 0xff); + if (cmp != 0) { + return cmp; + } + } + + if (aPos < aEnd) { + return (aPath[aPos] & 0xff) - lastPathChar(bMode); + } + if (bPos < bEnd) { + return lastPathChar(aMode) - (bPath[bPos] & 0xff); + } + return 0; + } + + private static int lastPathChar(int mode) { + return TREE.equals(mode) ? '/' : '\0'; + } + /** * Finish, write, commit this change, and release the index lock. *
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java
index fa0339544..ecdfe823a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java
@@ -800,8 +800,11 @@ public int findEntry(final String path) {
* information. If < 0 the entry does not exist in the index.
* @since 3.4
*/
- public int findEntry(final byte[] p, final int pLen) {
- int low = 0;
+ public int findEntry(byte[] p, int pLen) {
+ return findEntry(0, p, pLen);
+ }
+
+ int findEntry(int low, byte[] p, int pLen) {
int high = entryCnt;
while (low < high) {
int mid = (low + high) >>> 1;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuildIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuildIterator.java
index da5530666..c10e41608 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuildIterator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuildIterator.java
@@ -130,4 +130,9 @@ public void stopWalk() {
if (cur < cnt)
builder.keep(cur, cnt - cur);
}
+
+ @Override
+ protected boolean needsStopWalk() {
+ return ptr < cache.getEntryCount();
+ }
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java
index 932ef94db..889e19422 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java
@@ -44,6 +44,10 @@
package org.eclipse.jgit.dircache;
+import static org.eclipse.jgit.dircache.DirCache.cmp;
+import static org.eclipse.jgit.dircache.DirCacheTree.peq;
+import static org.eclipse.jgit.lib.FileMode.TYPE_TREE;
+
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
@@ -72,11 +76,12 @@ public class DirCacheEditor extends BaseDirCacheEditor {
public int compare(final PathEdit o1, final PathEdit o2) {
final byte[] a = o1.path;
final byte[] b = o2.path;
- return DirCache.cmp(a, a.length, b, b.length);
+ return cmp(a, a.length, b, b.length);
}
};
private final List
@@ -181,6 +289,7 @@ private void applyEdits() {
*/
public abstract static class PathEdit {
final byte[] path;
+ boolean replace = true;
/**
* Create a new update command by path name.
@@ -192,6 +301,10 @@ public PathEdit(final String entryPath) {
path = Constants.encode(entryPath);
}
+ PathEdit(byte[] path) {
+ this.path = path;
+ }
+
/**
* Create a new update command for an existing entry instance.
*
@@ -203,6 +316,22 @@ public PathEdit(final DirCacheEntry ent) {
path = ent.path;
}
+ /**
+ * Configure if a file can replace a directory (or vice versa).
+ *
+ * Default is {@code true} as this is usually the desired behavior.
+ *
+ * @param ok
+ * if true a file can replace a directory, or a directory can
+ * replace a file.
+ * @return {@code this}
+ * @since 4.2
+ */
+ public PathEdit setReplace(boolean ok) {
+ replace = ok;
+ return this;
+ }
+
/**
* Apply the update to a single cache entry matching the path.
*
@@ -214,6 +343,12 @@ public PathEdit(final DirCacheEntry ent) {
* the path is a new path in the index.
*/
public abstract void apply(DirCacheEntry ent);
+
+ @Override
+ public String toString() {
+ String p = DirCacheEntry.toString(path);
+ return getClass().getSimpleName() + '[' + p + ']';
+ }
}
/**
@@ -281,6 +416,21 @@ public DeleteTree(String entryPath) {
: entryPath + '/');
}
+ DeleteTree(byte[] path) {
+ super(appendSlash(path));
+ }
+
+ private static byte[] appendSlash(byte[] path) {
+ int n = path.length;
+ if (n > 0 && path[n - 1] != '/') {
+ byte[] r = new byte[n + 1];
+ System.arraycopy(path, 0, r, 0, n);
+ r[n] = '/';
+ return r;
+ }
+ return path;
+ }
+
public void apply(final DirCacheEntry ent) {
throw new UnsupportedOperationException(JGitText.get().noApplyInDelete);
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java
index c8bc0960f..5df9e0b4d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java
@@ -745,7 +745,7 @@ private static void checkPath(byte[] path) {
}
}
- private static String toString(final byte[] path) {
+ static String toString(final byte[] path) {
return Constants.CHARSET.decode(ByteBuffer.wrap(path)).toString();
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/DirCacheNameConflictException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/DirCacheNameConflictException.java
new file mode 100644
index 000000000..5f67e3439
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/DirCacheNameConflictException.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2015, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.errors;
+
+/**
+ * Thrown by DirCache code when entries overlap in impossible way.
+ *
+ * @since 4.2
+ */
+public class DirCacheNameConflictException extends IllegalStateException {
+ private static final long serialVersionUID = 1L;
+
+ private final String path1;
+ private final String path2;
+
+ /**
+ * Construct an exception for a specific path.
+ *
+ * @param path1
+ * one path that conflicts.
+ * @param path2
+ * another path that conflicts.
+ */
+ public DirCacheNameConflictException(String path1, String path2) {
+ super(path1 + ' ' + path2);
+ this.path1 = path1;
+ this.path2 = path2;
+ }
+
+ /** @return one of the paths that has a conflict. */
+ public String getPath1() {
+ return path1;
+ }
+
+ /** @return another path that has a conflict. */
+ public String getPath2() {
+ return path2;
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java
index 5e7188957..27d0f7b58 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java
@@ -691,6 +691,14 @@ public void stopWalk() {
// Do nothing by default. Most iterators do not care.
}
+ /**
+ * @return true if the iterator implements {@link #stopWalk()}.
+ * @since 4.2
+ */
+ protected boolean needsStopWalk() {
+ return false;
+ }
+
/**
* @return the length of the name component of the path for the current entry
*/
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/EmptyTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/EmptyTreeIterator.java
index 8dbf80e6a..ec4a84eff 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/EmptyTreeIterator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/EmptyTreeIterator.java
@@ -142,4 +142,9 @@ public void stopWalk() {
if (parent != null)
parent.stopWalk();
}
+
+ @Override
+ protected boolean needsStopWalk() {
+ return parent != null && parent.needsStopWalk();
+ }
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/NameConflictTreeWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/NameConflictTreeWalk.java
index 350f56396..d2195a874 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/NameConflictTreeWalk.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/NameConflictTreeWalk.java
@@ -43,6 +43,8 @@
package org.eclipse.jgit.treewalk;
+import java.io.IOException;
+
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.lib.FileMode;
@@ -338,6 +340,41 @@ void skipEntriesEqual() throws CorruptObjectException {
dfConflict = null;
}
+ void stopWalk() throws IOException {
+ if (!needsStopWalk()) {
+ return;
+ }
+
+ // Name conflicts make aborting early difficult. Multiple paths may
+ // exist between the file and directory versions of a name. To ensure
+ // the directory version is skipped over (as it was previously visited
+ // during the file version step) requires popping up the stack and
+ // finishing out each subtree that the walker dove into. Siblings in
+ // parents do not need to be recursed into, bounding the cost.
+ for (;;) {
+ AbstractTreeIterator t = min();
+ if (t.eof()) {
+ if (depth > 0) {
+ exitSubtree();
+ popEntriesEqual();
+ continue;
+ }
+ return;
+ }
+ currentHead = t;
+ skipEntriesEqual();
+ }
+ }
+
+ private boolean needsStopWalk() {
+ for (AbstractTreeIterator t : trees) {
+ if (t.needsStopWalk()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* True if the current entry is covered by a directory/file conflict.
*
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java
index 438549520..83fada4f9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java
@@ -57,6 +57,7 @@
import org.eclipse.jgit.attributes.AttributesNode;
import org.eclipse.jgit.attributes.AttributesNodeProvider;
import org.eclipse.jgit.attributes.AttributesProvider;
+import org.eclipse.jgit.dircache.DirCacheBuildIterator;
import org.eclipse.jgit.dircache.DirCacheIterator;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -256,7 +257,7 @@ public static TreeWalk forPath(final Repository db, final String path,
private boolean postOrderTraversal;
- private int depth;
+ int depth;
private boolean advance;
@@ -665,12 +666,29 @@ public boolean next() throws MissingObjectException,
return true;
}
} catch (StopWalkException stop) {
- for (final AbstractTreeIterator t : trees)
- t.stopWalk();
+ stopWalk();
return false;
}
}
+ /**
+ * Notify iterators the walk is aborting.
+ *
+ * Primarily to notify {@link DirCacheBuildIterator} the walk is aborting so
+ * that it can copy any remaining entries.
+ *
+ * @throws IOException
+ * if traversal of remaining entries throws an exception during
+ * object access. This should never occur as remaining trees
+ * should already be in memory, however the methods used to
+ * finish traversal are declared to throw IOException.
+ */
+ void stopWalk() throws IOException {
+ for (AbstractTreeIterator t : trees) {
+ t.stopWalk();
+ }
+ }
+
/**
* Obtain the tree iterator for the current entry.
*
@@ -1065,7 +1083,7 @@ void skipEntriesEqual() throws CorruptObjectException {
}
}
- private void exitSubtree() {
+ void exitSubtree() {
depth--;
for (int i = 0; i < trees.length; i++)
trees[i] = trees[i].parent;