diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterLogicTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterLogicTest.java new file mode 100644 index 000000000..7c819c5ee --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterLogicTest.java @@ -0,0 +1,360 @@ +/* + * Copyright (C) 2017 Magnus Vigerlöf (magnus.vigerlof@gmail.com) + * 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.treewalk.filter; + +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheBuilder; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class PathFilterLogicTest extends RepositoryTestCase { + + private ObjectId treeId; + + @Before + public void setup() throws IOException { + String[] paths = new String[] { + "a.txt", + "sub1.txt", + "sub1/suba/a.txt", + "sub1/subb/b.txt", + "sub2/suba/a.txt" + }; + treeId = createTree(paths); + } + + @Test + public void testSinglePath() throws IOException { + List expected = Arrays.asList("sub1/suba/a.txt", + "sub1/subb/b.txt"); + + TreeFilter tf = PathFilter.create("sub1"); + List paths = getMatchingPaths(treeId, tf); + + assertEquals(expected, paths); + } + + @Test + public void testSingleSubPath() throws IOException { + List expected = Collections.singletonList("sub1/suba/a.txt"); + + TreeFilter tf = PathFilter.create("sub1/suba"); + List paths = getMatchingPaths(treeId, tf); + + assertEquals(expected, paths); + } + + @Test + public void testSinglePathNegate() throws IOException { + List expected = Arrays.asList("a.txt", "sub1.txt", + "sub2/suba/a.txt"); + + TreeFilter tf = PathFilter.create("sub1").negate(); + List paths = getMatchingPaths(treeId, tf); + + assertEquals(expected, paths); + } + + @Test + public void testSingleSubPathNegate() throws IOException { + List expected = Arrays.asList("a.txt", "sub1.txt", + "sub1/subb/b.txt", "sub2/suba/a.txt"); + + TreeFilter tf = PathFilter.create("sub1/suba").negate(); + List paths = getMatchingPaths(treeId, tf); + + assertEquals(expected, paths); + } + + @Test + public void testOrMultiTwoPath() throws IOException { + List expected = Arrays.asList("sub1/suba/a.txt", + "sub1/subb/b.txt", "sub2/suba/a.txt"); + + TreeFilter[] tf = new TreeFilter[] {PathFilter.create("sub1"), + PathFilter.create("sub2")}; + List paths = getMatchingPaths(treeId, OrTreeFilter.create(tf)); + + assertEquals(expected, paths); + } + + @Test + public void testOrMultiThreePath() throws IOException { + List expected = Arrays.asList("sub1.txt", "sub1/suba/a.txt", + "sub1/subb/b.txt", "sub2/suba/a.txt"); + + TreeFilter[] tf = new TreeFilter[] {PathFilter.create("sub1"), + PathFilter.create("sub2"), PathFilter.create("sub1.txt")}; + List paths = getMatchingPaths(treeId, OrTreeFilter.create(tf)); + + assertEquals(expected, paths); + } + + @Test + public void testOrMultiTwoSubPath() throws IOException { + List expected = Arrays.asList("sub1/subb/b.txt", + "sub2/suba/a.txt"); + + TreeFilter[] tf = new TreeFilter[] {PathFilter.create("sub1/subb"), + PathFilter.create("sub2/suba")}; + List paths = getMatchingPaths(treeId, OrTreeFilter.create(tf)); + + assertEquals(expected, paths); + } + + @Test + public void testOrMultiTwoMixSubPath() throws IOException { + List expected = Arrays.asList("sub1/subb/b.txt", + "sub2/suba/a.txt"); + + TreeFilter[] tf = new TreeFilter[] {PathFilter.create("sub1/subb"), + PathFilter.create("sub2")}; + List paths = getMatchingPaths(treeId, OrTreeFilter.create(tf)); + + assertEquals(expected, paths); + } + + @Test + public void testOrMultiTwoMixSubPathNegate() throws IOException { + List expected = Arrays.asList("a.txt", "sub1.txt", + "sub1/suba/a.txt", "sub2/suba/a.txt"); + + TreeFilter[] tf = new TreeFilter[] {PathFilter.create("sub1").negate(), + PathFilter.create("sub1/suba")}; + List paths = getMatchingPaths(treeId, OrTreeFilter.create(tf)); + + assertEquals(expected, paths); + } + + @Test + public void testOrMultiThreeMixSubPathNegate() throws IOException { + List expected = Arrays.asList("a.txt", "sub1.txt", + "sub1/suba/a.txt", "sub2/suba/a.txt"); + + TreeFilter[] tf = new TreeFilter[] {PathFilter.create("sub1").negate(), + PathFilter.create("sub1/suba"), PathFilter.create("no/path")}; + List paths = getMatchingPaths(treeId, OrTreeFilter.create(tf)); + + assertEquals(expected, paths); + } + + @Test + public void testPatternParentFileMatch() throws IOException { + List expected = Collections.emptyList(); + + TreeFilter tf = PathFilter.create("a.txt/test/path"); + List paths = getMatchingPaths(treeId, tf); + + assertEquals(expected, paths); + } + + @Test + public void testAndMultiPath() throws IOException { + List expected = Collections.emptyList(); + + TreeFilter[] tf = new TreeFilter[] {PathFilter.create("sub1"), + PathFilter.create("sub2")}; + List paths = getMatchingPaths(treeId, AndTreeFilter.create(tf)); + + assertEquals(expected, paths); + } + + @Test + public void testAndMultiPathNegate() throws IOException { + List expected = Arrays.asList("sub1/suba/a.txt", + "sub1/subb/b.txt"); + + TreeFilter[] tf = new TreeFilter[] {PathFilter.create("sub1"), + PathFilter.create("sub2").negate()}; + List paths = getMatchingPaths(treeId, AndTreeFilter.create(tf)); + + assertEquals(expected, paths); + } + + @Test + public void testAndMultiSubPathDualNegate() throws IOException { + List expected = Arrays.asList("a.txt", "sub1.txt", + "sub1/subb/b.txt"); + + TreeFilter[] tf = new TreeFilter[] {PathFilter.create("sub1/suba").negate(), + PathFilter.create("sub2").negate()}; + List paths = getMatchingPaths(treeId, AndTreeFilter.create(tf)); + + assertEquals(expected, paths); + } + + @Test + public void testAndMultiSubPath() throws IOException { + List expected = Collections.emptyList(); + + TreeFilter[] tf = new TreeFilter[] {PathFilter.create("sub1"), + PathFilter.create("sub2/suba")}; + List paths = getMatchingPaths(treeId, AndTreeFilter.create(tf)); + + assertEquals(expected, paths); + } + + @Test + public void testAndMultiSubPathNegate() throws IOException { + List expected = Collections.singletonList("sub1/subb/b.txt"); + + TreeFilter[] tf = new TreeFilter[] {PathFilter.create("sub1"), + PathFilter.create("sub1/suba").negate()}; + List paths = getMatchingPaths(treeId, AndTreeFilter.create(tf)); + + assertEquals(expected, paths); + } + + @Test + public void testAndMultiThreeSubPathNegate() throws IOException { + List expected = Collections.singletonList("sub1/subb/b.txt"); + + TreeFilter[] tf = new TreeFilter[]{PathFilter.create("sub1"), + PathFilter.create("sub1/suba").negate(), + PathFilter.create("no/path").negate()}; + List paths = getMatchingPaths(treeId, AndTreeFilter.create(tf)); + + assertEquals(expected, paths); + } + + @Test + public void testTopAndMultiPathDualNegate() throws IOException { + List expected = Arrays.asList("a.txt", "sub1.txt"); + + TreeFilter[] tf = new TreeFilter[] {PathFilter.create("sub1").negate(), + PathFilter.create("sub2").negate()}; + List paths = getMatchingPathsFlat(treeId, AndTreeFilter.create(tf)); + + assertEquals(expected, paths); + } + + @Test + public void testTopAndMultiSubPathDualNegate() throws IOException { + List expected = Arrays.asList("a.txt", "sub1.txt", "sub1"); + + // Filter on 'sub1/suba' is kind of silly for a non-recursive walk. + // The result is interesting though as the 'sub1' path should be + // returned, due to the fact that there may be hits once the pattern + // is tested with one of the leaf paths. + TreeFilter[] tf = new TreeFilter[] {PathFilter.create("sub1/suba").negate(), + PathFilter.create("sub2").negate()}; + List paths = getMatchingPathsFlat(treeId, AndTreeFilter.create(tf)); + + assertEquals(expected, paths); + } + + @Test + public void testTopOrMultiPathDual() throws IOException { + List expected = Arrays.asList("sub1.txt", "sub2"); + + TreeFilter[] tf = new TreeFilter[] {PathFilter.create("sub1.txt"), + PathFilter.create("sub2")}; + List paths = getMatchingPathsFlat(treeId, OrTreeFilter.create(tf)); + + assertEquals(expected, paths); + } + + @Test + public void testTopNotPath() throws IOException { + List expected = Arrays.asList("a.txt", "sub1.txt", "sub2"); + + TreeFilter tf = PathFilter.create("sub1"); + List paths = getMatchingPathsFlat(treeId, NotTreeFilter.create(tf)); + + assertEquals(expected, paths); + } + + private List getMatchingPaths(final ObjectId objId, + TreeFilter tf) throws IOException { + return getMatchingPaths(objId, tf, true); + } + + private List getMatchingPathsFlat(final ObjectId objId, + TreeFilter tf) throws IOException { + return getMatchingPaths(objId, tf, false); + } + + private List getMatchingPaths(final ObjectId objId, + TreeFilter tf, boolean recursive) throws IOException { + try (TreeWalk tw = new TreeWalk(db)) { + tw.setFilter(tf); + tw.setRecursive(recursive); + tw.addTree(objId); + + List paths = new ArrayList<>(); + while (tw.next()) { + paths.add(tw.getPathString()); + } + return paths; + } + } + + private ObjectId createTree(String... paths) throws IOException { + final ObjectInserter odi = db.newObjectInserter(); + final DirCache dc = db.readDirCache(); + final DirCacheBuilder builder = dc.builder(); + for (String path : paths) { + DirCacheEntry entry = createEntry(path, FileMode.REGULAR_FILE); + builder.add(entry); + } + builder.finish(); + final ObjectId objId = dc.writeTree(odi); + odi.flush(); + return objId; + } +} + 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 1ed946815..c54e1484c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java @@ -826,7 +826,7 @@ public boolean next() throws MissingObjectException, } currentHead = t; - if (!filter.include(this)) { + if (filter.matchFilter(this) == 1) { skipEntriesEqual(); continue; } @@ -1059,6 +1059,60 @@ public int getPathLength() { return currentHead.pathLen; } + /** + * Test if the supplied path matches the current entry's path. + *

+ * This method detects if the supplied path is equal to, a subtree of, or + * not similar at all to the current entry. It is faster to use this + * method than to use {@link #getPathString()} to first create a String + * object, then test startsWith or some other type of string + * match function. + *

+ * If the current entry is a subtree, then all paths within the subtree + * are considered to match it. + * + * @param p + * path buffer to test. Callers should ensure the path does not + * end with '/' prior to invocation. + * @param pLen + * number of bytes from buf to test. + * @return -1 if the current path is a parent to p; 0 if p matches the current + * path; 1 if the current path is different and will never match + * again on this tree walk. + * @since 4.7 + */ + public int isPathMatch(final byte[] p, final int pLen) { + final AbstractTreeIterator t = currentHead; + final byte[] c = t.path; + final int cLen = t.pathLen; + int ci; + + for (ci = 0; ci < cLen && ci < pLen; ci++) { + final int c_value = (c[ci] & 0xff) - (p[ci] & 0xff); + if (c_value != 0) { + // Paths do not and will never match + return 1; + } + } + + if (ci < cLen) { + // Ran out of pattern but we still had current data. + // If c[ci] == '/' then pattern matches the subtree. + // Otherwise it is a partial match == miss + return c[ci] == '/' ? 0 : 1; + } + + if (ci < pLen) { + // Ran out of current, but we still have pattern data. + // If p[ci] == '/' then this subtree is a parent in the pattern, + // otherwise it's a miss. + return p[ci] == '/' && FileMode.TREE.equals(t.mode) ? -1 : 1; + } + + // Both strings are identical. + return 0; + } + /** * Test if the supplied path matches the current entry's path. *

diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/AndTreeFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/AndTreeFilter.java index d5e7464d4..9658166a8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/AndTreeFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/AndTreeFilter.java @@ -128,7 +128,25 @@ private static class Binary extends AndTreeFilter { public boolean include(final TreeWalk walker) throws MissingObjectException, IncorrectObjectTypeException, IOException { - return a.include(walker) && b.include(walker); + return matchFilter(walker) <= 0; + } + + @Override + public int matchFilter(TreeWalk walker) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + final int ra = a.matchFilter(walker); + if (ra == 1) { + return 1; + } + final int rb = b.matchFilter(walker); + if (rb == 1) { + return 1; + } + if (ra == -1 || rb == -1) { + return -1; + } + return 0; } @Override @@ -159,11 +177,24 @@ private static class List extends AndTreeFilter { public boolean include(final TreeWalk walker) throws MissingObjectException, IncorrectObjectTypeException, IOException { + return matchFilter(walker) <= 0; + } + + @Override + public int matchFilter(TreeWalk walker) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + int m = 0; for (final TreeFilter f : subfilters) { - if (!f.include(walker)) - return false; + int r = f.matchFilter(walker); + if (r == 1) { + return 1; + } + if (r == -1) { + m = -1; + } } - return true; + return m; } @Override diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/NotTreeFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/NotTreeFilter.java index 8ec04bb32..80c0b87e1 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/NotTreeFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/NotTreeFilter.java @@ -78,7 +78,23 @@ public TreeFilter negate() { public boolean include(final TreeWalk walker) throws MissingObjectException, IncorrectObjectTypeException, IOException { - return !a.include(walker); + return matchFilter(walker) == 0; + } + + @Override + public int matchFilter(TreeWalk walker) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + final int r = a.matchFilter(walker); + // switch 0 and 1, keep -1 as that defines a subpath that must be + // traversed before a final verdict can be made. + if (r == 0) { + return 1; + } + if (r == 1) { + return 0; + } + return -1; } @Override diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/OrTreeFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/OrTreeFilter.java index 270633ce6..2c1a9d438 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/OrTreeFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/OrTreeFilter.java @@ -126,7 +126,25 @@ private static class Binary extends OrTreeFilter { public boolean include(final TreeWalk walker) throws MissingObjectException, IncorrectObjectTypeException, IOException { - return a.include(walker) || b.include(walker); + return matchFilter(walker) <= 0; + } + + @Override + public int matchFilter(TreeWalk walker) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + final int ra = a.matchFilter(walker); + if (ra == 0) { + return 0; + } + final int rb = b.matchFilter(walker); + if (rb == 0) { + return 0; + } + if (ra == -1 || rb == -1) { + return -1; + } + return 1; } @Override @@ -157,11 +175,24 @@ private static class List extends OrTreeFilter { public boolean include(final TreeWalk walker) throws MissingObjectException, IncorrectObjectTypeException, IOException { + return matchFilter(walker) <= 0; + } + + @Override + public int matchFilter(TreeWalk walker) + throws MissingObjectException, IncorrectObjectTypeException, + IOException { + int m = 1; for (final TreeFilter f : subfilters) { - if (f.include(walker)) - return true; + int r = f.matchFilter(walker); + if (r == 0) { + return 0; + } + if (r == -1) { + m = -1; + } } - return false; + return m; } @Override diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilter.java index 103b0e227..445ba157e 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilter.java @@ -97,7 +97,12 @@ public String getPath() { @Override public boolean include(final TreeWalk walker) { - return walker.isPathPrefix(pathRaw, pathRaw.length) == 0; + return matchFilter(walker) <= 0; + } + + @Override + public int matchFilter(final TreeWalk walker) { + return walker.isPathMatch(pathRaw, pathRaw.length); } @Override diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/TreeFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/TreeFilter.java index 9f468ec3f..2c2fb4746 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/TreeFilter.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/TreeFilter.java @@ -198,6 +198,34 @@ public abstract boolean include(TreeWalk walker) throws MissingObjectException, IncorrectObjectTypeException, IOException; + /** + * Determine if the current entry is a parent, a match, or no match. + *

+ * This method extends the result returned by {@link #include(TreeWalk)} + * with a third option (-1), splitting the value true. This gives the + * application a possibility to distinguish between an exact match + * and the case when a subtree to the current entry might be a match. + * + * @param walker + * the walker the filter needs to examine. + * @return -1 if the current entry is a parent of the filter but no + * exact match has been made; 0 if the current entry should + * be seen by the application; 1 if it should be hidden. + * @throws MissingObjectException + * as thrown by {@link #include(TreeWalk)} + * @throws IncorrectObjectTypeException + * as thrown by {@link #include(TreeWalk)} + * @throws IOException + * as thrown by {@link #include(TreeWalk)} + * @since 4.7 + */ + public int matchFilter(final TreeWalk walker) + throws MissingObjectException, IncorrectObjectTypeException, + IOException + { + return include(walker) ? 0 : 1; + } + /** * Does this tree filter require a recursive walk to match everything? *