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>
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 50d020c..0717379 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 void testRelativePaths() throws Exception {
 		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 e8dd952..23c416a 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 void testFileNameWildcards() {
 		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 void testTargetWithoutLeadingSlash() {
 		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 void testParentDirectoryGitAttributes() {
 		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 void testNameOnlyMatches() {
 		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 e0a6d16..c8ff6c8 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 @@ private void assertSameAsCGit() throws Exception {
 	}
 
 	@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 void testNestedMatchNot() throws Exception {
 	}
 
 	@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 void testStarMatchOnSlashNot() throws Exception {
 		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 d365171..68b1bd9 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 @@ private Optional<Ref> getBestMatch(List<Ref> tags) {
 			// 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 b88a16e..3cf5de8 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 boolean isMatch(String relativeTarget, boolean isDirectory) {
 			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 ef67d49..7298a08 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 boolean isMatch(String path, boolean directory) {
 			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 61f7b83..5b184cb 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 boolean matches(String segment, int startIncl, int endExcl,
 	 * @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 0065123..9667837 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 boolean matches(String segment, int startIncl, int endExcl,
 			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 ce9ad80..85073ec 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.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 @@ private boolean isSimplePathWithSegments(String path) {
 				&& 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 @@ private static IMatcher createNameMatcher0(String segment,
 	}
 
 	@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 boolean matches(String path, boolean assumeDirectory) {
 	 * 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 boolean matches(String segment, int startIncl, int endExcl,
 				"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 @@ boolean iterate(final String path, final int startIncl, final int endExcl,
 				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 @@ boolean iterate(final String path, final int startIncl, final int endExcl,
 		}
 	}
 
-	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 93ea13c..363b3ce 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 @@ private WildMatcher() {
 	}
 
 	@Override
-	public final boolean matches(String path, boolean assumeDirectory) {
+	public final boolean matches(String path, boolean assumeDirectory,
+			boolean pathMatch) {
 		return true;
 	}