aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Wolf <thomas.wolf@paranor.ch>2017-08-12 21:51:52 +0200
committerMatthias Sohn <matthias.sohn@sap.com>2017-08-27 16:02:40 +0200
commitd80b999c76ef7c02c39810d071ec20acaf7d200a (patch)
treed4c2bd9fbe9a3ab39f324ce81cf84603b66369ae
parent08d4bef6b7db50398088974d0f9a7b0d52015f86 (diff)
downloadjgit-d80b999c76ef7c02c39810d071ec20acaf7d200a.tar.gz
jgit-d80b999c76ef7c02c39810d071ec20acaf7d200a.zip
Fix path pattern matching to work also for gitattributes
Path pattern matching for attribute rules is different than matching for excluded files. The first difference concerns patterns without slashes. For gitattributes those must match on the last component only, not on any earlier segment. This is true also for directory-only patterns. The second difference concerns directory-only patterns. Those also must not match on a prefix or segment except the last one. They do not apply recursively to all files beneath. And third, matches only on a prefix must match for gitattributes only if the last matcher was "/**". Add a new parameter for such path matching to IMatcher.matches() and pass it through as appropriate (false for gitignore, true for gitattributes). As far as gitignore is concerned, there is no change. New tests have been added, and some existing attribute matching tests have been fixed since they operated on wrong assumptions. Bug: 508568 Change-Id: Ie825dc2cac8a85a72a7eeb0abb888f3193d21dd2 Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java224
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesMatcherTest.java92
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/CGitAttributesTest.java94
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java3
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesRule.java4
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/ignore/FastIgnoreRule.java2
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/IMatcher.java12
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/NameMatcher.java78
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/PathMatcher.java73
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/WildMatcher.java3
10 files changed, 479 insertions, 106 deletions
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java
index 50d020c57c..0717379e24 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java
@@ -1,4 +1,7 @@
/*
+ * Copyright (C) 2015, 2017 Ivan Motsch <ivan.motsch@bsiag.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
@@ -259,25 +262,230 @@ public class AttributesHandlerTest extends RepositoryTestCase {
setupRepo("sub/ global", "sub/** init",
"sub/** top_sub\n*.txt top",
"sub/** subsub\nsub/ subsub2\n*.txt foo");
- // The last two sub/** and sub/ rules are in sub/.gitattributes. They
- // must not apply to any of the files here. They would match for a
- // further subdirectory sub/sub.
+ // The last sub/** is in sub/.gitattributes. It must not
+ // apply to any of the files here. It would match for a
+ // further subdirectory sub/sub. The sub/ rules must match
+ // only for directories.
walk = beginWalk();
assertIteration(F, ".gitattributes");
assertIteration(D, "sub", attrs("global"));
- assertIteration(F, "sub/.gitattributes", attrs("init top_sub global"));
- assertIteration(F, "sub/a.txt", attrs("init foo top top_sub global"));
+ assertIteration(F, "sub/.gitattributes", attrs("init top_sub"));
+ assertIteration(F, "sub/a.txt", attrs("init foo top top_sub"));
endWalk();
// All right, let's see that they *do* apply in sub/sub:
writeTrashFile("sub/sub/b.txt", "b");
walk = beginWalk();
assertIteration(F, ".gitattributes");
assertIteration(D, "sub", attrs("global"));
- assertIteration(F, "sub/.gitattributes", attrs("init top_sub global"));
- assertIteration(F, "sub/a.txt", attrs("init foo top top_sub global"));
+ assertIteration(F, "sub/.gitattributes", attrs("init top_sub"));
+ assertIteration(F, "sub/a.txt", attrs("init foo top top_sub"));
assertIteration(D, "sub/sub", attrs("init subsub2 top_sub global"));
assertIteration(F, "sub/sub/b.txt",
- attrs("init foo subsub2 subsub top top_sub global"));
+ attrs("init foo subsub top top_sub"));
+ endWalk();
+ }
+
+ @Test
+ public void testNestedMatchNot() throws Exception {
+ setupRepo(null, null, "*.xml xml\n*.jar jar", null);
+ writeTrashFile("foo.xml/bar.jar", "b");
+ writeTrashFile("foo.xml/bar.xml", "bx");
+ writeTrashFile("sub/b.jar", "bj");
+ writeTrashFile("sub/b.xml", "bx");
+ // On foo.xml/bar.jar we must not have 'xml'
+ walk = beginWalk();
+ assertIteration(F, ".gitattributes");
+ assertIteration(D, "foo.xml", attrs("xml"));
+ assertIteration(F, "foo.xml/bar.jar", attrs("jar"));
+ assertIteration(F, "foo.xml/bar.xml", attrs("xml"));
+ assertIteration(D, "sub");
+ assertIteration(F, "sub/a.txt");
+ assertIteration(F, "sub/b.jar", attrs("jar"));
+ assertIteration(F, "sub/b.xml", attrs("xml"));
+ endWalk();
+ }
+
+ @Test
+ public void testNestedMatch() throws Exception {
+ // See also CGitAttributeTest.testNestedMatch()
+ setupRepo(null, null, "foo/ xml\nsub/foo/ sub\n*.jar jar", null);
+ writeTrashFile("foo/bar.jar", "b");
+ writeTrashFile("foo/bar.xml", "bx");
+ writeTrashFile("sub/b.jar", "bj");
+ writeTrashFile("sub/b.xml", "bx");
+ writeTrashFile("sub/foo/b.jar", "bf");
+ walk = beginWalk();
+ assertIteration(F, ".gitattributes");
+ assertIteration(D, "foo", attrs("xml"));
+ assertIteration(F, "foo/bar.jar", attrs("jar"));
+ assertIteration(F, "foo/bar.xml");
+ assertIteration(D, "sub");
+ assertIteration(F, "sub/a.txt");
+ assertIteration(F, "sub/b.jar", attrs("jar"));
+ assertIteration(F, "sub/b.xml");
+ assertIteration(D, "sub/foo", attrs("sub xml"));
+ assertIteration(F, "sub/foo/b.jar", attrs("jar"));
+ endWalk();
+ }
+
+ @Test
+ public void testNestedMatchRecursive() throws Exception {
+ setupRepo(null, null, "foo/** xml\n*.jar jar", null);
+ writeTrashFile("foo/bar.jar", "b");
+ writeTrashFile("foo/bar.xml", "bx");
+ writeTrashFile("sub/b.jar", "bj");
+ writeTrashFile("sub/b.xml", "bx");
+ writeTrashFile("sub/foo/b.jar", "bf");
+ // On foo.xml/bar.jar we must not have 'xml'
+ walk = beginWalk();
+ assertIteration(F, ".gitattributes");
+ assertIteration(D, "foo");
+ assertIteration(F, "foo/bar.jar", attrs("jar xml"));
+ assertIteration(F, "foo/bar.xml", attrs("xml"));
+ assertIteration(D, "sub");
+ assertIteration(F, "sub/a.txt");
+ assertIteration(F, "sub/b.jar", attrs("jar"));
+ assertIteration(F, "sub/b.xml");
+ assertIteration(D, "sub/foo");
+ assertIteration(F, "sub/foo/b.jar", attrs("jar"));
+ endWalk();
+ }
+
+ @Test
+ public void testStarMatchOnSlashNot() throws Exception {
+ setupRepo(null, null, "s*xt bar", null);
+ writeTrashFile("sub/a.txt", "1");
+ writeTrashFile("foo/sext", "2");
+ writeTrashFile("foo/s.txt", "3");
+ walk = beginWalk();
+ assertIteration(F, ".gitattributes");
+ assertIteration(D, "foo");
+ assertIteration(F, "foo/s.txt", attrs("bar"));
+ assertIteration(F, "foo/sext", attrs("bar"));
+ assertIteration(D, "sub");
+ assertIteration(F, "sub/a.txt");
+ endWalk();
+ }
+
+ @Test
+ public void testPrefixMatchNot() throws Exception {
+ setupRepo(null, null, "sub/new bar", null);
+ writeTrashFile("sub/new/foo.txt", "1");
+ walk = beginWalk();
+ assertIteration(F, ".gitattributes");
+ assertIteration(D, "sub");
+ assertIteration(F, "sub/a.txt");
+ assertIteration(D, "sub/new", attrs("bar"));
+ assertIteration(F, "sub/new/foo.txt");
+ endWalk();
+ }
+
+ @Test
+ public void testComplexPathMatch() throws Exception {
+ setupRepo(null, null, "s[t-v]b/n[de]w bar", null);
+ writeTrashFile("sub/new/foo.txt", "1");
+ writeTrashFile("sub/ndw", "2");
+ walk = beginWalk();
+ assertIteration(F, ".gitattributes");
+ assertIteration(D, "sub");
+ assertIteration(F, "sub/a.txt");
+ assertIteration(F, "sub/ndw", attrs("bar"));
+ assertIteration(D, "sub/new", attrs("bar"));
+ assertIteration(F, "sub/new/foo.txt");
+ endWalk();
+ }
+
+ @Test
+ public void testStarPathMatch() throws Exception {
+ setupRepo(null, null, "sub/new/* bar", null);
+ writeTrashFile("sub/new/foo.txt", "1");
+ writeTrashFile("sub/new/lower/foo.txt", "2");
+ walk = beginWalk();
+ assertIteration(F, ".gitattributes");
+ assertIteration(D, "sub");
+ assertIteration(F, "sub/a.txt");
+ assertIteration(D, "sub/new");
+ assertIteration(F, "sub/new/foo.txt", attrs("bar"));
+ assertIteration(D, "sub/new/lower", attrs("bar"));
+ assertIteration(F, "sub/new/lower/foo.txt");
+ endWalk();
+ }
+
+ @Test
+ public void testDirectoryMatchSubSimple() throws Exception {
+ setupRepo(null, null, "sub/new/ bar", null);
+ writeTrashFile("sub/new/foo.txt", "1");
+ writeTrashFile("foo/sub/new/foo.txt", "2");
+ writeTrashFile("sub/sub/new/foo.txt", "3");
+ walk = beginWalk();
+ assertIteration(F, ".gitattributes");
+ assertIteration(D, "foo");
+ assertIteration(D, "foo/sub");
+ assertIteration(D, "foo/sub/new");
+ assertIteration(F, "foo/sub/new/foo.txt");
+ assertIteration(D, "sub");
+ assertIteration(F, "sub/a.txt");
+ assertIteration(D, "sub/new", attrs("bar"));
+ assertIteration(F, "sub/new/foo.txt");
+ assertIteration(D, "sub/sub");
+ assertIteration(D, "sub/sub/new");
+ assertIteration(F, "sub/sub/new/foo.txt");
+ endWalk();
+ }
+
+ @Test
+ public void testDirectoryMatchSubRecursive() throws Exception {
+ setupRepo(null, null, "**/sub/new/ bar", null);
+ writeTrashFile("sub/new/foo.txt", "1");
+ writeTrashFile("foo/sub/new/foo.txt", "2");
+ walk = beginWalk();
+ assertIteration(F, ".gitattributes");
+ assertIteration(D, "foo");
+ assertIteration(D, "foo/sub");
+ assertIteration(D, "foo/sub/new", attrs("bar"));
+ assertIteration(F, "foo/sub/new/foo.txt");
+ assertIteration(D, "sub");
+ assertIteration(F, "sub/a.txt");
+ assertIteration(D, "sub/new", attrs("bar"));
+ assertIteration(F, "sub/new/foo.txt");
+ endWalk();
+ }
+
+ @Test
+ public void testDirectoryMatchSubComplex() throws Exception {
+ setupRepo(null, null, "s[uv]b/n*/ bar", null);
+ writeTrashFile("sub/new/foo.txt", "1");
+ writeTrashFile("foo/sub/new/foo.txt", "2");
+ walk = beginWalk();
+ assertIteration(F, ".gitattributes");
+ assertIteration(D, "foo");
+ assertIteration(D, "foo/sub");
+ assertIteration(D, "foo/sub/new");
+ assertIteration(F, "foo/sub/new/foo.txt");
+ assertIteration(D, "sub");
+ assertIteration(F, "sub/a.txt");
+ assertIteration(D, "sub/new", attrs("bar"));
+ assertIteration(F, "sub/new/foo.txt");
+ endWalk();
+ }
+
+ @Test
+ public void testDirectoryMatch() throws Exception {
+ setupRepo(null, null, "new/ bar", null);
+ writeTrashFile("sub/new/foo.txt", "1");
+ writeTrashFile("foo/sub/new/foo.txt", "2");
+ writeTrashFile("foo/new", "3");
+ walk = beginWalk();
+ assertIteration(F, ".gitattributes");
+ assertIteration(D, "foo");
+ assertIteration(F, "foo/new");
+ assertIteration(D, "foo/sub");
+ assertIteration(D, "foo/sub/new", attrs("bar"));
+ assertIteration(F, "foo/sub/new/foo.txt");
+ assertIteration(D, "sub");
+ assertIteration(F, "sub/a.txt");
+ assertIteration(D, "sub/new", attrs("bar"));
+ assertIteration(F, "sub/new/foo.txt");
endWalk();
}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesMatcherTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesMatcherTest.java
index e8dd952324..23c416a45b 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesMatcherTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesMatcherTest.java
@@ -109,16 +109,16 @@ public class AttributesMatcherTest {
pattern = "/src/ne?";
assertMatched(pattern, "/src/new/");
assertMatched(pattern, "/src/new");
- assertMatched(pattern, "/src/new/a.c");
- assertMatched(pattern, "/src/new/a/a.c");
+ assertNotMatched(pattern, "/src/new/a.c");
+ assertNotMatched(pattern, "/src/new/a/a.c");
assertNotMatched(pattern, "/src/new.c");
//Test name-only fnmatcher matches
pattern = "ne?";
assertMatched(pattern, "/src/new/");
assertMatched(pattern, "/src/new");
- assertMatched(pattern, "/src/new/a.c");
- assertMatched(pattern, "/src/new/a/a.c");
+ assertNotMatched(pattern, "/src/new/a.c");
+ assertNotMatched(pattern, "/src/new/a/a.c");
assertMatched(pattern, "/neb");
assertNotMatched(pattern, "/src/new.c");
}
@@ -169,16 +169,16 @@ public class AttributesMatcherTest {
pattern = "/src/ne?";
assertMatched(pattern, "src/new/");
assertMatched(pattern, "src/new");
- assertMatched(pattern, "src/new/a.c");
- assertMatched(pattern, "src/new/a/a.c");
+ assertNotMatched(pattern, "src/new/a.c");
+ assertNotMatched(pattern, "src/new/a/a.c");
assertNotMatched(pattern, "src/new.c");
//Test name-only fnmatcher matches
pattern = "ne?";
assertMatched(pattern, "src/new/");
assertMatched(pattern, "src/new");
- assertMatched(pattern, "src/new/a.c");
- assertMatched(pattern, "src/new/a/a.c");
+ assertNotMatched(pattern, "src/new/a.c");
+ assertNotMatched(pattern, "src/new/a/a.c");
assertMatched(pattern, "neb");
assertNotMatched(pattern, "src/new.c");
}
@@ -197,35 +197,50 @@ public class AttributesMatcherTest {
pattern = "/src/new";
assertMatched(pattern, "/src/new/");
assertMatched(pattern, "/src/new");
- assertMatched(pattern, "/src/new/a.c");
- assertMatched(pattern, "/src/new/a/a.c");
+ assertNotMatched(pattern, "/src/new/a.c");
+ assertNotMatched(pattern, "/src/new/a/a.c");
assertNotMatched(pattern, "/src/new.c");
//Test child directory is matched, slash after name
pattern = "/src/new/";
assertMatched(pattern, "/src/new/");
- assertMatched(pattern, "/src/new/a.c");
- assertMatched(pattern, "/src/new/a/a.c");
+ assertNotMatched(pattern, "/src/new/a.c");
+ assertNotMatched(pattern, "/src/new/a/a.c");
assertNotMatched(pattern, "/src/new");
assertNotMatched(pattern, "/src/new.c");
//Test directory is matched by name only
pattern = "b1";
- assertMatched(pattern, "/src/new/a/b1/a.c");
+ assertNotMatched(pattern, "/src/new/a/b1/a.c");
assertNotMatched(pattern, "/src/new/a/b2/file.c");
assertNotMatched(pattern, "/src/new/a/bb1/file.c");
assertNotMatched(pattern, "/src/new/a/file.c");
+ assertNotMatched(pattern, "/src/new/a/bb1");
+ assertMatched(pattern, "/src/new/a/b1");
}
@Test
public void testTrailingSlash() {
String pattern = "/src/";
assertMatched(pattern, "/src/");
- assertMatched(pattern, "/src/new");
- assertMatched(pattern, "/src/new/a.c");
- assertMatched(pattern, "/src/a.c");
+ assertNotMatched(pattern, "/src/new");
+ assertNotMatched(pattern, "/src/new/a.c");
+ assertNotMatched(pattern, "/src/a.c");
assertNotMatched(pattern, "/src");
assertNotMatched(pattern, "/srcA/");
+
+ pattern = "src/";
+ assertMatched(pattern, "src/");
+ assertMatched(pattern, "/src/");
+ assertNotMatched(pattern, "src");
+ assertNotMatched(pattern, "/src/new");
+ assertNotMatched(pattern, "/src/new/a.c");
+ assertNotMatched(pattern, "/src/a.c");
+ assertNotMatched(pattern, "foo/src/a.c");
+ assertNotMatched(pattern, "foo/src/bar/a.c");
+ assertNotMatched(pattern, "foo/src/bar/src");
+ assertMatched(pattern, "foo/src/");
+ assertMatched(pattern, "foo/src/bar/src/");
}
@Test
@@ -239,51 +254,58 @@ public class AttributesMatcherTest {
assertMatched(pattern, "/src/test.stp");
assertNotMatched(pattern, "/test.stp1");
assertNotMatched(pattern, "/test.astp");
+ assertNotMatched(pattern, "test.stp/foo.bar");
+ assertMatched(pattern, "test.stp");
+ assertMatched(pattern, "test.stp/");
+ assertMatched(pattern, "test.stp/test.stp");
//Test matches for name-only, applies to file name or folder name
pattern = "src";
assertMatched(pattern, "/src");
assertMatched(pattern, "/src/");
- assertMatched(pattern, "/src/a.c");
- assertMatched(pattern, "/src/new/a.c");
- assertMatched(pattern, "/new/src/a.c");
+ assertNotMatched(pattern, "/src/a.c");
+ assertNotMatched(pattern, "/src/new/a.c");
+ assertNotMatched(pattern, "/new/src/a.c");
assertMatched(pattern, "/file/src");
//Test matches for name-only, applies only to folder names
pattern = "src/";
- assertMatched(pattern, "/src/");
- assertMatched(pattern, "/src/a.c");
- assertMatched(pattern, "/src/new/a.c");
- assertMatched(pattern, "/new/src/a.c");
+ assertNotMatched(pattern, "/src/a.c");
+ assertNotMatched(pattern, "/src/new/a.c");
+ assertNotMatched(pattern, "/new/src/a.c");
assertNotMatched(pattern, "/src");
assertNotMatched(pattern, "/file/src");
+ assertMatched(pattern, "/file/src/");
//Test matches for name-only, applies to file name or folder name
//With a small wildcard
pattern = "?rc";
- assertMatched(pattern, "/src/a.c");
- assertMatched(pattern, "/src/new/a.c");
- assertMatched(pattern, "/new/src/a.c");
+ assertNotMatched(pattern, "/src/a.c");
+ assertNotMatched(pattern, "/src/new/a.c");
+ assertNotMatched(pattern, "/new/src/a.c");
+ assertMatched(pattern, "/new/src/");
assertMatched(pattern, "/file/src");
assertMatched(pattern, "/src/");
//Test matches for name-only, applies to file name or folder name
//With a small wildcard
pattern = "?r[a-c]";
- assertMatched(pattern, "/src/a.c");
- assertMatched(pattern, "/src/new/a.c");
- assertMatched(pattern, "/new/src/a.c");
+ assertNotMatched(pattern, "/src/a.c");
+ assertNotMatched(pattern, "/src/new/a.c");
+ assertNotMatched(pattern, "/new/src/a.c");
assertMatched(pattern, "/file/src");
assertMatched(pattern, "/src/");
- assertMatched(pattern, "/srb/a.c");
- assertMatched(pattern, "/grb/new/a.c");
- assertMatched(pattern, "/new/crb/a.c");
+ assertNotMatched(pattern, "/srb/a.c");
+ assertNotMatched(pattern, "/grb/new/a.c");
+ assertNotMatched(pattern, "/new/crb/a.c");
assertMatched(pattern, "/file/3rb");
assertMatched(pattern, "/xrb/");
- assertMatched(pattern, "/3ra/a.c");
- assertMatched(pattern, "/5ra/new/a.c");
- assertMatched(pattern, "/new/1ra/a.c");
+ assertNotMatched(pattern, "/3ra/a.c");
+ assertNotMatched(pattern, "/5ra/new/a.c");
+ assertNotMatched(pattern, "/new/1ra/a.c");
+ assertNotMatched(pattern, "/new/1ra/a.c/");
assertMatched(pattern, "/file/dra");
+ assertMatched(pattern, "/file/dra/");
assertMatched(pattern, "/era/");
assertNotMatched(pattern, "/crg");
assertNotMatched(pattern, "/cr3");
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/CGitAttributesTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/CGitAttributesTest.java
index e0a6d16156..c8ff6c8752 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/CGitAttributesTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/CGitAttributesTest.java
@@ -202,6 +202,13 @@ public class CGitAttributesTest extends RepositoryTestCase {
}
@Test
+ public void testBug508568() throws Exception {
+ createFiles("foo.xml/bar.jar", "sub/foo.xml/bar.jar");
+ writeTrashFile(".gitattributes", "*.xml xml\n" + "*.jar jar\n");
+ assertSameAsCGit();
+ }
+
+ @Test
public void testRelativePath() throws Exception {
createFiles("sub/foo.txt");
writeTrashFile("sub/.gitattributes", "sub/** sub\n" + "*.txt txt\n");
@@ -225,6 +232,44 @@ public class CGitAttributesTest extends RepositoryTestCase {
}
@Test
+ public void testNestedMatch() throws Exception {
+ // This is an interesting test. At the time of this writing, the
+ // gitignore documentation says: "In other words, foo/ will match a
+ // directory foo AND PATHS UNDERNEATH IT, but will not match a regular
+ // file or a symbolic link foo". (Emphasis added.) And gitattributes is
+ // supposed to follow the same rules. But the documentation appears to
+ // lie: C-git will *not* apply the attribute "xml" to *any* files in
+ // any subfolder "foo" here. It will only apply the "jar" attribute
+ // to the three *.jar files.
+ //
+ // The point is probably that ignores are handled top-down, and once a
+ // directory "foo" is matched (here: on paths "foo" and "sub/foo" by
+ // pattern "foo/"), the directory is excluded and the gitignore
+ // documentation also says: "It is not possible to re-include a file if
+ // a parent directory of that file is excluded." So once the pattern
+ // "foo/" has matched, it appears as if everything beneath would also be
+ // matched.
+ //
+ // But not so for gitattributes! The foo/ rule only matches the
+ // directory itself, but not anything beneath.
+ createFiles("foo/bar.jar", "foo/bar.xml", "sub/b.jar", "sub/b.xml",
+ "sub/foo/b.jar");
+ writeTrashFile(".gitattributes",
+ "foo/ xml\n" + "sub/foo/ sub\n" + "*.jar jar\n");
+ assertSameAsCGit();
+ }
+
+ @Test
+ public void testNestedMatchWithWildcard() throws Exception {
+ // See above.
+ createFiles("foo/bar.jar", "foo/bar.xml", "sub/b.jar", "sub/b.xml",
+ "sub/foo/b.jar");
+ writeTrashFile(".gitattributes",
+ "**/foo/ xml\n" + "*/foo/ sub\n" + "*.jar jar\n");
+ assertSameAsCGit();
+ }
+
+ @Test
public void testNestedMatchRecursive() throws Exception {
createFiles("foo/bar.jar", "foo/bar.xml", "sub/b.jar", "sub/b.xml",
"sub/foo/b.jar");
@@ -238,4 +283,53 @@ public class CGitAttributesTest extends RepositoryTestCase {
writeTrashFile(".gitattributes", "s*xt bar");
assertSameAsCGit();
}
+
+ @Test
+ public void testPrefixMatchNot() throws Exception {
+ createFiles("src/new/foo.txt");
+ writeTrashFile(".gitattributes", "src/new bar\n");
+ assertSameAsCGit();
+ }
+
+ @Test
+ public void testComplexPathMatchNot() throws Exception {
+ createFiles("src/new/foo.txt", "src/ndw");
+ writeTrashFile(".gitattributes", "s[p-s]c/n[de]w bar\n");
+ assertSameAsCGit();
+ }
+
+ @Test
+ public void testStarPathMatchNot() throws Exception {
+ createFiles("src/new/foo.txt", "src/ndw");
+ writeTrashFile(".gitattributes", "src/* bar\n");
+ assertSameAsCGit();
+ }
+
+ @Test
+ public void testDirectoryMatchSubSimple() throws Exception {
+ createFiles("src/new/foo.txt", "foo/src/new/foo.txt", "sub/src/new");
+ writeTrashFile(".gitattributes", "src/new/ bar\n");
+ assertSameAsCGit();
+ }
+
+ @Test
+ public void testDirectoryMatchSubRecursive() throws Exception {
+ createFiles("src/new/foo.txt", "foo/src/new/foo.txt", "sub/src/new");
+ writeTrashFile(".gitattributes", "**/src/new/ bar\n");
+ assertSameAsCGit();
+ }
+
+ @Test
+ public void testDirectoryMatchSubComplex() throws Exception {
+ createFiles("src/new/foo.txt", "foo/src/new/foo.txt", "sub/src/new");
+ writeTrashFile(".gitattributes", "s[rs]c/n*/ bar\n");
+ assertSameAsCGit();
+ }
+
+ @Test
+ public void testDirectoryMatch() throws Exception {
+ createFiles("src/new/foo.txt", "foo/src/new/foo.txt", "sub/src/new");
+ writeTrashFile(".gitattributes", "new/ bar\n");
+ assertSameAsCGit();
+ }
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java
index d365171ed1..68b1bd9e29 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java
@@ -209,7 +209,8 @@ public class DescribeCommand extends GitCommand<String> {
// Find the first tag that matches one of the matchers; precedence according to matcher definition order
for (IMatcher matcher : matchers) {
Optional<Ref> match = tags.stream()
- .filter(tag -> matcher.matches(tag.getName(), false))
+ .filter(tag -> matcher.matches(tag.getName(), false,
+ false))
.findFirst();
if (match.isPresent()) {
return match;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesRule.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesRule.java
index b88a16e86c..3cf5de8be5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesRule.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesRule.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2010, Red Hat Inc.
+ * Copyright (C) 2010, 2017 Red Hat Inc.
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
@@ -210,7 +210,7 @@ public class AttributesRule {
return false;
if (relativeTarget.length() == 0)
return false;
- boolean match = matcher.matches(relativeTarget, isDirectory);
+ boolean match = matcher.matches(relativeTarget, isDirectory, true);
return match;
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/FastIgnoreRule.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/FastIgnoreRule.java
index ef67d49419..7298a082c7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/FastIgnoreRule.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/FastIgnoreRule.java
@@ -155,7 +155,7 @@ public class FastIgnoreRule {
return false;
if (path.length() == 0)
return false;
- boolean match = matcher.matches(path, directory);
+ boolean match = matcher.matches(path, directory, false);
return match;
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/IMatcher.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/IMatcher.java
index 61f7b83400..5b184cb19f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/IMatcher.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/IMatcher.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2014, Andrey Loskutov <loskutov@gmx.de>
+ * Copyright (C) 2014, 2017 Andrey Loskutov <loskutov@gmx.de>
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
@@ -52,7 +52,8 @@ public interface IMatcher {
*/
public static final IMatcher NO_MATCH = new IMatcher() {
@Override
- public boolean matches(String path, boolean assumeDirectory) {
+ public boolean matches(String path, boolean assumeDirectory,
+ boolean pathMatch) {
return false;
}
@@ -71,9 +72,14 @@ public interface IMatcher {
* @param assumeDirectory
* true to assume this path as directory (even if it doesn't end
* with a slash)
+ * @param pathMatch
+ * {@code true} if the match is for the full path: prefix-only
+ * matches are not allowed, and {@link NameMatcher}s must match
+ * only the last component (if they can -- they may not, if they
+ * are anchored at the beginning)
* @return true if this matcher pattern matches given string
*/
- boolean matches(String path, boolean assumeDirectory);
+ boolean matches(String path, boolean assumeDirectory, boolean pathMatch);
/**
* Matches only part of given string
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/NameMatcher.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/NameMatcher.java
index 00651237db..9667837a41 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/NameMatcher.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/NameMatcher.java
@@ -64,26 +64,59 @@ public class NameMatcher extends AbstractMatcher {
pattern = Strings.deleteBackslash(pattern);
}
beginning = pattern.length() == 0 ? false : pattern.charAt(0) == slash;
- if (!beginning)
+ if (!beginning) {
this.subPattern = pattern;
- else
+ } else {
this.subPattern = pattern.substring(1);
+ }
}
@Override
- public boolean matches(String path, boolean assumeDirectory) {
- int end = 0;
- int firstChar = 0;
- do {
- firstChar = getFirstNotSlash(path, end);
- end = getFirstSlash(path, firstChar);
- boolean match = matches(path, firstChar, end, assumeDirectory);
- if (match)
+ public boolean matches(String path, boolean assumeDirectory,
+ boolean pathMatch) {
+ // A NameMatcher's pattern does not contain a slash.
+ int start = 0;
+ int stop = path.length();
+ if (stop > 0 && path.charAt(0) == slash) {
+ start++;
+ }
+ if (pathMatch) {
+ // Can match only after the last slash
+ int lastSlash = path.lastIndexOf(slash, stop - 1);
+ if (lastSlash == stop - 1) {
+ // Skip trailing slash
+ lastSlash = path.lastIndexOf(slash, lastSlash - 1);
+ stop--;
+ }
+ boolean match;
+ if (lastSlash < start) {
+ match = matches(path, start, stop, assumeDirectory);
+ } else {
+ // Can't match if the path contains a slash if the pattern is
+ // anchored at the beginning
+ match = !beginning
+ && matches(path, lastSlash + 1, stop, assumeDirectory);
+ }
+ if (match && dirOnly) {
+ match = assumeDirectory;
+ }
+ return match;
+ }
+ while (start < stop) {
+ int end = path.indexOf(slash, start);
+ if (end < 0) {
+ end = stop;
+ }
+ if (end > start && matches(path, start, end, assumeDirectory)) {
// make sure the directory matches: either if we are done with
// segment and there is next one, or if the directory is assumed
- return !dirOnly ? true : (end > 0 && end != path.length())
- || assumeDirectory;
- } while (!beginning && end != path.length());
+ return !dirOnly || assumeDirectory || end < stop;
+ }
+ if (beginning) {
+ break;
+ }
+ start = end + 1;
+ }
return false;
}
@@ -92,25 +125,18 @@ public class NameMatcher extends AbstractMatcher {
boolean assumeDirectory) {
// faster local access, same as in string.indexOf()
String s = subPattern;
- if (s.length() != (endExcl - startIncl))
+ int length = s.length();
+ if (length != (endExcl - startIncl)) {
return false;
- for (int i = 0; i < s.length(); i++) {
+ }
+ for (int i = 0; i < length; i++) {
char c1 = s.charAt(i);
char c2 = segment.charAt(i + startIncl);
- if (c1 != c2)
+ if (c1 != c2) {
return false;
+ }
}
return true;
}
- private int getFirstNotSlash(String s, int start) {
- int slashIdx = s.indexOf(slash, start);
- return slashIdx == start ? start + 1 : start;
- }
-
- private int getFirstSlash(String s, int start) {
- int slashIdx = s.indexOf(slash, start);
- return slashIdx == -1 ? s.length() : slashIdx;
- }
-
}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/PathMatcher.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/PathMatcher.java
index ce9ad80fb0..85073ecfb2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/PathMatcher.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/PathMatcher.java
@@ -52,7 +52,6 @@ import java.util.ArrayList;
import java.util.List;
import org.eclipse.jgit.errors.InvalidPatternException;
-import org.eclipse.jgit.ignore.FastIgnoreRule;
import org.eclipse.jgit.ignore.internal.Strings.PatternState;
/**
@@ -68,9 +67,10 @@ public class PathMatcher extends AbstractMatcher {
private final char slash;
- private boolean beginning;
+ private final boolean beginning;
- PathMatcher(String pattern, Character pathSeparator, boolean dirOnly)
+ private PathMatcher(String pattern, Character pathSeparator,
+ boolean dirOnly)
throws InvalidPatternException {
super(pattern, dirOnly);
slash = getPathSeparator(pathSeparator);
@@ -87,7 +87,7 @@ public class PathMatcher extends AbstractMatcher {
&& count(path, slash, true) > 0;
}
- static private List<IMatcher> createMatchers(List<String> segments,
+ private static List<IMatcher> createMatchers(List<String> segments,
Character pathSeparator, boolean dirOnly)
throws InvalidPatternException {
List<IMatcher> matchers = new ArrayList<>(segments.size());
@@ -171,10 +171,12 @@ public class PathMatcher extends AbstractMatcher {
}
@Override
- public boolean matches(String path, boolean assumeDirectory) {
- if (matchers == null)
- return simpleMatch(path, assumeDirectory);
- return iterate(path, 0, path.length(), assumeDirectory);
+ public boolean matches(String path, boolean assumeDirectory,
+ boolean pathMatch) {
+ if (matchers == null) {
+ return simpleMatch(path, assumeDirectory, pathMatch);
+ }
+ return iterate(path, 0, path.length(), assumeDirectory, pathMatch);
}
/*
@@ -182,31 +184,31 @@ public class PathMatcher extends AbstractMatcher {
* wildcards or single segments (mean: this is multi-segment path which must
* be at the beginning of the another string)
*/
- private boolean simpleMatch(String path, boolean assumeDirectory) {
+ private boolean simpleMatch(String path, boolean assumeDirectory,
+ boolean pathMatch) {
boolean hasSlash = path.indexOf(slash) == 0;
- if (beginning && !hasSlash)
+ if (beginning && !hasSlash) {
path = slash + path;
-
- if (!beginning && hasSlash)
+ }
+ if (!beginning && hasSlash) {
path = path.substring(1);
-
- if (path.equals(pattern))
- // Exact match
- if (dirOnly && !assumeDirectory)
- // Directory expectations not met
- return false;
- else
- // Directory expectations met
- return true;
-
+ }
+ if (path.equals(pattern)) {
+ // Exact match: must meet directory expectations
+ return !dirOnly || assumeDirectory;
+ }
/*
* Add slashes for startsWith check. This avoids matching e.g.
* "/src/new" to /src/newfile" but allows "/src/new" to match
* "/src/new/newfile", as is the git standard
*/
- if (path.startsWith(pattern + FastIgnoreRule.PATH_SEPARATOR))
+ String prefix = pattern + slash;
+ if (pathMatch) {
+ return path.equals(prefix) && (!dirOnly || assumeDirectory);
+ }
+ if (path.startsWith(prefix)) {
return true;
-
+ }
return false;
}
@@ -217,8 +219,8 @@ public class PathMatcher extends AbstractMatcher {
"Path matcher works only on entire paths"); //$NON-NLS-1$
}
- boolean iterate(final String path, final int startIncl, final int endExcl,
- boolean assumeDirectory) {
+ private boolean iterate(final String path, final int startIncl,
+ final int endExcl, boolean assumeDirectory, boolean pathMatch) {
int matcher = 0;
int right = startIncl;
boolean match = false;
@@ -256,14 +258,26 @@ public class PathMatcher extends AbstractMatcher {
continue;
}
if (match) {
- if (matchers.get(matcher) == WILD) {
+ boolean wasWild = matchers.get(matcher) == WILD;
+ if (wasWild) {
lastWildmatch = matcher;
// ** can match *nothing*: a/**/b match also a/b
right = left - 1;
}
matcher++;
if (matcher == matchers.size()) {
- return true;
+ // We had a prefix match here.
+ if (!pathMatch) {
+ return true;
+ } else {
+ if (right == endExcl - 1) {
+ // Extra slash at the end: actually a full match.
+ // Must meet directory expectations
+ return !dirOnly || assumeDirectory;
+ }
+ // Prefix matches only if pattern ended with /**
+ return wasWild;
+ }
}
} else if (lastWildmatch != -1) {
matcher = lastWildmatch + 1;
@@ -274,7 +288,8 @@ public class PathMatcher extends AbstractMatcher {
}
}
- boolean matches(int matcherIdx, String path, int startIncl, int endExcl,
+ private boolean matches(int matcherIdx, String path, int startIncl,
+ int endExcl,
boolean assumeDirectory) {
IMatcher matcher = matchers.get(matcherIdx);
return matcher.matches(path, startIncl, endExcl, assumeDirectory);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/WildMatcher.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/WildMatcher.java
index 93ea13c726..363b3cee84 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/WildMatcher.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/WildMatcher.java
@@ -62,7 +62,8 @@ public final class WildMatcher extends AbstractMatcher {
}
@Override
- public final boolean matches(String path, boolean assumeDirectory) {
+ public final boolean matches(String path, boolean assumeDirectory,
+ boolean pathMatch) {
return true;
}