Skip to content

Commit 3c21645

Browse files
authored
Merge pull request libgit2#4296 from pks-t/pks/pattern-based-gitignore
Fix negative ignore rules with patterns
2 parents 4b000fc + 2d9ff8f commit 3c21645

File tree

3 files changed

+120
-36
lines changed

3 files changed

+120
-36
lines changed

src/ignore.c

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -48,38 +48,42 @@
4848
*/
4949
static int does_negate_pattern(git_attr_fnmatch *rule, git_attr_fnmatch *neg)
5050
{
51+
int (*cmp)(const char *, const char *, size_t);
5152
git_attr_fnmatch *longer, *shorter;
5253
char *p;
5354

54-
if ((rule->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0
55-
&& (neg->flags & GIT_ATTR_FNMATCH_NEGATIVE) != 0) {
56-
57-
/* If lengths match we need to have an exact match */
58-
if (rule->length == neg->length) {
59-
return strcmp(rule->pattern, neg->pattern) == 0;
60-
} else if (rule->length < neg->length) {
61-
shorter = rule;
62-
longer = neg;
63-
} else {
64-
shorter = neg;
65-
longer = rule;
66-
}
55+
if ((rule->flags & GIT_ATTR_FNMATCH_NEGATIVE) != 0
56+
|| (neg->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0)
57+
return false;
58+
59+
if (neg->flags & GIT_ATTR_FNMATCH_ICASE)
60+
cmp = git__strncasecmp;
61+
else
62+
cmp = strncmp;
63+
64+
/* If lengths match we need to have an exact match */
65+
if (rule->length == neg->length) {
66+
return cmp(rule->pattern, neg->pattern, rule->length) == 0;
67+
} else if (rule->length < neg->length) {
68+
shorter = rule;
69+
longer = neg;
70+
} else {
71+
shorter = neg;
72+
longer = rule;
73+
}
6774

68-
/* Otherwise, we need to check if the shorter
69-
* rule is a basename only (that is, it contains
70-
* no path separator) and, if so, if it
71-
* matches the tail of the longer rule */
72-
p = longer->pattern + longer->length - shorter->length;
75+
/* Otherwise, we need to check if the shorter
76+
* rule is a basename only (that is, it contains
77+
* no path separator) and, if so, if it
78+
* matches the tail of the longer rule */
79+
p = longer->pattern + longer->length - shorter->length;
7380

74-
if (p[-1] != '/')
75-
return false;
76-
if (memchr(shorter->pattern, '/', shorter->length) != NULL)
77-
return false;
81+
if (p[-1] != '/')
82+
return false;
83+
if (memchr(shorter->pattern, '/', shorter->length) != NULL)
84+
return false;
7885

79-
return memcmp(p, shorter->pattern, shorter->length) == 0;
80-
}
81-
82-
return false;
86+
return cmp(p, shorter->pattern, shorter->length) == 0;
8387
}
8488

8589
/**
@@ -97,14 +101,18 @@ static int does_negate_pattern(git_attr_fnmatch *rule, git_attr_fnmatch *neg)
97101
*/
98102
static int does_negate_rule(int *out, git_vector *rules, git_attr_fnmatch *match)
99103
{
100-
int error = 0;
104+
int error = 0, fnflags;
101105
size_t i;
102106
git_attr_fnmatch *rule;
103107
char *path;
104108
git_buf buf = GIT_BUF_INIT;
105109

106110
*out = 0;
107111

112+
fnflags = FNM_PATHNAME;
113+
if (match->flags & GIT_ATTR_FNMATCH_ICASE)
114+
fnflags |= FNM_IGNORECASE;
115+
108116
/* path of the file relative to the workdir, so we match the rules in subdirs */
109117
if (match->containing_dir) {
110118
git_buf_puts(&buf, match->containing_dir);
@@ -125,12 +133,12 @@ static int does_negate_rule(int *out, git_vector *rules, git_attr_fnmatch *match
125133
continue;
126134
}
127135

128-
/*
129-
* When dealing with a directory, we add '/<star>' so
130-
* p_fnmatch() honours FNM_PATHNAME. Checking for LEADINGDIR
131-
* alone isn't enough as that's also set for nagations, so we
132-
* need to check that NEGATIVE is off.
133-
*/
136+
/*
137+
* When dealing with a directory, we add '/<star>' so
138+
* p_fnmatch() honours FNM_PATHNAME. Checking for LEADINGDIR
139+
* alone isn't enough as that's also set for nagations, so we
140+
* need to check that NEGATIVE is off.
141+
*/
134142
git_buf_clear(&buf);
135143
if (rule->containing_dir) {
136144
git_buf_puts(&buf, rule->containing_dir);
@@ -144,7 +152,7 @@ static int does_negate_rule(int *out, git_vector *rules, git_attr_fnmatch *match
144152
if (error < 0)
145153
goto out;
146154

147-
if ((error = p_fnmatch(git_buf_cstr(&buf), path, FNM_PATHNAME)) < 0) {
155+
if ((error = p_fnmatch(git_buf_cstr(&buf), path, fnflags)) < 0) {
148156
giterr_set(GITERR_INVALID, "error matching pattern");
149157
goto out;
150158
}
@@ -207,8 +215,14 @@ static int parse_ignore_file(
207215

208216
scan = git__next_line(scan);
209217

210-
/* if a negative match doesn't actually do anything, throw it away */
211-
if (match->flags & GIT_ATTR_FNMATCH_NEGATIVE)
218+
/*
219+
* If a negative match doesn't actually do anything,
220+
* throw it away. As we cannot always verify whether a
221+
* rule containing wildcards negates another rule, we
222+
* do not optimize away these rules, though.
223+
* */
224+
if (match->flags & GIT_ATTR_FNMATCH_NEGATIVE
225+
&& !(match->flags & GIT_ATTR_FNMATCH_HASWILD))
212226
error = does_negate_rule(&valid_rule, &attrs->rules, match);
213227

214228
if (!error && valid_rule)

tests/attr/ignore.c

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,3 +303,46 @@ void test_attr_ignore__test(void)
303303
assert_is_ignored(true, "dist/foo.o");
304304
assert_is_ignored(true, "bin/foo");
305305
}
306+
307+
void test_attr_ignore__unignore_dir_succeeds(void)
308+
{
309+
cl_git_rewritefile("attr/.gitignore",
310+
"*.c\n"
311+
"!src/*.c\n");
312+
assert_is_ignored(false, "src/foo.c");
313+
assert_is_ignored(true, "src/foo/foo.c");
314+
}
315+
316+
void test_attr_ignore__case_insensitive_unignores_previous_rule(void)
317+
{
318+
git_config *cfg;
319+
320+
cl_git_rewritefile("attr/.gitignore",
321+
"/case\n"
322+
"!/Case/\n");
323+
324+
cl_git_pass(git_repository_config(&cfg, g_repo));
325+
cl_git_pass(git_config_set_bool(cfg, "core.ignorecase", true));
326+
327+
cl_must_pass(p_mkdir("attr/case", 0755));
328+
cl_git_mkfile("attr/case/file", "content");
329+
330+
assert_is_ignored(false, "case/file");
331+
}
332+
333+
void test_attr_ignore__case_sensitive_unignore_does_nothing(void)
334+
{
335+
git_config *cfg;
336+
337+
cl_git_rewritefile("attr/.gitignore",
338+
"/case\n"
339+
"!/Case/\n");
340+
341+
cl_git_pass(git_repository_config(&cfg, g_repo));
342+
cl_git_pass(git_config_set_bool(cfg, "core.ignorecase", false));
343+
344+
cl_must_pass(p_mkdir("attr/case", 0755));
345+
cl_git_mkfile("attr/case/file", "content");
346+
347+
assert_is_ignored(true, "case/file");
348+
}

tests/status/ignore.c

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,3 +1155,30 @@ void test_status_ignore__subdir_ignore_everything_except_certain_files(void)
11551155
refute_is_ignored("project/src/foo.c");
11561156
refute_is_ignored("project/src/foo/foo.c");
11571157
}
1158+
1159+
void test_status_ignore__deeper(void)
1160+
{
1161+
int ignored;
1162+
1163+
g_repo = cl_git_sandbox_init("empty_standard_repo");
1164+
1165+
cl_git_mkfile("empty_standard_repo/.gitignore",
1166+
"*.data\n"
1167+
"!dont_ignore/*.data\n");
1168+
1169+
cl_git_pass(p_mkdir("empty_standard_repo/dont_ignore", 0777));
1170+
cl_git_mkfile("empty_standard_repo/foo.data", "");
1171+
cl_git_mkfile("empty_standard_repo/bar.data", "");
1172+
cl_git_mkfile("empty_standard_repo/dont_ignore/foo.data", "");
1173+
cl_git_mkfile("empty_standard_repo/dont_ignore/bar.data", "");
1174+
1175+
cl_git_pass(git_ignore_path_is_ignored(&ignored, g_repo, "foo.data"));
1176+
cl_assert_equal_i(1, ignored);
1177+
cl_git_pass(git_ignore_path_is_ignored(&ignored, g_repo, "bar.data"));
1178+
cl_assert_equal_i(1, ignored);
1179+
1180+
cl_git_pass(git_ignore_path_is_ignored(&ignored, g_repo, "dont_ignore/foo.data"));
1181+
cl_assert_equal_i(0, ignored);
1182+
cl_git_pass(git_ignore_path_is_ignored(&ignored, g_repo, "dont_ignore/bar.data"));
1183+
cl_assert_equal_i(0, ignored);
1184+
}

0 commit comments

Comments
 (0)