Merge pull request #2508 from libgit2/rb/fix-ignore-slash-star Fix bugs with negative ignores inside an ignored parent directory
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 154 155
diff --git a/src/attr_file.c b/src/attr_file.c
index 3e95a21..07ffacb 100644
--- a/src/attr_file.c
+++ b/src/attr_file.c
@@ -378,6 +378,18 @@ bool git_attr_fnmatch__match(
return (matchval != FNM_NOMATCH);
}
+ /* if path is a directory prefix of a negated pattern, then match */
+ if ((match->flags & GIT_ATTR_FNMATCH_NEGATIVE) && path->is_dir) {
+ size_t pathlen = strlen(path->path);
+ bool prefixed = (pathlen <= match->length) &&
+ ((match->flags & GIT_ATTR_FNMATCH_ICASE) ?
+ !strncasecmp(match->pattern, path->path, pathlen) :
+ !strncmp(match->pattern, path->path, pathlen));
+
+ if (prefixed && git_path_at_end_of_segment(&match->pattern[pathlen]))
+ return true;
+ }
+
return (p_fnmatch(match->pattern, filename, flags) != FNM_NOMATCH);
}
@@ -522,7 +534,8 @@ int git_attr_fnmatch__parse(
}
if (*pattern == '!' && (spec->flags & GIT_ATTR_FNMATCH_ALLOWNEG) != 0) {
- spec->flags = spec->flags | GIT_ATTR_FNMATCH_NEGATIVE;
+ spec->flags = spec->flags |
+ GIT_ATTR_FNMATCH_NEGATIVE | GIT_ATTR_FNMATCH_LEADINGDIR;
pattern++;
}
diff --git a/src/path.h b/src/path.h
index 2e86241..46d6efe 100644
--- a/src/path.h
+++ b/src/path.h
@@ -128,6 +128,14 @@ GIT_INLINE(int) git_path_is_relative(const char *p)
return (p[0] == '.' && (p[1] == '/' || (p[1] == '.' && p[2] == '/')));
}
+/**
+ * Check if string is at end of path segment (i.e. looking at '/' or '\0')
+ */
+GIT_INLINE(int) git_path_at_end_of_segment(const char *p)
+{
+ return !*p || *p == '/';
+}
+
extern int git__percent_decode(git_buf *decoded_out, const char *input);
/**
diff --git a/tests/status/ignore.c b/tests/status/ignore.c
index a4e766f..b2af790 100644
--- a/tests/status/ignore.c
+++ b/tests/status/ignore.c
@@ -788,3 +788,98 @@ void test_status_ignore__negative_ignores_inside_ignores(void)
refute_is_ignored("top/mid/btm/tracked");
refute_is_ignored("top/mid/btm/untracked");
}
+
+void test_status_ignore__negative_ignores_in_slash_star(void)
+{
+ git_status_options status_opts = GIT_STATUS_OPTIONS_INIT;
+ git_status_list *list;
+ int found_look_ma = 0, found_what_about = 0;
+ size_t i;
+ static const char *test_files[] = {
+ "empty_standard_repo/bin/look-ma.txt",
+ "empty_standard_repo/bin/what-about-me.txt",
+ NULL
+ };
+
+ make_test_data("empty_standard_repo", test_files);
+ cl_git_mkfile(
+ "empty_standard_repo/.gitignore",
+ "bin/*\n"
+ "!bin/w*\n");
+
+ assert_is_ignored("bin/look-ma.txt");
+ refute_is_ignored("bin/what-about-me.txt");
+
+ status_opts.flags = GIT_STATUS_OPT_DEFAULTS;
+ cl_git_pass(git_status_list_new(&list, g_repo, &status_opts));
+ for (i = 0; i < git_status_list_entrycount(list); i++) {
+ const git_status_entry *entry = git_status_byindex(list, i);
+
+ if (!strcmp("bin/look-ma.txt", entry->index_to_workdir->new_file.path))
+ found_look_ma = 1;
+
+ if (!strcmp("bin/what-about-me.txt", entry->index_to_workdir->new_file.path))
+ found_what_about = 1;
+ }
+ git_status_list_free(list);
+
+ cl_assert(found_look_ma);
+ cl_assert(found_what_about);
+}
+
+void test_status_ignore__negative_ignores_without_trailing_slash_inside_ignores(void)
+{
+ git_status_options status_opts = GIT_STATUS_OPTIONS_INIT;
+ git_status_list *list;
+ int found_parent_file = 0, found_parent_child1_file = 0, found_parent_child2_file = 0;
+ size_t i;
+ static const char *test_files[] = {
+ "empty_standard_repo/parent/file.txt",
+ "empty_standard_repo/parent/force.txt",
+ "empty_standard_repo/parent/child1/file.txt",
+ "empty_standard_repo/parent/child2/file.txt",
+ NULL
+ };
+
+ make_test_data("empty_standard_repo", test_files);
+ cl_git_mkfile(
+ "empty_standard_repo/.gitignore",
+ "parent/*\n"
+ "!parent/force.txt\n"
+ "!parent/child1\n"
+ "!parent/child2/\n");
+
+ add_one_to_index("parent/force.txt");
+
+ assert_is_ignored("parent/file.txt");
+ refute_is_ignored("parent/force.txt");
+ refute_is_ignored("parent/child1/file.txt");
+ refute_is_ignored("parent/child2/file.txt");
+
+ status_opts.flags = GIT_STATUS_OPT_DEFAULTS;
+ cl_git_pass(git_status_list_new(&list, g_repo, &status_opts));
+ for (i = 0; i < git_status_list_entrycount(list); i++) {
+ const git_status_entry *entry = git_status_byindex(list, i);
+
+ if (!entry->index_to_workdir)
+ continue;
+
+ if (!strcmp("parent/file.txt", entry->index_to_workdir->new_file.path))
+ found_parent_file = 1;
+
+ if (!strcmp("parent/force.txt", entry->index_to_workdir->new_file.path))
+ found_parent_file = 1;
+
+ if (!strcmp("parent/child1/file.txt", entry->index_to_workdir->new_file.path))
+ found_parent_child1_file = 1;
+
+ if (!strcmp("parent/child2/file.txt", entry->index_to_workdir->new_file.path))
+ found_parent_child2_file = 1;
+ }
+ git_status_list_free(list);
+
+ cl_assert(found_parent_file);
+ cl_assert(found_parent_child1_file);
+ cl_assert(found_parent_child2_file);
+}
+