From 2a5d20c138b0e16160329e9030d56158da9ceffb Mon Sep 17 00:00:00 2001 From: Magnus Vigerlöf Date: Sat, 18 Feb 2017 19:28:39 +0100 Subject: Correct the boolean logic for filtering paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The TreeWalk filtering classes need to support the three different meanings of the return value the path comparison generates. A new path comparison method (isPathMatch) is created with three distinct return values (isPathPrefix use value '0' to encode two of these) which will makes it possible for the logical operators (especially NOT) to aggregate a correct verdict. A filter like: AND(Path("path"), NOT(Path("path/to/other"))) Should filter out 'path/to/other/file', but not 'path/to/my/file'. The path-limiting feature when testing path/to/my/file, would result to run test for the following paths: path path/to path/to/my path/to/my/file isPathPrefix('path/to/other') will return '0' for the first two and since there is no way for NOT to distinguish between an exact match and a match indicating that the tested path is a 'parent', it will incorrectly return false and thus remove everything below 'path' immediately. isPathMatch has a distinguished value for 'parent' matches that will be preserved through the logic operators and should not cause an over-eager removal of paths. The functionality of isPathPrefix is required by other parts and is untouched. Unit tests are included to ensure that the logical functionality is correct and can be preserved. Change-Id: Ice2ca9406f09f1b179569e99b86a0e5d77baa20d Signed-off-by: Magnus Vigerlöf --- .../jgit/treewalk/filter/PathFilterLogicTest.java | 360 +++++++++++++++++++++ 1 file changed, 360 insertions(+) create mode 100644 org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterLogicTest.java (limited to 'org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk') 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 0000000000..7c819c5eea --- /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; + } +} + -- cgit v1.2.3