Skip to content

Commit d589a67

Browse files
pcloudsgitster
authored andcommitted
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 <[email protected]> Helped-by: Micha Wiedenmann <[email protected]> Signed-off-by: Nguyễn Thái Ngọc Duy <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent c62a917 commit d589a67

File tree

4 files changed

+276
-10
lines changed

4 files changed

+276
-10
lines changed

Documentation/gitignore.txt

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,12 @@ PATTERN FORMAT
8282

8383
- An optional prefix "`!`" which negates the pattern; any
8484
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.
8986
Put a backslash ("`\`") in front of the first "`!`" for patterns
9087
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.
9191

9292
- If the pattern ends with a slash, it is removed for the
9393
purpose of the following description, but it would only find
@@ -141,6 +141,15 @@ not tracked by Git remain untracked.
141141
To stop tracking a file that is currently tracked, use
142142
'git rm --cached'.
143143

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+
144153
EXAMPLES
145154
--------
146155

dir.c

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -930,6 +930,75 @@ static int match_sticky(struct exclude *exc, const char *pathname, int pathlen,
930930
return 0;
931931
}
932932

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+
9331002
/*
9341003
* Scan the given exclude list in reverse to see whether pathname
9351004
* 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,
9431012
struct exclude_list *el)
9441013
{
9451014
struct exclude *exc = NULL; /* undecided */
946-
int i;
1015+
int i, maybe_descend = 0;
9471016

9481017
if (!el->nr)
9491018
return NULL; /* undefined */
@@ -955,6 +1024,10 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
9551024
const char *exclude = x->pattern;
9561025
int prefix = x->nowildcardlen;
9571026

1027+
if (!maybe_descend && i < el->nr - 1 &&
1028+
different_decisions(x, el->excludes[i+1]))
1029+
maybe_descend = 1;
1030+
9581031
if (x->sticky_paths.nr) {
9591032
if (*dtype == DT_UNKNOWN)
9601033
*dtype = get_dtype(NULL, pathname, pathlen);
@@ -998,6 +1071,34 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
9981071
return NULL;
9991072
}
10001073

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+
10011102
trace_printf_key(&trace_exclude, "exclude: %.*s vs %s at line %d => %s%s\n",
10021103
pathlen, pathname, exc->pattern, exc->srcpos,
10031104
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
20962197
if (has_symlink_leading_path(path, len))
20972198
return dir->nr;
20982199

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+
20992206
/*
21002207
* exclude patterns are treated like positive ones in
21012208
* create_simplify. Usually exclude patterns should be a

t/t3001-ls-files-others-exclude.sh

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,13 +175,10 @@ test_expect_success 'negated exclude matches can override previous ones' '
175175
grep "^a.1" output
176176
'
177177

178-
test_expect_success 'excluded directory overrides content patterns' '
178+
test_expect_success 'excluded directory does not override content patterns' '
179179
180180
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
185182
'
186183

187184
test_expect_success 'negated directory doesn'\''t affect content patterns' '

t/t3007-ls-files-other-negative.sh

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

0 commit comments

Comments
 (0)