]> git.proxmox.com Git - libgit2.git/commitdiff
ignore: allow unignoring basenames in subdirectories
authorPatrick Steinhardt <ps@pks.im>
Fri, 12 Aug 2016 07:06:15 +0000 (09:06 +0200)
committerPatrick Steinhardt <ps@pks.im>
Fri, 12 Aug 2016 12:47:54 +0000 (14:47 +0200)
The .gitignore file allows for patterns which unignore previous
ignore patterns. When unignoring a previous pattern, there are
basically three cases how this is matched when no globbing is
used:

1. when a previous file has been ignored, it can be unignored by
   using its exact name, e.g.

   foo/bar
   !foo/bar

2. when a file in a subdirectory has been ignored, it can be
   unignored by using its basename, e.g.

   foo/bar
   !bar

3. when all files with a basename are ignored, a specific file
   can be unignored again by specifying its path in a
   subdirectory, e.g.

   bar
   !foo/bar

The first problem in libgit2 is that we did not correctly treat
the second case. While we verified that the negative pattern
matches the tail of the positive one, we did not verify if it
only matches the basename of the positive pattern. So e.g. we
would have also negated a pattern like

    foo/fruz_bar
    !bar

Furthermore, we did not check for the third case, where a
basename is being unignored in a certain subdirectory again.

Both issues are fixed with this commit.

src/ignore.c
tests/status/ignore.c

index ac2af4f586a9452f76d18118e90a4f7a3e93c248..dcbd5c1cad00c700f07d2dac2d7e852a565785de 100644 (file)
 #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
+ * A negative ignore pattern can negate a positive one without
+ * wildcards if it is a basename only and equals the basename of
+ * the positive pattern. Thus
  *
  * foo/bar
  * !bar
  *
- * would result in foo/bar being unignored again.
+ * would result in foo/bar being unignored again while
+ *
+ * moo/foo/bar
+ * !foo/bar
+ *
+ * would do nothing. The reverse also holds true: a positive
+ * basename pattern can be negated by unignoring the basename in
+ * subdirectories. Thus
+ *
+ * bar
+ * !foo/bar
+ *
+ * would result in foo/bar being unignored again. As with the
+ * first case,
+ *
+ * foo/bar
+ * !moo/foo/bar
+ *
+ * would do nothing, again.
  */
 static int does_negate_pattern(git_attr_fnmatch *rule, git_attr_fnmatch *neg)
 {
+       git_attr_fnmatch *longer, *shorter;
        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)
+
+               /* If lengths match we need to have an exact match */
+               if (rule->length == neg->length) {
+                       return strcmp(rule->pattern, neg->pattern) == 0;
+               } else if (rule->length < neg->length) {
+                       shorter = rule;
+                       longer = neg;
+               } else {
+                       shorter = neg;
+                       longer = rule;
+               }
+
+               /* Otherwise, we need to check if the shorter
+                * rule is a basename only (that is, it contains
+                * no path separator) and, if so, if it
+                * matches the tail of the longer rule */
+               p = longer->pattern + longer->length - shorter->length;
+
+               if (p[-1] != '/')
+                       return false;
+               if (memchr(shorter->pattern, '/', shorter->length) != NULL)
                        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 memcmp(p, shorter->pattern, shorter->length) == 0;
        }
 
        return false;
index c318046dad8c070613e5d0ba3885c80bd1efe57d..c4878b2dd9e04b9ee3c25183e54a1ebc0dcd32bb 100644 (file)
@@ -945,6 +945,44 @@ void test_status_ignore__negative_directory_ignores(void)
        assert_is_ignored("padded_parent/child8/bar.txt");
 }
 
+void test_status_ignore__unignore_entry_in_ignored_dir(void)
+{
+       static const char *test_files[] = {
+               "empty_standard_repo/bar.txt",
+               "empty_standard_repo/parent/bar.txt",
+               "empty_standard_repo/parent/child/bar.txt",
+               "empty_standard_repo/nested/parent/child/bar.txt",
+               NULL
+       };
+
+       make_test_data("empty_standard_repo", test_files);
+       cl_git_mkfile(
+               "empty_standard_repo/.gitignore",
+               "bar.txt\n"
+               "!parent/child/bar.txt\n");
+
+       assert_is_ignored("bar.txt");
+       assert_is_ignored("parent/bar.txt");
+       refute_is_ignored("parent/child/bar.txt");
+       assert_is_ignored("nested/parent/child/bar.txt");
+}
+
+void test_status_ignore__do_not_unignore_basename_prefix(void)
+{
+       static const char *test_files[] = {
+               "empty_standard_repo/foo_bar.txt",
+               NULL
+       };
+
+       make_test_data("empty_standard_repo", test_files);
+       cl_git_mkfile(
+               "empty_standard_repo/.gitignore",
+               "foo_bar.txt\n"
+               "!bar.txt\n");
+
+       assert_is_ignored("foo_bar.txt");
+}
+
 void test_status_ignore__filename_with_cr(void)
 {
        int ignored;