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