From f25bc0b2e6d764a496047e4f033767303b35b27e Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Fri, 8 Aug 2014 14:51:36 -0700 Subject: [PATCH 1/4] Fix rejection of parent dir of negated ignores While scanning through a directory hierarchy, this prevents a positive ignore match on a parent directory from blocking the scan of a directory when a negative match rule exists for files inside the directory. --- src/attr_file.c | 12 ++++++++++++ src/path.h | 8 ++++++++ 2 files changed, 20 insertions(+) diff --git a/src/attr_file.c b/src/attr_file.c index 3e95a2134..2f0953736 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); } 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); /** From aa5cdf63bfa4581110fb37cdb2ccffc021eb78fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Mart=C3=ADn=20Nieto?= Date: Wed, 4 Jun 2014 11:57:53 +0200 Subject: [PATCH 2/4] status: failing test with slash-star When writing 'bin/*' in the rules, this means we ignore very file inside bin/ individually, but do not ignore the directory itself. Thus the status listing should list both files under bin/, one untracked and one ignored. --- tests/status/ignore.c | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/status/ignore.c b/tests/status/ignore.c index a4e766fdf..88575cef0 100644 --- a/tests/status/ignore.c +++ b/tests/status/ignore.c @@ -788,3 +788,41 @@ 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); +} From a0cacc82d5bd3ba6b0240f5e3a7e926e977d3535 Mon Sep 17 00:00:00 2001 From: Russell Belfer Date: Fri, 8 Aug 2014 15:18:40 -0700 Subject: [PATCH 3/4] For negative matches, always use leading dir match --- src/attr_file.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/attr_file.c b/src/attr_file.c index 2f0953736..07ffacbaf 100644 --- a/src/attr_file.c +++ b/src/attr_file.c @@ -534,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++; } From bbe13802b7f85343d3db1aeb799662ee11461e6b Mon Sep 17 00:00:00 2001 From: Rob Rix Date: Thu, 12 Jun 2014 14:19:34 -0400 Subject: [PATCH 4/4] Demonstrate a trailing slash failure. `git help ignore` has this to say about trailing slashes: > If the pattern ends with a slash, it is removed for the purpose of > the following description, but it would only find a match with a > directory. In other words, foo/ will match a directory foo and > paths underneath it, but will not match a regular file or a > symbolic link foo (this is consistent with the way how pathspec > works in general in Git). Sure enough, having manually performed the same steps as this test, `git status` tells us the following: # On branch master # # Initial commit # # Changes to be committed: # (use "git rm --cached ..." to unstage) # # new file: force.txt # # Untracked files: # (use "git add ..." to include in what will be committed) # # ../.gitignore # child1/ # child2/ i.e. neither child1 nor child2 is ignored. --- tests/status/ignore.c | 57 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/status/ignore.c b/tests/status/ignore.c index 88575cef0..b2af79074 100644 --- a/tests/status/ignore.c +++ b/tests/status/ignore.c @@ -826,3 +826,60 @@ void test_status_ignore__negative_ignores_in_slash_star(void) 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); +} +