Skip to content

Commit 20b4c17

Browse files
committed
ignore: fix negative leading directory rules unignoring subdirectory files
When computing whether a file is ignored, we simply search for the first matching rule and return whether it is a positive ignore rule (the file is really ignored) or whether it is a negative ignore rule (the file is being unignored). Each rule has a set of flags which are being passed to `fnmatch`, depending on what kind of rule it is. E.g. in case it is a negative ignore we add a flag `GIT_ATTR_FNMATCH_NEGATIVE`, in case it contains a glob we set the `GIT_ATTR_FNMATCH_HASGLOB` flag. One of these flags is the `GIT_ATTR_FNMATCH_LEADINGDIR` flag, which is always set in case the pattern has a trailing "/*" or in case the pattern is negative. The flag causes the `fnmatch` function to return a match in case a string is a leading directory of another, e.g. "dir/" matches "dir/foo/bar.c". In case of negative patterns, this is wrong in certain cases. Take the following simple example of a gitignore: dir/ !dir/ The `LEADINGDIR` flag causes "!dir/" to match "dir/foo/bar.c", and we correctly unignore the directory. But take this example: *.test !dir/* We expect everything in "dir/" to be unignored, but e.g. a file in a subdirectory of dir should be ignored, as the "*" does not cross directory hierarchies. With `LEADINGDIR`, though, we would just see that "dir/" matches and return that the file is unignored, even if it is contained in a subdirectory. Instead, we want to ignore leading directories here and check "*.test". Afterwards, we have to iterate up to the parent directory and do the same checks. To fix the issue, disallow matching against leading directories in gitignore files. This can be trivially done by just adding the `GIT_ATTR_FNMATCH_NOLEADINGDIR` to the spec passed to `git_attr_fnmatch__parse`. Due to a bug in that function, though, this flag is being ignored for negative patterns, which is fixed in this commit, as well. As a last fix, we need to ignore rules that are supposed to match a directory when our path itself is a file. All together, these changes fix the described error case.
1 parent 9beb73e commit 20b4c17

File tree

3 files changed

+46
-3
lines changed

3 files changed

+46
-3
lines changed

src/attr_file.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -594,8 +594,9 @@ int git_attr_fnmatch__parse(
594594
}
595595

596596
if (*pattern == '!' && (spec->flags & GIT_ATTR_FNMATCH_ALLOWNEG) != 0) {
597-
spec->flags = spec->flags |
598-
GIT_ATTR_FNMATCH_NEGATIVE | GIT_ATTR_FNMATCH_LEADINGDIR;
597+
spec->flags = spec->flags | GIT_ATTR_FNMATCH_NEGATIVE;
598+
if ((spec->flags & GIT_ATTR_FNMATCH_NOLEADINGDIR) == 0)
599+
spec->flags |= GIT_ATTR_FNMATCH_LEADINGDIR;
599600
pattern++;
600601
}
601602

src/ignore.c

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,10 @@ static int parse_ignore_file(
203203
break;
204204
}
205205

206-
match->flags = GIT_ATTR_FNMATCH_ALLOWSPACE | GIT_ATTR_FNMATCH_ALLOWNEG;
206+
match->flags =
207+
GIT_ATTR_FNMATCH_ALLOWSPACE |
208+
GIT_ATTR_FNMATCH_ALLOWNEG |
209+
GIT_ATTR_FNMATCH_NOLEADINGDIR;
207210

208211
if (!(error = git_attr_fnmatch__parse(
209212
match, &attrs->pool, context, &scan)))
@@ -445,6 +448,9 @@ static bool ignore_lookup_in_rules(
445448
git_attr_fnmatch *match;
446449

447450
git_vector_rforeach(&file->rules, j, match) {
451+
if (match->flags & GIT_ATTR_FNMATCH_DIRECTORY &&
452+
path->is_dir == GIT_DIR_FLAG_FALSE)
453+
continue;
448454
if (git_attr_fnmatch__match(match, path)) {
449455
*ignored = ((match->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0) ?
450456
GIT_IGNORE_TRUE : GIT_IGNORE_FALSE;

tests/status/ignore.c

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,3 +1177,39 @@ void test_status_ignore__deeper(void)
11771177
refute_is_ignored("dont_ignore/foo.data");
11781178
refute_is_ignored("dont_ignore/bar.data");
11791179
}
1180+
1181+
void test_status_ignore__unignored_dir_with_ignored_contents(void)
1182+
{
1183+
static const char *test_files[] = {
1184+
"empty_standard_repo/dir/a.test",
1185+
"empty_standard_repo/dir/subdir/a.test",
1186+
NULL
1187+
};
1188+
1189+
make_test_data("empty_standard_repo", test_files);
1190+
cl_git_mkfile(
1191+
"empty_standard_repo/.gitignore",
1192+
"*.test\n"
1193+
"!dir/*\n");
1194+
1195+
refute_is_ignored("dir/a.test");
1196+
assert_is_ignored("dir/subdir/a.test");
1197+
}
1198+
1199+
void test_status_ignore__unignored_subdirs(void)
1200+
{
1201+
static const char *test_files[] = {
1202+
"empty_standard_repo/dir/a.test",
1203+
"empty_standard_repo/dir/subdir/a.test",
1204+
NULL
1205+
};
1206+
1207+
make_test_data("empty_standard_repo", test_files);
1208+
cl_git_mkfile(
1209+
"empty_standard_repo/.gitignore",
1210+
"dir/*\n"
1211+
"!dir/*/\n");
1212+
1213+
assert_is_ignored("dir/a.test");
1214+
refute_is_ignored("dir/subdir/a.test");
1215+
}

0 commit comments

Comments
 (0)