ignore: fix negative ignores without wildcards.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
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;