Skip to content

Commit ef79b1f

Browse files
pcloudsgitster
authored andcommitted
Support pathspec magic :(exclude) and its short form :!
Signed-off-by: Nguyễn Thái Ngọc Duy <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 8b7cb51 commit ef79b1f

File tree

7 files changed

+324
-13
lines changed

7 files changed

+324
-13
lines changed

Documentation/glossary-content.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,11 @@ full pathname may have special meaning:
379379
- Other consecutive asterisks are considered invalid.
380380
+
381381
Glob magic is incompatible with literal magic.
382+
383+
exclude;;
384+
After a path matches any non-exclude pathspec, it will be run
385+
through all exclude pathspec (magic signature: `!`). If it
386+
matches, the path is ignored.
382387
--
383388

384389
[[def_parent]]parent::

builtin/add.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,10 +540,13 @@ int cmd_add(int argc, const char **argv, const char *prefix)
540540
PATHSPEC_FROMTOP |
541541
PATHSPEC_LITERAL |
542542
PATHSPEC_GLOB |
543-
PATHSPEC_ICASE);
543+
PATHSPEC_ICASE |
544+
PATHSPEC_EXCLUDE);
544545

545546
for (i = 0; i < pathspec.nr; i++) {
546547
const char *path = pathspec.items[i].match;
548+
if (pathspec.items[i].magic & PATHSPEC_EXCLUDE)
549+
continue;
547550
if (!seen[i] &&
548551
((pathspec.items[i].magic &
549552
(PATHSPEC_GLOB | PATHSPEC_ICASE)) ||

dir.c

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,13 @@ static size_t common_prefix_len(const struct pathspec *pathspec)
126126
PATHSPEC_MAXDEPTH |
127127
PATHSPEC_LITERAL |
128128
PATHSPEC_GLOB |
129-
PATHSPEC_ICASE);
129+
PATHSPEC_ICASE |
130+
PATHSPEC_EXCLUDE);
130131

131132
for (n = 0; n < pathspec->nr; n++) {
132133
size_t i = 0, len = 0, item_len;
134+
if (pathspec->items[n].magic & PATHSPEC_EXCLUDE)
135+
continue;
133136
if (pathspec->items[n].magic & PATHSPEC_ICASE)
134137
item_len = pathspec->items[n].prefix;
135138
else
@@ -279,9 +282,10 @@ static int match_pathspec_item(const struct pathspec_item *item, int prefix,
279282
* pathspec did not match any names, which could indicate that the
280283
* user mistyped the nth pathspec.
281284
*/
282-
int match_pathspec_depth(const struct pathspec *ps,
283-
const char *name, int namelen,
284-
int prefix, char *seen)
285+
static int match_pathspec_depth_1(const struct pathspec *ps,
286+
const char *name, int namelen,
287+
int prefix, char *seen,
288+
int exclude)
285289
{
286290
int i, retval = 0;
287291

@@ -290,7 +294,8 @@ int match_pathspec_depth(const struct pathspec *ps,
290294
PATHSPEC_MAXDEPTH |
291295
PATHSPEC_LITERAL |
292296
PATHSPEC_GLOB |
293-
PATHSPEC_ICASE);
297+
PATHSPEC_ICASE |
298+
PATHSPEC_EXCLUDE);
294299

295300
if (!ps->nr) {
296301
if (!ps->recursive ||
@@ -309,8 +314,19 @@ int match_pathspec_depth(const struct pathspec *ps,
309314

310315
for (i = ps->nr - 1; i >= 0; i--) {
311316
int how;
317+
318+
if ((!exclude && ps->items[i].magic & PATHSPEC_EXCLUDE) ||
319+
( exclude && !(ps->items[i].magic & PATHSPEC_EXCLUDE)))
320+
continue;
321+
312322
if (seen && seen[i] == MATCHED_EXACTLY)
313323
continue;
324+
/*
325+
* Make exclude patterns optional and never report
326+
* "pathspec ':(exclude)foo' matches no files"
327+
*/
328+
if (seen && ps->items[i].magic & PATHSPEC_EXCLUDE)
329+
seen[i] = MATCHED_FNMATCH;
314330
how = match_pathspec_item(ps->items+i, prefix, name, namelen);
315331
if (ps->recursive &&
316332
(ps->magic & PATHSPEC_MAXDEPTH) &&
@@ -334,6 +350,18 @@ int match_pathspec_depth(const struct pathspec *ps,
334350
return retval;
335351
}
336352

353+
int match_pathspec_depth(const struct pathspec *ps,
354+
const char *name, int namelen,
355+
int prefix, char *seen)
356+
{
357+
int positive, negative;
358+
positive = match_pathspec_depth_1(ps, name, namelen, prefix, seen, 0);
359+
if (!(ps->magic & PATHSPEC_EXCLUDE) || !positive)
360+
return positive;
361+
negative = match_pathspec_depth_1(ps, name, namelen, prefix, seen, 1);
362+
return negative ? 0 : positive;
363+
}
364+
337365
/*
338366
* Return the length of the "simple" part of a path match limiter.
339367
*/
@@ -1375,11 +1403,18 @@ int read_directory(struct dir_struct *dir, const char *path, int len, const stru
13751403
PATHSPEC_MAXDEPTH |
13761404
PATHSPEC_LITERAL |
13771405
PATHSPEC_GLOB |
1378-
PATHSPEC_ICASE);
1406+
PATHSPEC_ICASE |
1407+
PATHSPEC_EXCLUDE);
13791408

13801409
if (has_symlink_leading_path(path, len))
13811410
return dir->nr;
13821411

1412+
/*
1413+
* exclude patterns are treated like positive ones in
1414+
* create_simplify. Usually exclude patterns should be a
1415+
* subset of positive ones, which has no impacts on
1416+
* create_simplify().
1417+
*/
13831418
simplify = create_simplify(pathspec ? pathspec->_raw : NULL);
13841419
if (!len || treat_leading_path(dir, path, len, simplify))
13851420
read_directory_recursive(dir, path, len, 0, simplify);

pathspec.c

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ static struct pathspec_magic {
7171
{ PATHSPEC_LITERAL, 0, "literal" },
7272
{ PATHSPEC_GLOB, '\0', "glob" },
7373
{ PATHSPEC_ICASE, '\0', "icase" },
74+
{ PATHSPEC_EXCLUDE, '!', "exclude" },
7475
};
7576

7677
/*
@@ -355,7 +356,7 @@ void parse_pathspec(struct pathspec *pathspec,
355356
{
356357
struct pathspec_item *item;
357358
const char *entry = argv ? *argv : NULL;
358-
int i, n, prefixlen;
359+
int i, n, prefixlen, nr_exclude = 0;
359360

360361
memset(pathspec, 0, sizeof(*pathspec));
361362

@@ -412,6 +413,8 @@ void parse_pathspec(struct pathspec *pathspec,
412413
if ((flags & PATHSPEC_LITERAL_PATH) &&
413414
!(magic_mask & PATHSPEC_LITERAL))
414415
item[i].magic |= PATHSPEC_LITERAL;
416+
if (item[i].magic & PATHSPEC_EXCLUDE)
417+
nr_exclude++;
415418
if (item[i].magic & magic_mask)
416419
unsupported_magic(entry,
417420
item[i].magic & magic_mask,
@@ -427,6 +430,10 @@ void parse_pathspec(struct pathspec *pathspec,
427430
pathspec->magic |= item[i].magic;
428431
}
429432

433+
if (nr_exclude == n)
434+
die(_("There is nothing to exclude from by :(exclude) patterns.\n"
435+
"Perhaps you forgot to add either ':/' or '.' ?"));
436+
430437

431438
if (pathspec->magic & PATHSPEC_MAXDEPTH) {
432439
if (flags & PATHSPEC_KEEP_ORDER)

pathspec.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
#define PATHSPEC_LITERAL (1<<2)
88
#define PATHSPEC_GLOB (1<<3)
99
#define PATHSPEC_ICASE (1<<4)
10+
#define PATHSPEC_EXCLUDE (1<<5)
1011
#define PATHSPEC_ALL_MAGIC \
1112
(PATHSPEC_FROMTOP | \
1213
PATHSPEC_MAXDEPTH | \
1314
PATHSPEC_LITERAL | \
1415
PATHSPEC_GLOB | \
15-
PATHSPEC_ICASE)
16+
PATHSPEC_ICASE | \
17+
PATHSPEC_EXCLUDE)
1618

1719
#define PATHSPEC_ONESTAR 1 /* the pathspec pattern satisfies GFNM_ONESTAR */
1820

t/t6132-pathspec-exclude.sh

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
#!/bin/sh
2+
3+
test_description='test case exclude pathspec'
4+
5+
. ./test-lib.sh
6+
7+
test_expect_success 'setup' '
8+
for p in file sub/file sub/sub/file sub/file2 sub/sub/sub/file sub2/file; do
9+
if echo $p | grep /; then
10+
mkdir -p `dirname $p`
11+
fi &&
12+
: >$p &&
13+
git add $p &&
14+
git commit -m $p
15+
done &&
16+
git log --oneline --format=%s >actual &&
17+
cat <<EOF >expect &&
18+
sub2/file
19+
sub/sub/sub/file
20+
sub/file2
21+
sub/sub/file
22+
sub/file
23+
file
24+
EOF
25+
test_cmp expect actual
26+
'
27+
28+
test_expect_success 'exclude only should error out' '
29+
test_must_fail git log --oneline --format=%s -- ":(exclude)sub"
30+
'
31+
32+
test_expect_success 't_e_i() exclude sub' '
33+
git log --oneline --format=%s -- . ":(exclude)sub" >actual
34+
cat <<EOF >expect &&
35+
sub2/file
36+
file
37+
EOF
38+
test_cmp expect actual
39+
'
40+
41+
test_expect_success 't_e_i() exclude sub/sub/file' '
42+
git log --oneline --format=%s -- . ":(exclude)sub/sub/file" >actual
43+
cat <<EOF >expect &&
44+
sub2/file
45+
sub/sub/sub/file
46+
sub/file2
47+
sub/file
48+
file
49+
EOF
50+
test_cmp expect actual
51+
'
52+
53+
test_expect_success 't_e_i() exclude sub using mnemonic' '
54+
git log --oneline --format=%s -- . ":!sub" >actual
55+
cat <<EOF >expect &&
56+
sub2/file
57+
file
58+
EOF
59+
test_cmp expect actual
60+
'
61+
62+
test_expect_success 't_e_i() exclude :(icase)SUB' '
63+
git log --oneline --format=%s -- . ":(exclude,icase)SUB" >actual
64+
cat <<EOF >expect &&
65+
sub2/file
66+
file
67+
EOF
68+
test_cmp expect actual
69+
'
70+
71+
test_expect_success 't_e_i() exclude sub2 from sub' '
72+
(
73+
cd sub &&
74+
git log --oneline --format=%s -- :/ ":/!sub2" >actual
75+
cat <<EOF >expect &&
76+
sub/sub/sub/file
77+
sub/file2
78+
sub/sub/file
79+
sub/file
80+
file
81+
EOF
82+
test_cmp expect actual
83+
)
84+
'
85+
86+
test_expect_success 't_e_i() exclude sub/*file' '
87+
git log --oneline --format=%s -- . ":(exclude)sub/*file" >actual
88+
cat <<EOF >expect &&
89+
sub2/file
90+
sub/file2
91+
file
92+
EOF
93+
test_cmp expect actual
94+
'
95+
96+
test_expect_success 't_e_i() exclude :(glob)sub/*/file' '
97+
git log --oneline --format=%s -- . ":(exclude,glob)sub/*/file" >actual
98+
cat <<EOF >expect &&
99+
sub2/file
100+
sub/sub/sub/file
101+
sub/file2
102+
sub/file
103+
file
104+
EOF
105+
test_cmp expect actual
106+
'
107+
108+
test_expect_success 'm_p_d() exclude sub' '
109+
git ls-files -- . ":(exclude)sub" >actual
110+
cat <<EOF >expect &&
111+
file
112+
sub2/file
113+
EOF
114+
test_cmp expect actual
115+
'
116+
117+
test_expect_success 'm_p_d() exclude sub/sub/file' '
118+
git ls-files -- . ":(exclude)sub/sub/file" >actual
119+
cat <<EOF >expect &&
120+
file
121+
sub/file
122+
sub/file2
123+
sub/sub/sub/file
124+
sub2/file
125+
EOF
126+
test_cmp expect actual
127+
'
128+
129+
test_expect_success 'm_p_d() exclude sub using mnemonic' '
130+
git ls-files -- . ":!sub" >actual
131+
cat <<EOF >expect &&
132+
file
133+
sub2/file
134+
EOF
135+
test_cmp expect actual
136+
'
137+
138+
test_expect_success 'm_p_d() exclude :(icase)SUB' '
139+
git ls-files -- . ":(exclude,icase)SUB" >actual
140+
cat <<EOF >expect &&
141+
file
142+
sub2/file
143+
EOF
144+
test_cmp expect actual
145+
'
146+
147+
test_expect_success 'm_p_d() exclude sub2 from sub' '
148+
(
149+
cd sub &&
150+
git ls-files -- :/ ":/!sub2" >actual
151+
cat <<EOF >expect &&
152+
../file
153+
file
154+
file2
155+
sub/file
156+
sub/sub/file
157+
EOF
158+
test_cmp expect actual
159+
)
160+
'
161+
162+
test_expect_success 'm_p_d() exclude sub/*file' '
163+
git ls-files -- . ":(exclude)sub/*file" >actual
164+
cat <<EOF >expect &&
165+
file
166+
sub/file2
167+
sub2/file
168+
EOF
169+
test_cmp expect actual
170+
'
171+
172+
test_expect_success 'm_p_d() exclude :(glob)sub/*/file' '
173+
git ls-files -- . ":(exclude,glob)sub/*/file" >actual
174+
cat <<EOF >expect &&
175+
file
176+
sub/file
177+
sub/file2
178+
sub/sub/sub/file
179+
sub2/file
180+
EOF
181+
test_cmp expect actual
182+
'
183+
184+
test_done

0 commit comments

Comments
 (0)