diff --git a/src/attr_file.c b/src/attr_file.c index 3e95a2134..07ffacbaf 100644 --- a/src/attr_file.c +++ b/src/attr_file.c @@ -378,6 +378,18 @@ bool git_attr_fnmatch__match( return (matchval != FNM_NOMATCH); } + /* if path is a directory prefix of a negated pattern, then match */ + if ((match->flags & GIT_ATTR_FNMATCH_NEGATIVE) && path->is_dir) { + size_t pathlen = strlen(path->path); + bool prefixed = (pathlen <= match->length) && + ((match->flags & GIT_ATTR_FNMATCH_ICASE) ? + !strncasecmp(match->pattern, path->path, pathlen) : + !strncmp(match->pattern, path->path, pathlen)); + + if (prefixed && git_path_at_end_of_segment(&match->pattern[pathlen])) + return true; + } + return (p_fnmatch(match->pattern, filename, flags) != FNM_NOMATCH); } @@ -522,7 +534,8 @@ int git_attr_fnmatch__parse( } if (*pattern == '!' && (spec->flags & GIT_ATTR_FNMATCH_ALLOWNEG) != 0) { - spec->flags = spec->flags | GIT_ATTR_FNMATCH_NEGATIVE; + spec->flags = spec->flags | + GIT_ATTR_FNMATCH_NEGATIVE | GIT_ATTR_FNMATCH_LEADINGDIR; pattern++; } diff --git a/src/path.h b/src/path.h index 2e86241e1..46d6efe93 100644 --- a/src/path.h +++ b/src/path.h @@ -128,6 +128,14 @@ GIT_INLINE(int) git_path_is_relative(const char *p) return (p[0] == '.' && (p[1] == '/' || (p[1] == '.' && p[2] == '/'))); } +/** + * Check if string is at end of path segment (i.e. looking at '/' or '\0') + */ +GIT_INLINE(int) git_path_at_end_of_segment(const char *p) +{ + return !*p || *p == '/'; +} + extern int git__percent_decode(git_buf *decoded_out, const char *input); /** diff --git a/tests/status/ignore.c b/tests/status/ignore.c index a4e766fdf..b2af79074 100644 --- a/tests/status/ignore.c +++ b/tests/status/ignore.c @@ -788,3 +788,98 @@ void test_status_ignore__negative_ignores_inside_ignores(void) refute_is_ignored("top/mid/btm/tracked"); refute_is_ignored("top/mid/btm/untracked"); } + +void test_status_ignore__negative_ignores_in_slash_star(void) +{ + git_status_options status_opts = GIT_STATUS_OPTIONS_INIT; + git_status_list *list; + int found_look_ma = 0, found_what_about = 0; + size_t i; + static const char *test_files[] = { + "empty_standard_repo/bin/look-ma.txt", + "empty_standard_repo/bin/what-about-me.txt", + NULL + }; + + make_test_data("empty_standard_repo", test_files); + cl_git_mkfile( + "empty_standard_repo/.gitignore", + "bin/*\n" + "!bin/w*\n"); + + assert_is_ignored("bin/look-ma.txt"); + refute_is_ignored("bin/what-about-me.txt"); + + status_opts.flags = GIT_STATUS_OPT_DEFAULTS; + cl_git_pass(git_status_list_new(&list, g_repo, &status_opts)); + for (i = 0; i < git_status_list_entrycount(list); i++) { + const git_status_entry *entry = git_status_byindex(list, i); + + if (!strcmp("bin/look-ma.txt", entry->index_to_workdir->new_file.path)) + found_look_ma = 1; + + if (!strcmp("bin/what-about-me.txt", entry->index_to_workdir->new_file.path)) + found_what_about = 1; + } + git_status_list_free(list); + + cl_assert(found_look_ma); + cl_assert(found_what_about); +} + +void test_status_ignore__negative_ignores_without_trailing_slash_inside_ignores(void) +{ + git_status_options status_opts = GIT_STATUS_OPTIONS_INIT; + git_status_list *list; + int found_parent_file = 0, found_parent_child1_file = 0, found_parent_child2_file = 0; + size_t i; + static const char *test_files[] = { + "empty_standard_repo/parent/file.txt", + "empty_standard_repo/parent/force.txt", + "empty_standard_repo/parent/child1/file.txt", + "empty_standard_repo/parent/child2/file.txt", + NULL + }; + + make_test_data("empty_standard_repo", test_files); + cl_git_mkfile( + "empty_standard_repo/.gitignore", + "parent/*\n" + "!parent/force.txt\n" + "!parent/child1\n" + "!parent/child2/\n"); + + add_one_to_index("parent/force.txt"); + + assert_is_ignored("parent/file.txt"); + refute_is_ignored("parent/force.txt"); + refute_is_ignored("parent/child1/file.txt"); + refute_is_ignored("parent/child2/file.txt"); + + status_opts.flags = GIT_STATUS_OPT_DEFAULTS; + cl_git_pass(git_status_list_new(&list, g_repo, &status_opts)); + for (i = 0; i < git_status_list_entrycount(list); i++) { + const git_status_entry *entry = git_status_byindex(list, i); + + if (!entry->index_to_workdir) + continue; + + if (!strcmp("parent/file.txt", entry->index_to_workdir->new_file.path)) + found_parent_file = 1; + + if (!strcmp("parent/force.txt", entry->index_to_workdir->new_file.path)) + found_parent_file = 1; + + if (!strcmp("parent/child1/file.txt", entry->index_to_workdir->new_file.path)) + found_parent_child1_file = 1; + + if (!strcmp("parent/child2/file.txt", entry->index_to_workdir->new_file.path)) + found_parent_child2_file = 1; + } + git_status_list_free(list); + + cl_assert(found_parent_file); + cl_assert(found_parent_child1_file); + cl_assert(found_parent_child2_file); +} +