dir.c: don't exclude whole dir prematurely

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". Helped-by: brian m. carlson <sandals@crustytoothpaste.net> Helped-by: Micha Wiedenmann <mw-u2@gmx.de> 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 Feb 15, 2016 at 16:03 UTC d589a67eceacd1cc171bbe94906ca7c9a0edd8c5
4 files changed +276 -10
Documentation/gitignore.txt
+13 -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,15 @@ 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 EXAMPLES
154 --------
155
dir.c
+108 -1
@@ -930,6 +930,75 @@ static int match_sticky(struct exclude *exc, const char *pathname, int pathlen,
930 return 0;
931 }
932
933 +static inline int different_decisions(const struct exclude *a,
934 + const struct exclude *b)
935 +{
936 + return (a->flags & EXC_FLAG_NEGATIVE) != (b->flags & EXC_FLAG_NEGATIVE);
937 +}
938 +
939 +/*
940 + * Return non-zero if pathname is a directory and an ancestor of the
941 + * literal path in a pattern.
942 + */
943 +static int match_directory_part(const char *pathname, int pathlen,
944 + int *dtype, struct exclude *x)
945 +{
946 + const char *base = x->base;
947 + int baselen = x->baselen ? x->baselen - 1 : 0;
948 + const char *pattern = x->pattern;
949 + int prefix = x->nowildcardlen;
950 + int patternlen = x->patternlen;
951 +
952 + if (*dtype == DT_UNKNOWN)
953 + *dtype = get_dtype(NULL, pathname, pathlen);
954 + if (*dtype != DT_DIR)
955 + return 0;
956 +
957 + if (*pattern == '/') {
958 + pattern++;
959 + patternlen--;
960 + prefix--;
961 + }
962 +
963 + if (baselen) {
964 + if (((pathlen < baselen && base[pathlen] == '/') ||
965 + pathlen == baselen) &&
966 + !strncmp_icase(pathname, base, pathlen))
967 + return 1;
968 + pathname += baselen + 1;
969 + pathlen -= baselen + 1;
970 + }
971 +
972 +
973 + if (prefix &&
974 + (((pathlen < prefix && pattern[pathlen] == '/') ||
975 + pathlen == prefix) &&
976 + !strncmp_icase(pathname, pattern, pathlen)))
977 + return 1;
978 +
979 + return 0;
980 +}
981 +
982 +static struct exclude *should_descend(const char *pathname, int pathlen,
983 + int *dtype, struct exclude_list *el,
984 + struct exclude *exc)
985 +{
986 + int i;
987 +
988 + for (i = el->nr - 1; 0 <= i; i--) {
989 + struct exclude *x = el->excludes[i];
990 +
991 + if (x == exc)
992 + break;
993 +
994 + if (!(x->flags & EXC_FLAG_NODIR) &&
995 + different_decisions(x, exc) &&
996 + match_directory_part(pathname, pathlen, dtype, x))
997 + return x;
998 + }
999 + return NULL;
1000 +}
1001 +
1002 /*
1003 * Scan the given exclude list in reverse to see whether pathname
1004 * should be ignored. The first match (i.e. the last on the list), if
@@ -943,7 +1012,7 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
1012 struct exclude_list *el)
1013 {
1014 struct exclude *exc = NULL; /* undecided */
946 - int i;
1015 + int i, maybe_descend = 0;
1016
1017 if (!el->nr)
1018 return NULL; /* undefined */
@@ -955,6 +1024,10 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
1024 const char *exclude = x->pattern;
1025 int prefix = x->nowildcardlen;
1026
1027 + if (!maybe_descend && i < el->nr - 1 &&
1028 + different_decisions(x, el->excludes[i+1]))
1029 + maybe_descend = 1;
1030 +
1031 if (x->sticky_paths.nr) {
1032 if (*dtype == DT_UNKNOWN)
1033 *dtype = get_dtype(NULL, pathname, pathlen);
@@ -998,6 +1071,34 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
1071 return NULL;
1072 }
1073
1074 + /*
1075 + * We have found a matching pattern "exc" that may exclude whole
1076 + * directory. We also found that there may be a pattern that matches
1077 + * something inside the directory and reincludes stuff.
1078 + *
1079 + * Go through the patterns again, find that pattern and double check.
1080 + * If it's true, return "undecided" and keep descending in. "exc" is
1081 + * marked sticky so that it continues to match inside the directory.
1082 + */
1083 + if (!(exc->flags & EXC_FLAG_NEGATIVE) && maybe_descend) {
1084 + struct exclude *x;
1085 +
1086 + if (*dtype == DT_UNKNOWN)
1087 + *dtype = get_dtype(NULL, pathname, pathlen);
1088 +
1089 + if (*dtype == DT_DIR &&
1090 + (x = should_descend(pathname, pathlen, dtype, el, exc))) {
1091 + add_sticky(exc, pathname, pathlen);
1092 + trace_printf_key(&trace_exclude,
1093 + "exclude: %.*s vs %s at line %d => %s,"
1094 + " forced open by %s at line %d => n/a\n",
1095 + pathlen, pathname, exc->pattern, exc->srcpos,
1096 + exc->flags & EXC_FLAG_NEGATIVE ? "no" : "yes",
1097 + x->pattern, x->srcpos);
1098 + return NULL;
1099 + }
1100 + }
1101 +
1102 trace_printf_key(&trace_exclude, "exclude: %.*s vs %s at line %d => %s%s\n",
1103 pathlen, pathname, exc->pattern, exc->srcpos,
1104 exc->flags & EXC_FLAG_NEGATIVE ? "no" : "yes",
@@ -2096,6 +2197,12 @@ int read_directory(struct dir_struct *dir, const char *path, int len, const stru
2197 if (has_symlink_leading_path(path, len))
2198 return dir->nr;
2199
2200 + /*
2201 + * Stay on the safe side. if read_directory() has run once on
2202 + * "dir", some sticky flag may have been left. Clear them all.
2203 + */
2204 + clear_sticky(dir);
2205 +
2206 /*
2207 * exclude patterns are treated like positive ones in
2208 * create_simplify. Usually exclude patterns should be a
t/t3001-ls-files-others-exclude.sh
+2 -5
@@ -175,13 +175,10 @@ test_expect_success 'negated exclude matches can override previous ones' '
175 grep "^a.1" output
176 '
177
178 -test_expect_success 'excluded directory overrides content patterns' '
178 +test_expect_success 'excluded directory does not override content patterns' '
179
180 git ls-files --others --exclude="one" --exclude="!one/a.1" >output &&
181 - if grep "^one/a.1" output
182 - then
183 - false
184 - fi
181 + grep "^one/a.1" output
182 '
183
184 test_expect_success 'negated directory doesn'\''t affect content patterns' '
t/t3007-ls-files-other-negative.sh new
+153
@@ -0,0 +1,153 @@
1 +#!/bin/sh
2 +
3 +test_description='test re-include patterns'
4 +
5 +. ./test-lib.sh
6 +
7 +test_expect_success 'setup' '
8 + mkdir -p fooo foo/bar tmp &&
9 + touch abc foo/def foo/bar/ghi foo/bar/bar
10 +'
11 +
12 +test_expect_success 'no match, do not enter subdir and waste cycles' '
13 + cat >.gitignore <<-\EOF &&
14 + /tmp
15 + /foo
16 + !fooo/bar/bar
17 + EOF
18 + GIT_TRACE_EXCLUDE="$(pwd)/tmp/trace" git ls-files -o --exclude-standard >tmp/actual &&
19 + ! grep "enter .foo/.\$" tmp/trace &&
20 + cat >tmp/expected <<-\EOF &&
21 + .gitignore
22 + abc
23 + EOF
24 + test_cmp tmp/expected tmp/actual
25 +'
26 +
27 +test_expect_success 'match, excluded by literal pathname pattern' '
28 + cat >.gitignore <<-\EOF &&
29 + /tmp
30 + /fooo
31 + /foo
32 + !foo/bar/bar
33 + EOF
34 + cat >fooo/.gitignore <<-\EOF &&
35 + !/*
36 + EOF git ls-files -o --exclude-standard >tmp/actual &&
37 + cat >tmp/expected <<-\EOF &&
38 + .gitignore
39 + abc
40 + foo/bar/bar
41 + EOF
42 + test_cmp tmp/expected tmp/actual
43 +'
44 +
45 +test_expect_success 'match, excluded by wildcard pathname pattern' '
46 + cat >.gitignore <<-\EOF &&
47 + /tmp
48 + /fooo
49 + /fo?
50 + !foo/bar/bar
51 + EOF
52 + git ls-files -o --exclude-standard >tmp/actual &&
53 + cat >tmp/expected <<-\EOF &&
54 + .gitignore
55 + abc
56 + foo/bar/bar
57 + EOF
58 + test_cmp tmp/expected tmp/actual
59 +'
60 +
61 +test_expect_success 'match, excluded by literal basename pattern' '
62 + cat >.gitignore <<-\EOF &&
63 + /tmp
64 + /fooo
65 + foo
66 + !foo/bar/bar
67 + EOF
68 + git ls-files -o --exclude-standard >tmp/actual &&
69 + cat >tmp/expected <<-\EOF &&
70 + .gitignore
71 + abc
72 + foo/bar/bar
73 + EOF
74 + test_cmp tmp/expected tmp/actual
75 +'
76 +
77 +test_expect_success 'match, excluded by wildcard basename pattern' '
78 + cat >.gitignore <<-\EOF &&
79 + /tmp
80 + /fooo
81 + fo?
82 + !foo/bar/bar
83 + EOF
84 + git ls-files -o --exclude-standard >tmp/actual &&
85 + cat >tmp/expected <<-\EOF &&
86 + .gitignore
87 + abc
88 + foo/bar/bar
89 + EOF
90 + test_cmp tmp/expected tmp/actual
91 +'
92 +
93 +test_expect_success 'match, excluded by literal mustbedir, basename pattern' '
94 + cat >.gitignore <<-\EOF &&
95 + /tmp
96 + /fooo
97 + foo/
98 + !foo/bar/bar
99 + EOF
100 + git ls-files -o --exclude-standard >tmp/actual &&
101 + cat >tmp/expected <<-\EOF &&
102 + .gitignore
103 + abc
104 + foo/bar/bar
105 + EOF
106 + test_cmp tmp/expected tmp/actual
107 +'
108 +
109 +test_expect_success 'match, excluded by literal mustbedir, pathname pattern' '
110 + cat >.gitignore <<-\EOF &&
111 + /tmp
112 + /fooo
113 + /foo/
114 + !foo/bar/bar
115 + EOF
116 + git ls-files -o --exclude-standard >tmp/actual &&
117 + cat >tmp/expected <<-\EOF &&
118 + .gitignore
119 + abc
120 + foo/bar/bar
121 + EOF
122 + test_cmp tmp/expected tmp/actual
123 +'
124 +
125 +test_expect_success 'prepare for nested negatives' '
126 + cat >.git/info/exclude <<-\EOF &&
127 + /.gitignore
128 + /tmp
129 + /foo
130 + /abc
131 + EOF
132 + git ls-files -o --exclude-standard >tmp/actual &&
133 + test_must_be_empty tmp/actual &&
134 + mkdir -p 1/2/3/4 &&
135 + touch 1/f 1/2/f 1/2/3/f 1/2/3/4/f
136 +'
137 +
138 +test_expect_success 'match, literal pathname, nested negatives' '
139 + cat >.gitignore <<-\EOF &&
140 + /1
141 + !1/2
142 + 1/2/3
143 + !1/2/3/4
144 + EOF
145 + git ls-files -o --exclude-standard >tmp/actual &&
146 + cat >tmp/expected <<-\EOF &&
147 + 1/2/3/4/f
148 + 1/2/f
149 + EOF
150 + test_cmp tmp/expected tmp/actual
151 +'
152 +
153 +test_done