Commit 6720eef938d7614cd7a9fd2138a27da3667d62cf

Vicent Marti 2014-04-07T11:22:23

Merge pull request #2249 from libgit2/rb/starstar-fnmatch Add support for ** matches in ignores

diff --git a/src/fnmatch.c b/src/fnmatch.c
index e3e47f3..3846bab 100644
--- a/src/fnmatch.c
+++ b/src/fnmatch.c
@@ -30,6 +30,7 @@ p_fnmatchx(const char *pattern, const char *string, int flags, size_t recurs)
 		const char *stringstart;
 		char *newp;
 		char c, test;
+		int recurs_flags = flags & ~FNM_PERIOD;
 
 		if (recurs-- == 0)
 				return FNM_NORES;
@@ -53,9 +54,15 @@ p_fnmatchx(const char *pattern, const char *string, int flags, size_t recurs)
 						break;
 				case '*':
 						c = *pattern;
-						/* Collapse multiple stars. */
-						while (c == '*')
+
+						/* Let '**' override PATHNAME match for this segment.
+						 * It will be restored if/when we recurse below.
+						 */
+						if (c == '*') {
+							flags &= ~FNM_PATHNAME;
+							while (c == '*')
 								c = *++pattern;
+						}
 
 						if (*string == '.' && (flags & FNM_PERIOD) &&
 							(string == stringstart ||
@@ -80,7 +87,7 @@ p_fnmatchx(const char *pattern, const char *string, int flags, size_t recurs)
 						while ((test = *string) != EOS) {
 								int e;
 
-								e = p_fnmatchx(pattern, string, flags & ~FNM_PERIOD, recurs);
+								e = p_fnmatchx(pattern, string, recurs_flags, recurs);
 								if (e != FNM_NOMATCH)
 										return e;
 								if (test == '/' && (flags & FNM_PATHNAME))
diff --git a/tests/attr/ignore.c b/tests/attr/ignore.c
index 0f945eb..4ed9238 100644
--- a/tests/attr/ignore.c
+++ b/tests/attr/ignore.c
@@ -16,13 +16,20 @@ void test_attr_ignore__cleanup(void)
 	g_repo = NULL;
 }
 
-void assert_is_ignored(bool expected, const char *filepath)
+void assert_is_ignored_(
+	bool expected, const char *filepath, const char *file, int line)
 {
-	int is_ignored;
+	int is_ignored = 0;
 
-	cl_git_pass(git_ignore_path_is_ignored(&is_ignored, g_repo, filepath));
-	cl_assert_equal_b(expected, is_ignored);
+	cl_git_pass_(
+		git_ignore_path_is_ignored(&is_ignored, g_repo, filepath), file, line);
+
+	clar__assert_equal(
+		file, line, "expected != is_ignored", 1, "%d",
+		(int)(expected != 0), (int)(is_ignored != 0));
 }
+#define assert_is_ignored(expected, filepath) \
+	assert_is_ignored_(expected, filepath, __FILE__, __LINE__)
 
 void test_attr_ignore__honor_temporary_rules(void)
 {
@@ -54,6 +61,58 @@ void test_attr_ignore__ignore_root(void)
 	assert_is_ignored(true, "NewFolder/NewFolder/File.txt");
 }
 
+void test_attr_ignore__full_paths(void)
+{
+	cl_git_rewritefile("attr/.gitignore", "Folder/*/Contained");
+
+	assert_is_ignored(true, "Folder/Middle/Contained");
+	assert_is_ignored(false, "Folder/Middle/More/More/Contained");
+
+	cl_git_rewritefile("attr/.gitignore", "Folder/**/Contained");
+
+	assert_is_ignored(true, "Folder/Middle/Contained");
+	assert_is_ignored(true, "Folder/Middle/More/More/Contained");
+
+	cl_git_rewritefile("attr/.gitignore", "Folder/**/Contained/*/Child");
+
+	assert_is_ignored(true, "Folder/Middle/Contained/Happy/Child");
+	assert_is_ignored(false, "Folder/Middle/Contained/Not/Happy/Child");
+	assert_is_ignored(true, "Folder/Middle/More/More/Contained/Happy/Child");
+	assert_is_ignored(false, "Folder/Middle/More/More/Contained/Not/Happy/Child");
+}
+
+void test_attr_ignore__leading_stars(void)
+{
+	cl_git_rewritefile(
+		"attr/.gitignore",
+		"*/onestar\n"
+		"**/twostars\n"
+		"*/parent1/kid1/*\n"
+		"**/parent2/kid2/*\n");
+
+	assert_is_ignored(true, "dir1/onestar");
+	assert_is_ignored(true, "dir1/onestar/child"); /* in ignored dir */
+	assert_is_ignored(false, "dir1/dir2/onestar");
+
+	assert_is_ignored(true, "dir1/twostars");
+	assert_is_ignored(true, "dir1/twostars/child"); /* in ignored dir */
+	assert_is_ignored(true, "dir1/dir2/twostars");
+	assert_is_ignored(true, "dir1/dir2/twostars/child"); /* in ignored dir */
+	assert_is_ignored(true, "dir1/dir2/dir3/twostars");
+
+	assert_is_ignored(true, "dir1/parent1/kid1/file");
+	assert_is_ignored(true, "dir1/parent1/kid1/file/inside/parent");
+	assert_is_ignored(false, "dir1/dir2/parent1/kid1/file");
+	assert_is_ignored(false, "dir1/parent1/file");
+	assert_is_ignored(false, "dir1/kid1/file");
+
+	assert_is_ignored(true, "dir1/parent2/kid2/file");
+	assert_is_ignored(true, "dir1/parent2/kid2/file/inside/parent");
+	assert_is_ignored(true, "dir1/dir2/parent2/kid2/file");
+	assert_is_ignored(true, "dir1/dir2/dir3/parent2/kid2/file");
+	assert_is_ignored(false, "dir1/parent2/file");
+	assert_is_ignored(false, "dir1/kid2/file");
+}
 
 void test_attr_ignore__skip_gitignore_directory(void)
 {
diff --git a/tests/clar_libgit2.h b/tests/clar_libgit2.h
index 9151112..3de80bf 100644
--- a/tests/clar_libgit2.h
+++ b/tests/clar_libgit2.h
@@ -11,11 +11,13 @@
  *
  * Use this wrapper around all `git_` library calls that return error codes!
  */
-#define cl_git_pass(expr) do { \
+#define cl_git_pass(expr) cl_git_pass_(expr, __FILE__, __LINE__)
+
+#define cl_git_pass_(expr, file, line) do { \
 	int _lg2_error; \
 	giterr_clear(); \
 	if ((_lg2_error = (expr)) != 0) \
-		cl_git_report_failure(_lg2_error, __FILE__, __LINE__, "Function call failed: " #expr); \
+		cl_git_report_failure(_lg2_error, file, line, "Function call failed: " #expr); \
 	} while (0)
 
 /**