Commit 4f3586034b3db317d360de87bd962de1ef3d524e

Patrick Steinhardt 2015-03-24T16:33:50

ignore: fix negative ignores without wildcards.

diff --git a/src/ignore.c b/src/ignore.c
index dd299f0..3a5efed 100644
--- a/src/ignore.c
+++ b/src/ignore.c
@@ -11,6 +11,41 @@
 #define GIT_IGNORE_DEFAULT_RULES ".\n..\n.git\n"
 
 /**
+ * A negative ignore pattern can match a positive one without
+ * wildcards if its pattern equals the tail of the positive
+ * pattern. Thus
+ *
+ * foo/bar
+ * !bar
+ *
+ * would result in foo/bar being unignored again.
+ */
+static int does_negate_pattern(git_attr_fnmatch *rule, git_attr_fnmatch *neg)
+{
+	char *p;
+
+	if ((rule->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0
+		&& (neg->flags & GIT_ATTR_FNMATCH_NEGATIVE) != 0) {
+		/*
+		 * no chance of matching if rule is shorter than
+		 * the negated one
+		 */
+		if (rule->length < neg->length)
+			return false;
+
+		/*
+		 * shift pattern so its tail aligns with the
+		 * negated pattern
+		 */
+		p = rule->pattern + rule->length - neg->length;
+		if (strcmp(p, neg->pattern) == 0)
+			return true;
+	}
+
+	return false;
+}
+
+/**
  * A negative ignore can only unignore a file which is given explicitly before, thus
  *
  *    foo
@@ -31,6 +66,8 @@ static int does_negate_rule(int *out, git_vector *rules, git_attr_fnmatch *match
 	char *path;
 	git_buf buf = GIT_BUF_INIT;
 
+	*out = 0;
+
 	/* path of the file relative to the workdir, so we match the rules in subdirs */
 	if (match->containing_dir) {
 		git_buf_puts(&buf, match->containing_dir);
@@ -41,9 +78,14 @@ static int does_negate_rule(int *out, git_vector *rules, git_attr_fnmatch *match
 	path = git_buf_detach(&buf);
 
 	git_vector_foreach(rules, i, rule) {
-		/* no chance of matching w/o a wilcard */
-		if (!(rule->flags & GIT_ATTR_FNMATCH_HASWILD))
-			continue;
+		if (!(rule->flags & GIT_ATTR_FNMATCH_HASWILD)) {
+			if (does_negate_pattern(rule, match)) {
+				*out = 1;
+				goto out;
+			}
+			else
+				continue;
+		}
 
 	/*
 	 * If we're dealing with a directory (which we know via the
@@ -62,7 +104,6 @@ static int does_negate_rule(int *out, git_vector *rules, git_attr_fnmatch *match
 		if (error < 0)
 			goto out;
 
-
 		if ((error = p_fnmatch(git_buf_cstr(&buf), path, FNM_PATHNAME)) < 0) {
 			giterr_set(GITERR_INVALID, "error matching pattern");
 			goto out;
@@ -76,7 +117,6 @@ static int does_negate_rule(int *out, git_vector *rules, git_attr_fnmatch *match
 		}
 	}
 
-	*out = 0;
 	error = 0;
 
 out:
diff --git a/tests/status/ignore.c b/tests/status/ignore.c
index a15b11d..3193d31 100644
--- a/tests/status/ignore.c
+++ b/tests/status/ignore.c
@@ -892,6 +892,59 @@ void test_status_ignore__negative_ignores_without_trailing_slash_inside_ignores(
 	cl_assert(found_parent_child2_file);
 }
 
+void test_status_ignore__negative_directory_ignores(void)
+{
+	static const char *test_files[] = {
+		"empty_standard_repo/parent/child1/bar.txt",
+		"empty_standard_repo/parent/child2/bar.txt",
+		"empty_standard_repo/parent/child3/foo.txt",
+		"empty_standard_repo/parent/child4/bar.txt",
+		"empty_standard_repo/parent/nested/child5/bar.txt",
+		"empty_standard_repo/parent/nested/child6/bar.txt",
+		"empty_standard_repo/parent/nested/child7/bar.txt",
+		"empty_standard_repo/padded_parent/child8/bar.txt",
+		NULL
+	};
+
+	make_test_data("empty_standard_repo", test_files);
+	cl_git_mkfile(
+		"empty_standard_repo/.gitignore",
+		"foo.txt\n"
+		"parent/child1\n"
+		"parent/child2\n"
+		"parent/child4\n"
+		"parent/nested/child5\n"
+		"nested/child6\n"
+		"nested/child7\n"
+		"padded_parent/child8\n"
+		/* test simple exact match */
+		"!parent/child1\n"
+		/* test negating file without negating dir */
+		"!parent/child2/bar.txt\n"
+		/* test negative pattern on dir with its content
+		 * being ignored */
+		"!parent/child3\n"
+		/* test with partial match at end */
+		"!child4\n"
+		/* test with partial match with '/' at end */
+		"!nested/child5\n"
+		/* test with complete match */
+		"!nested/child6\n"
+		/* test with trailing '/' */
+		"!child7/\n"
+		/* test with partial dir match */
+		"!_parent/child8\n");
+
+	refute_is_ignored("parent/child1/bar.txt");
+	assert_is_ignored("parent/child2/bar.txt");
+	assert_is_ignored("parent/child3/foo.txt");
+	refute_is_ignored("parent/child4/bar.txt");
+	assert_is_ignored("parent/nested/child5/bar.txt");
+	refute_is_ignored("parent/nested/child6/bar.txt");
+	refute_is_ignored("parent/nested/child7/bar.txt");
+	assert_is_ignored("padded_parent/child8/bar.txt");
+}
+
 void test_status_ignore__filename_with_cr(void)
 {
 	int ignored;