dir.c: don't exclude whole dir prematurely if neg pattern may match

If there is a pattern "!foo/bar", this patch makes it not exclude "foo" right away. This gives us a chance to examine "foo" and re-include "foo/bar". In order for it to detect that the directory under examination should not be excluded right away, in other words it is a parent directory of a negative pattern, the "directory path" of the negative pattern must be literal. Patterns like "!f?o/bar" can't stop "foo" from being excluded. Basename matching (i.e. "no slashes in the pattern") or must-be-dir matching (i.e. "trailing slash in the pattern") does not work well with this. For example, if we descend in "foo" and are examining "foo/abc", current code for "foo/" pattern will check if path "foo/abc", not "foo", is a directory. The same problem with basename matching. These may need big code reorg to make it work. Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Nguyễn Thái Ngọc Duy committed Sep 21, 2015 at 16:56 UTC 57534ee77d22e725d971ee89c77dc6aad61c573f
3 files changed +117 -5
Documentation/gitignore.txt
+19 -4
@@ -82,12 +82,12 @@ PATTERN FORMAT
82
83 - An optional prefix "`!`" which negates the pattern; any
84 matching file excluded by a previous pattern will become
85 - included again. It is not possible to re-include a file if a parent
86 - directory of that file is excluded. Git doesn't list excluded
87 - directories for performance reasons, so any patterns on contained
88 - files have no effect, no matter where they are defined.
85 + included again.
86 Put a backslash ("`\`") in front of the first "`!`" for patterns
87 that begin with a literal "`!`", for example, "`\!important!.txt`".
88 + It is possible to re-include a file if a parent directory of that
89 + file is excluded if certain conditions are met. See section NOTES
90 + for detail.
91
92 - If the pattern ends with a slash, it is removed for the
93 purpose of the following description, but it would only find
@@ -141,6 +141,21 @@ not tracked by Git remain untracked.
141 To stop tracking a file that is currently tracked, use
142 'git rm --cached'.
143
144 +To re-include files or directories when their parent directory is
145 +excluded, the following conditions must be met:
146 +
147 + - The rules to exclude a directory and re-include a subset back must
148 + be in the same .gitignore file.
149 +
150 + - The directory part in the re-include rules must be literal (i.e. no
151 + wildcards)
152 +
153 + - The rules to exclude the parent directory must not end with a
154 + trailing slash.
155 +
156 + - The rules to exclude the parent directory must have at least one
157 + slash.
158 +
159 EXAMPLES
160 --------
161
dir.c
+73 -1
@@ -733,6 +733,25 @@ int match_pathname(const char *pathname, int pathlen,
733 */
734 if (!patternlen && !namelen)
735 return 1;
736 + /*
737 + * This can happen when we ignore some exclude rules
738 + * on directories in other to see if negative rules
739 + * may match. E.g.
740 + *
741 + * /abc
742 + * !/abc/def/ghi
743 + *
744 + * The pattern of interest is "/abc". On the first
745 + * try, we should match path "abc" with this pattern
746 + * in the "if" statement right above, but the caller
747 + * ignores it.
748 + *
749 + * On the second try with paths within "abc",
750 + * e.g. "abc/xyz", we come here and try to match it
751 + * with "/abc".
752 + */
753 + if (!patternlen && namelen && *name == '/')
754 + return 1;
755 }
756
757 return fnmatch_icase_mem(pattern, patternlen,
@@ -740,6 +759,48 @@ int match_pathname(const char *pathname, int pathlen,
759 WM_PATHNAME) == 0;
760 }
761
762 +/*
763 + * Return non-zero if pathname is a directory and an ancestor of the
764 + * literal path in a (negative) pattern. This is used to keep
765 + * descending in "foo" and "foo/bar" when the pattern is
766 + * "!foo/bar/.gitignore". "foo/notbar" will not be descended however.
767 + */
768 +static int match_neg_path(const char *pathname, int pathlen, int *dtype,
769 + const char *base, int baselen,
770 + const char *pattern, int prefix, int patternlen,
771 + int flags)
772 +{
773 + assert((flags & EXC_FLAG_NEGATIVE) && !(flags & EXC_FLAG_NODIR));
774 +
775 + if (*dtype == DT_UNKNOWN)
776 + *dtype = get_dtype(NULL, pathname, pathlen);
777 + if (*dtype != DT_DIR)
778 + return 0;
779 +
780 + if (*pattern == '/') {
781 + pattern++;
782 + patternlen--;
783 + prefix--;
784 + }
785 +
786 + if (baselen) {
787 + if (((pathlen < baselen && base[pathlen] == '/') ||
788 + pathlen == baselen) &&
789 + !strncmp_icase(pathname, base, pathlen))
790 + return 1;
791 + pathname += baselen + 1;
792 + pathlen -= baselen + 1;
793 + }
794 +
795 +
796 + if (prefix &&
797 + ((pathlen < prefix && pattern[pathlen] == '/') &&
798 + !strncmp_icase(pathname, pattern, pathlen)))
799 + return 1;
800 +
801 + return 0;
802 +}
803 +
804 /*
805 * Scan the given exclude list in reverse to see whether pathname
806 * should be ignored. The first match (i.e. the last on the list), if
@@ -753,7 +814,7 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
814 struct exclude_list *el)
815 {
816 struct exclude *exc = NULL; /* undecided */
756 - int i;
817 + int i, matched_negative_path = 0;
818
819 if (!el->nr)
820 return NULL; /* undefined */
@@ -788,7 +849,18 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
849 exc = x;
850 break;
851 }
852 +
853 + if ((x->flags & EXC_FLAG_NEGATIVE) && !matched_negative_path &&
854 + match_neg_path(pathname, pathlen, dtype, x->base,
855 + x->baselen ? x->baselen - 1 : 0,
856 + exclude, prefix, x->patternlen, x->flags))
857 + matched_negative_path = 1;
858 }
859 + if (exc &&
860 + !(exc->flags & EXC_FLAG_NEGATIVE) &&
861 + !(exc->flags & EXC_FLAG_NODIR) &&
862 + matched_negative_path)
863 + exc = NULL;
864 return exc;
865 }
866
t/t3001-ls-files-others-exclude.sh
+25
@@ -305,4 +305,29 @@ test_expect_success 'ls-files with "**" patterns and no slashes' '
305 test_cmp expect actual
306 '
307
308 +test_expect_success 'negative patterns' '
309 + git init reinclude &&
310 + (
311 + cd reinclude &&
312 + cat >.gitignore <<-\EOF &&
313 + /fooo
314 + /foo
315 + !foo/bar/bar
316 + EOF
317 + mkdir fooo &&
318 + cat >fooo/.gitignore <<-\EOF &&
319 + !/*
320 + EOF
321 + mkdir -p foo/bar &&
322 + touch abc foo/def foo/bar/ghi foo/bar/bar &&
323 + git ls-files -o --exclude-standard >../actual &&
324 + cat >../expected <<-\EOF &&
325 + .gitignore
326 + abc
327 + foo/bar/bar
328 + EOF
329 + test_cmp ../expected ../actual
330 + )
331 +'
332 +
333 test_done