Commit 4296a36b7dff15711e972c56ef5de53860a5cd21

Patrick Steinhardt 2017-07-10T09:36:19

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.

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");
+}