Skip to content

Commit fcb2c1c

Browse files
committed
ignore: allow unignoring basenames in subdirectories
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.
1 parent 73dab76 commit fcb2c1c

File tree

2 files changed

+83
-16
lines changed

2 files changed

+83
-16
lines changed

src/ignore.c

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,64 @@
1111
#define GIT_IGNORE_DEFAULT_RULES ".\n..\n.git\n"
1212

1313
/**
14-
* A negative ignore pattern can match a positive one without
15-
* wildcards if its pattern equals the tail of the positive
16-
* pattern. Thus
14+
* A negative ignore pattern can negate a positive one without
15+
* wildcards if it is a basename only and equals the basename of
16+
* the positive pattern. Thus
1717
*
1818
* foo/bar
1919
* !bar
2020
*
21-
* would result in foo/bar being unignored again.
21+
* would result in foo/bar being unignored again while
22+
*
23+
* moo/foo/bar
24+
* !foo/bar
25+
*
26+
* would do nothing. The reverse also holds true: a positive
27+
* basename pattern can be negated by unignoring the basename in
28+
* subdirectories. Thus
29+
*
30+
* bar
31+
* !foo/bar
32+
*
33+
* would result in foo/bar being unignored again. As with the
34+
* first case,
35+
*
36+
* foo/bar
37+
* !moo/foo/bar
38+
*
39+
* would do nothing, again.
2240
*/
2341
static int does_negate_pattern(git_attr_fnmatch *rule, git_attr_fnmatch *neg)
2442
{
43+
git_attr_fnmatch *longer, *shorter;
2544
char *p;
2645

2746
if ((rule->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0
2847
&& (neg->flags & GIT_ATTR_FNMATCH_NEGATIVE) != 0) {
29-
/*
30-
* no chance of matching if rule is shorter than
31-
* the negated one
32-
*/
33-
if (rule->length < neg->length)
48+
49+
/* If lengths match we need to have an exact match */
50+
if (rule->length == neg->length) {
51+
return strcmp(rule->pattern, neg->pattern) == 0;
52+
} else if (rule->length < neg->length) {
53+
shorter = rule;
54+
longer = neg;
55+
} else {
56+
shorter = neg;
57+
longer = rule;
58+
}
59+
60+
/* Otherwise, we need to check if the shorter
61+
* rule is a basename only (that is, it contains
62+
* no path separator) and, if so, if it
63+
* matches the tail of the longer rule */
64+
p = longer->pattern + longer->length - shorter->length;
65+
66+
if (p[-1] != '/')
67+
return false;
68+
if (memchr(shorter->pattern, '/', shorter->length) != NULL)
3469
return false;
3570

36-
/*
37-
* shift pattern so its tail aligns with the
38-
* negated pattern
39-
*/
40-
p = rule->pattern + rule->length - neg->length;
41-
if (strcmp(p, neg->pattern) == 0)
42-
return true;
71+
return memcmp(p, shorter->pattern, shorter->length) == 0;
4372
}
4473

4574
return false;

tests/status/ignore.c

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -945,6 +945,44 @@ void test_status_ignore__negative_directory_ignores(void)
945945
assert_is_ignored("padded_parent/child8/bar.txt");
946946
}
947947

948+
void test_status_ignore__unignore_entry_in_ignored_dir(void)
949+
{
950+
static const char *test_files[] = {
951+
"empty_standard_repo/bar.txt",
952+
"empty_standard_repo/parent/bar.txt",
953+
"empty_standard_repo/parent/child/bar.txt",
954+
"empty_standard_repo/nested/parent/child/bar.txt",
955+
NULL
956+
};
957+
958+
make_test_data("empty_standard_repo", test_files);
959+
cl_git_mkfile(
960+
"empty_standard_repo/.gitignore",
961+
"bar.txt\n"
962+
"!parent/child/bar.txt\n");
963+
964+
assert_is_ignored("bar.txt");
965+
assert_is_ignored("parent/bar.txt");
966+
refute_is_ignored("parent/child/bar.txt");
967+
assert_is_ignored("nested/parent/child/bar.txt");
968+
}
969+
970+
void test_status_ignore__do_not_unignore_basename_prefix(void)
971+
{
972+
static const char *test_files[] = {
973+
"empty_standard_repo/foo_bar.txt",
974+
NULL
975+
};
976+
977+
make_test_data("empty_standard_repo", test_files);
978+
cl_git_mkfile(
979+
"empty_standard_repo/.gitignore",
980+
"foo_bar.txt\n"
981+
"!bar.txt\n");
982+
983+
assert_is_ignored("foo_bar.txt");
984+
}
985+
948986
void test_status_ignore__filename_with_cr(void)
949987
{
950988
int ignored;

0 commit comments

Comments
 (0)