Commit 05f9986a3555e3c5768e949743ed6dcdbd88252c

Patrick Steinhardt 2019-06-14T08:06:05

attr_file: convert to use `wildmatch` Upstream git has converted to use `wildmatch` instead of `fnmatch`. Convert our gitattributes logic to use `wildmatch` as the last user of `fnmatch`. Please, don't expect I know what I'm doing here: the fnmatch parser is one of the most fun things to play around with as it has a sh*tload of weird cases. In all honesty, I'm simply relying on our tests that are by now rather comprehensive in that area. The conversion actually fixes compatibility with how git.git parser "**" patterns when the given path does not contain any directory separators. Previously, a pattern "**.foo" erroneously wouldn't match a file "x.foo", while git.git would match. Remove the new-unused LEADINGDIR/NOLEADINGDIR flags for `git_attr_fnmatch`.

diff --git a/src/attr_file.c b/src/attr_file.c
index a2959ae..edcfbda 100644
--- a/src/attr_file.c
+++ b/src/attr_file.c
@@ -15,7 +15,7 @@
 #include "git2/tree.h"
 #include "blob.h"
 #include "index.h"
-#include "fnmatch.h"
+#include "wildmatch.h"
 #include <ctype.h>
 
 static void attr_file_free(git_attr_file *file)
@@ -402,18 +402,13 @@ bool git_attr_fnmatch__match(
 	}
 
 	if (match->flags & GIT_ATTR_FNMATCH_ICASE)
