diff --git a/src/ignore.c b/src/ignore.c index ac2af4f58..dcbd5c1ca 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 c318046da..c4878b2dd 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;