Commit 635a922274046ee077235b9764d0360e33d735ab

Edward Thomson 2016-08-17T08:54:48

Merge pull request #3895 from pks-t/pks/negate-basename-in-subdirs ignore: allow unignoring basenames in subdirectories

diff --git a/src/ignore.c b/src/ignore.c
index ac2af4f..dcbd5c1 100644
--- a/src/ignore.c
+++ b/src/ignore.c
@@ -11,35 +11,64 @@
 #define GIT_IGNORE_DEFAULT_RULES ".\n..\n.git\n"
 
 /**
- * A negative ignore pattern can match a positive one without
- * wildcards if its pattern equals the tail of the positive
- * pattern. Thus
+ * A negative ignore pattern can negate a positive one without
+ * wildcards if it is a basename only and equals the basename of
+ * the positive pattern. Thus
  *
  * foo/bar
  * !bar
  *
- * would result in foo/bar being unignored again.
+ * would result in foo/bar being unignored again while
+ *
+ * moo/foo/bar
+ * !foo/bar
+ *
+ * would do nothing. The reverse also holds true: a positive
+ * basename pattern can be negated by unignoring the basename in
+ * subdirectories. Thus
+ *
+ * bar
+ * !foo/bar
+ *
+ * would result in foo/bar being unignored again. As with the
+ * first case,
+ *
+ * foo/bar
+ * !moo/foo/bar
+ *
+ * would do nothing, again.
  */
 static int does_negate_pattern(git_attr_fnmatch *rule, git_attr_fnmatch *neg)
 {
+	git_attr_fnmatch *longer, *shorter;
 	char *p;
 
 	if ((rule->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0
 		&& (neg->flags & GIT_ATTR_FNMATCH_NEGATIVE) != 0) {
-		/*
-		 * no chance of matching if rule is shorter than
-		 * the negated one
-		 */
-		if (rule->length < neg->length)
+
+		/* If lengths match we need to have an exact match */
+		if (rule->length == neg->length) {
+			return strcmp(rule->pattern, neg->pattern) == 0;
+		} else if (rule->length < neg->length) {
+			shorter = rule;
+			longer = neg;
+		} else {
+			shorter = neg;
+			longer = rule;
+		}
+
+		/* Otherwise, we need to check if the shorter
+		 * rule is a basename only (that is, it contains
+		 * no path separator) and, if so, if it
+		 * matches the tail of the longer rule */
+		p = longer->pattern + longer->length - shorter->length;
+
+		if (p[-1] != '/')
+			return false;
+		if (memchr(shorter->pattern, '/', shorter->length) != NULL)
 			return false;
 
-		/*
-		 * shift pattern so its tail aligns with the
-		 * negated pattern
-		 */
-		p = rule->pattern + rule->length - neg->length;
-		if (strcmp(p, neg->pattern) == 0)
-			return true;
+		return memcmp(p, shorter->pattern, shorter->length) == 0;
 	}
 
 	return false;
diff --git a/tests/status/ignore.c b/tests/status/ignore.c
index c318046..c4878b2 100644
--- a/tests/status/ignore.c
+++ b/tests/status/ignore.c
@@ -945,6 +945,44 @@ void test_status_ignore__negative_directory_ignores(void)
 	assert_is_ignored("padded_parent/child8/bar.txt");
 }
 
+void test_status_ignore__unignore_entry_in_ignored_dir(void)
+{
+	static const char *test_files[] = {
+		"empty_standard_repo/bar.txt",
+		"empty_standard_repo/parent/bar.txt",
+		"empty_standard_repo/parent/child/bar.txt",
+		"empty_standard_repo/nested/parent/child/bar.txt",
+		NULL
+	};
+
+	make_test_data("empty_standard_repo", test_files);
+	cl_git_mkfile(
+		"empty_standard_repo/.gitignore",
+		"bar.txt\n"
+		"!parent/child/bar.txt\n");
+
+	assert_is_ignored("bar.txt");
+	assert_is_ignored("parent/bar.txt");
+	refute_is_ignored("parent/child/bar.txt");
+	assert_is_ignored("nested/parent/child/bar.txt");
+}
+
+void test_status_ignore__do_not_unignore_basename_prefix(void)
+{
+	static const char *test_files[] = {
+		"empty_standard_repo/foo_bar.txt",
+		NULL
+	};
+
+	make_test_data("empty_standard_repo", test_files);
+	cl_git_mkfile(
+		"empty_standard_repo/.gitignore",
+		"foo_bar.txt\n"
+		"!bar.txt\n");
+
+	assert_is_ignored("foo_bar.txt");
+}
+
 void test_status_ignore__filename_with_cr(void)
 {
 	int ignored;