-		flags |= FNM_CASEFOLD;
-	if (match->flags & GIT_ATTR_FNMATCH_LEADINGDIR)
-		flags |= FNM_LEADING_DIR;
+		flags |= WM_CASEFOLD;
 
 	if (match->flags & GIT_ATTR_FNMATCH_FULLPATH) {
 		filename = relpath;
-		flags |= FNM_PATHNAME;
+		flags |= WM_PATHNAME;
 	} else {
 		filename = path->basename;
-
-		if (path->is_dir)
-			flags |= FNM_LEADING_DIR;
 	}
 
 	if ((match->flags & GIT_ATTR_FNMATCH_DIRECTORY) && !path->is_dir) {
@@ -428,8 +423,6 @@ bool git_attr_fnmatch__match(
 			path->basename == relpath)
 			return false;
 
-		flags |= FNM_LEADING_DIR;
-
 		/* fail match if this is a file with same name as ignored folder */
 		samename = (match->flags & GIT_ATTR_FNMATCH_ICASE) ?
 			!strcasecmp(match->pattern, relpath) :
@@ -438,10 +431,10 @@ bool git_attr_fnmatch__match(
 		if (samename)
 			return false;
 
-		return (p_fnmatch(match->pattern, relpath, flags) != FNM_NOMATCH);
+		return (wildmatch(match->pattern, relpath, flags) == WM_MATCH);
 	}
 
-	return (p_fnmatch(match->pattern, filename, flags) != FNM_NOMATCH);
+	return (wildmatch(match->pattern, filename, flags) == WM_MATCH);
 }
 
 bool git_attr_rule__match(
@@ -659,8 +652,6 @@ int git_attr_fnmatch__parse(
 
 	if (*pattern == '!' && (spec->flags & GIT_ATTR_FNMATCH_ALLOWNEG) != 0) {
 		spec->flags = spec->flags | GIT_ATTR_FNMATCH_NEGATIVE;
-		if ((spec->flags & GIT_ATTR_FNMATCH_NOLEADINGDIR) == 0)
-			spec->flags |= GIT_ATTR_FNMATCH_LEADINGDIR;
 		pattern++;
 	}
 
@@ -716,14 +707,6 @@ int git_attr_fnmatch__parse(
 		if (--slash_count <= 0)
 			spec->flags = spec->flags & ~GIT_ATTR_FNMATCH_FULLPATH;
 	}
-	if ((spec->flags & GIT_ATTR_FNMATCH_NOLEADINGDIR) == 0 &&
-		spec->length >= 2 &&
-		pattern[spec->length - 1] == '*' &&
-		pattern[spec->length - 2] == '/') {
-		spec->length -= 2;
-		spec->flags = spec->flags | GIT_ATTR_FNMATCH_LEADINGDIR;
-		/* leave FULLPATH match on, however */
-	}
 
 	if (context) {
 		char *slash = strrchr(context, '/');
diff --git a/src/attr_file.h b/src/attr_file.h
index fedf55a..7a45516 100644
--- a/src/attr_file.h
+++ b/src/attr_file.h
@@ -32,12 +32,9 @@
 #define GIT_ATTR_FNMATCH_MATCH_ALL	(1U << 8)
 #define GIT_ATTR_FNMATCH_ALLOWNEG   (1U << 9)
 #define GIT_ATTR_FNMATCH_ALLOWMACRO (1U << 10)
-#define GIT_ATTR_FNMATCH_LEADINGDIR (1U << 11)
-#define GIT_ATTR_FNMATCH_NOLEADINGDIR (1U << 12)
 
 #define GIT_ATTR_FNMATCH__INCOMING \
-	(GIT_ATTR_FNMATCH_ALLOWSPACE | GIT_ATTR_FNMATCH_ALLOWNEG | \
-	 GIT_ATTR_FNMATCH_ALLOWMACRO | GIT_ATTR_FNMATCH_NOLEADINGDIR)
+	(GIT_ATTR_FNMATCH_ALLOWSPACE | GIT_ATTR_FNMATCH_ALLOWNEG | GIT_ATTR_FNMATCH_ALLOWMACRO)
 
 typedef enum {
 	GIT_ATTR_FILE__IN_MEMORY   = 0,
diff --git a/src/ignore.c b/src/ignore.c
index 1d256c9..3f748b8 100644
--- a/src/ignore.c
+++ b/src/ignore.c
@@ -193,9 +193,7 @@ static int parse_ignore_file(
 		}
 
 		match->flags =
-		    GIT_ATTR_FNMATCH_ALLOWSPACE |
-		    GIT_ATTR_FNMATCH_ALLOWNEG |
-		    GIT_ATTR_FNMATCH_NOLEADINGDIR;
+		    GIT_ATTR_FNMATCH_ALLOWSPACE | GIT_ATTR_FNMATCH_ALLOWNEG;
 
 		if (!(error = git_attr_fnmatch__parse(
 			match, &attrs->pool, context, &scan)))
diff --git a/src/pathspec.c b/src/pathspec.c
index 0cd3fc4..9eb9523 100644
--- a/src/pathspec.c
+++ b/src/pathspec.c
@@ -85,8 +85,7 @@ int git_pathspec__vinit(
 		if (!match)
 			return -1;
 
-		match->flags = GIT_ATTR_FNMATCH_ALLOWSPACE |
-			GIT_ATTR_FNMATCH_ALLOWNEG | GIT_ATTR_FNMATCH_NOLEADINGDIR;
+		match->flags = GIT_ATTR_FNMATCH_ALLOWSPACE | GIT_ATTR_FNMATCH_ALLOWNEG;
 
 		ret = git_attr_fnmatch__parse(match, strpool, NULL, &pattern);
 		if (ret == GIT_ENOTFOUND) {
diff --git a/tests/ignore/path.c b/tests/ignore/path.c
index bfed297..9526995 100644
--- a/tests/ignore/path.c
+++ b/tests/ignore/path.c
@@ -230,6 +230,28 @@ void test_ignore_path__globs_and_path_delimiters(void)
 	assert_is_ignored(false, "_test/foo/bar/code/file");
 }
 
+void test_ignore_path__globs_without_star(void)
+{
+	cl_git_rewritefile(
+		"attr/.gitignore",
+		"*.foo\n"
+		"**.bar\n"
+	);
+
+	assert_is_ignored(true, ".foo");
+	assert_is_ignored(true, "xyz.foo");
+	assert_is_ignored(true, ".bar");
+	assert_is_ignored(true, "x.bar");
+	assert_is_ignored(true, "xyz.bar");
+
+	assert_is_ignored(true, "test/.foo");
+	assert_is_ignored(true, "test/x.foo");
+	assert_is_ignored(true, "test/xyz.foo");
+	assert_is_ignored(true, "test/.bar");
+	assert_is_ignored(true, "test/x.bar");
+	assert_is_ignored(true, "test/xyz.bar");
+}
+
 void test_ignore_path__skip_gitignore_directory(void)
 {
 	cl_git_rewritefile("attr/.git/info/exclude", "/NewFolder\n/NewFolder/NewFolder");