ignore: honor case insensitivity for negative ignores When computing negative ignores, we throw away any rule which does not undo a previous rule to optimize. But on case insensitive file systems, we need to keep in mind that a negative ignore can also undo a previous rule with different case, which we did not yet honor while determining whether a rule undoes a previous one. So in the following example, we fail to unignore the "/Case" directory: /case !/Case Make both paths checking whether a plain- or wildcard-based rule undo a previous rule aware of case-insensitivity. This fixes the described issue.
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
diff --git a/src/ignore.c b/src/ignore.c
index e603ac8..0dc23b5 100644
--- a/src/ignore.c
+++ b/src/ignore.c
@@ -40,6 +40,7 @@
*/
static int does_negate_pattern(git_attr_fnmatch *rule, git_attr_fnmatch *neg)
{
+ int (*cmp)(const char *, const char *, size_t);
git_attr_fnmatch *longer, *shorter;
char *p;
@@ -47,9 +48,14 @@ static int does_negate_pattern(git_attr_fnmatch *rule, git_attr_fnmatch *neg)
|| (neg->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0)
return false;
+ if (neg->flags & GIT_ATTR_FNMATCH_ICASE)
+ cmp = git__strncasecmp;
+ else
+ cmp = strncmp;
+
/* If lengths match we need to have an exact match */
if (rule->length == neg->length) {
- return strcmp(rule->pattern, neg->pattern) == 0;
+ return cmp(rule->pattern, neg->pattern, rule->length) == 0;
} else if (rule->length < neg->length) {
shorter = rule;
longer = neg;
@@ -69,7 +75,7 @@ static int does_negate_pattern(git_attr_fnmatch *rule, git_attr_fnmatch *neg)
if (memchr(shorter->pattern, '/', shorter->length) != NULL)
return false;
- return memcmp(p, shorter->pattern, shorter->length) == 0;
+ return cmp(p, shorter->pattern, shorter->length) == 0;
}
/**
@@ -87,7 +93,7 @@ static int does_negate_pattern(git_attr_fnmatch *rule, git_attr_fnmatch *neg)
*/
static int does_negate_rule(int *out, git_vector *rules, git_attr_fnmatch *match)
{
- int error = 0;
+ int error = 0, fnflags;
size_t i;
git_attr_fnmatch *rule;
char *path;
@@ -95,6 +101,10 @@ static int does_negate_rule(int *out, git_vector *rules, git_attr_fnmatch *match
*out = 0;
+ fnflags = FNM_PATHNAME;
+ if (match->flags & GIT_ATTR_FNMATCH_ICASE)
+ fnflags |= FNM_IGNORECASE;
+
/* path of the file relative to the workdir, so we match the rules in subdirs */
if (match->containing_dir) {
git_buf_puts(&buf, match->containing_dir);
@@ -134,7 +144,7 @@ static int does_negate_rule(int *out, git_vector *rules, git_attr_fnmatch *match
if (error < 0)
goto out;
- if ((error = p_fnmatch(git_buf_cstr(&buf), path, FNM_PATHNAME)) < 0) {
+ if ((error = p_fnmatch(git_buf_cstr(&buf), path, fnflags)) < 0) {
giterr_set(GITERR_INVALID, "error matching pattern");
goto out;
}
diff --git a/tests/attr/ignore.c b/tests/attr/ignore.c
index 5adfaf5..856e61f 100644
--- a/tests/attr/ignore.c
+++ b/tests/attr/ignore.c
@@ -312,3 +312,37 @@ void test_attr_ignore__unignore_dir_succeeds(void)
assert_is_ignored(false, "src/foo.c");
assert_is_ignored(true, "src/foo/foo.c");
}
+
+void test_attr_ignore__case_insensitive_unignores_previous_rule(void)
+{
+ git_config *cfg;
+
+ cl_git_rewritefile("attr/.gitignore",
+ "/case\n"
+ "!/Case/\n");
+
+ cl_git_pass(git_repository_config(&cfg, g_repo));
+ cl_git_pass(git_config_set_bool(cfg, "core.ignorecase", true));
+
+ cl_must_pass(p_mkdir("attr/case", 0755));
+ cl_git_mkfile("attr/case/file", "content");
+
+ assert_is_ignored(false, "case/file");
+}
+
+void test_attr_ignore__case_sensitive_unignore_does_nothing(void)
+{
+ git_config *cfg;
+
+ cl_git_rewritefile("attr/.gitignore",
+ "/case\n"
+ "!/Case/\n");
+
+ cl_git_pass(git_repository_config(&cfg, g_repo));
+ cl_git_pass(git_config_set_bool(cfg, "core.ignorecase", false));
+
+ cl_must_pass(p_mkdir("attr/case", 0755));
+ cl_git_mkfile("attr/case/file", "content");
+
+ assert_is_ignored(true, "case/file");
+}