Support pathspec magic :(exclude) and its short form :!
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
Dec 6, 2013 at 14:30 UTC
ef79b1f8704668a6cdf4278f9255e03ca785bfb4
7 files changed
+324
-13
Documentation/glossary-content.txt
+5
@@ -379,6 +379,11 @@ full pathname may have special meaning:
379
- Other consecutive asterisks are considered invalid.
380
+
381
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.
387
--
388
389
[[def_parent]]parent::
builtin/add.c
+4
-1
@@ -540,10 +540,13 @@ int cmd_add(int argc, const char **argv, const char *prefix)
540
PATHSPEC_FROMTOP |
541
PATHSPEC_LITERAL |
542
PATHSPEC_GLOB |
543
- PATHSPEC_ICASE);
543
+ PATHSPEC_ICASE |
544
+ PATHSPEC_EXCLUDE);
545
546
for (i = 0; i < pathspec.nr; i++) {
547
const char *path = pathspec.items[i].match;
548
+ if (pathspec.items[i].magic & PATHSPEC_EXCLUDE)
549
+ continue;
550
if (!seen[i] &&
551
((pathspec.items[i].magic &
552
(PATHSPEC_GLOB | PATHSPEC_ICASE)) ||
dir.c
+41
-6
@@ -126,10 +126,13 @@ static size_t common_prefix_len(const struct pathspec *pathspec)
126
PATHSPEC_MAXDEPTH |
127
PATHSPEC_LITERAL |
128
PATHSPEC_GLOB |
129
- PATHSPEC_ICASE);
129
+ PATHSPEC_ICASE |
130
+ PATHSPEC_EXCLUDE);
131
132
for (n = 0; n < pathspec->nr; n++) {
133
size_t i = 0, len = 0, item_len;
134
+ if (pathspec->items[n].magic & PATHSPEC_EXCLUDE)
135
+ continue;
136
if (pathspec->items[n].magic & PATHSPEC_ICASE)
137
item_len = pathspec->items[n].prefix;
138
else
@@ -279,9 +282,10 @@ static int match_pathspec_item(const struct pathspec_item *item, int prefix,
282
* pathspec did not match any names, which could indicate that the
283
* user mistyped the nth pathspec.
284
*/
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)
289
{
290
int i, retval = 0;
291
@@ -290,7 +294,8 @@ int match_pathspec_depth(const struct pathspec *ps,
294
PATHSPEC_MAXDEPTH |
295
PATHSPEC_LITERAL |
296
PATHSPEC_GLOB |
293
- PATHSPEC_ICASE);
297
+ PATHSPEC_ICASE |
298
+ PATHSPEC_EXCLUDE);
299
300
if (!ps->nr) {
301
if (!ps->recursive ||
@@ -309,8 +314,19 @@ int match_pathspec_depth(const struct pathspec *ps,
314
315
for (i = ps->nr - 1; i >= 0; i--) {
316
int how;
317
+
318
+ if ((!exclude && ps->items[i].magic & PATHSPEC_EXCLUDE) ||
319
+ ( exclude && !(ps->items[i].magic & PATHSPEC_EXCLUDE)))
320
+ continue;
321
+
322
if (seen && seen[i] == MATCHED_EXACTLY)
323
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;
330
how = match_pathspec_item(ps->items+i, prefix, name, namelen);
331
if (ps->recursive &&
332
(ps->magic & PATHSPEC_MAXDEPTH) &&
@@ -334,6 +350,18 @@ int match_pathspec_depth(const struct pathspec *ps,
350
return retval;
351
}
352
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
+
365
/*
366
* Return the length of the "simple" part of a path match limiter.
367
*/
@@ -1375,11 +1403,18 @@ int read_directory(struct dir_struct *dir, const char *path, int len, const stru
1403
PATHSPEC_MAXDEPTH |
1404
PATHSPEC_LITERAL |
1405
PATHSPEC_GLOB |
1378
- PATHSPEC_ICASE);
1406
+ PATHSPEC_ICASE |
1407
+ PATHSPEC_EXCLUDE);
1408
1409
if (has_symlink_leading_path(path, len))
1410
return dir->nr;
1411
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
+ */
1418
simplify = create_simplify(pathspec ? pathspec->_raw : NULL);
1419
if (!len || treat_leading_path(dir, path, len, simplify))
1420
read_directory_recursive(dir, path, len, 0, simplify);
pathspec.c
+8
-1
@@ -71,6 +71,7 @@ static struct pathspec_magic {
71
{ PATHSPEC_LITERAL, 0, "literal" },
72
{ PATHSPEC_GLOB, '\0', "glob" },
73
{ PATHSPEC_ICASE, '\0', "icase" },
74
+ { PATHSPEC_EXCLUDE, '!', "exclude" },
75
};
76
77
/*
@@ -355,7 +356,7 @@ void parse_pathspec(struct pathspec *pathspec,
356
{
357
struct pathspec_item *item;
358
const char *entry = argv ? *argv : NULL;
358
- int i, n, prefixlen;
359
+ int i, n, prefixlen, nr_exclude = 0;
360
361
memset(pathspec, 0, sizeof(*pathspec));
362
@@ -412,6 +413,8 @@ void parse_pathspec(struct pathspec *pathspec,
413
if ((flags & PATHSPEC_LITERAL_PATH) &&
414
!(magic_mask & PATHSPEC_LITERAL))
415
item[i].magic |= PATHSPEC_LITERAL;
416
+ if (item[i].magic & PATHSPEC_EXCLUDE)
417
+ nr_exclude++;
418
if (item[i].magic & magic_mask)
419
unsupported_magic(entry,
420
item[i].magic & magic_mask,
@@ -427,6 +430,10 @@ void parse_pathspec(struct pathspec *pathspec,
430
pathspec->magic |= item[i].magic;
431
}
432
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
+
437
438
if (pathspec->magic & PATHSPEC_MAXDEPTH) {
439
if (flags & PATHSPEC_KEEP_ORDER)
pathspec.h
+3
-1
@@ -7,12 +7,14 @@
7
#define PATHSPEC_LITERAL (1<<2)
8
#define PATHSPEC_GLOB (1<<3)
9
#define PATHSPEC_ICASE (1<<4)
10
+#define PATHSPEC_EXCLUDE (1<<5)
11
#define PATHSPEC_ALL_MAGIC \
12
(PATHSPEC_FROMTOP | \
13
PATHSPEC_MAXDEPTH | \
14
PATHSPEC_LITERAL | \
15
PATHSPEC_GLOB | \
15
- PATHSPEC_ICASE)
16
+ PATHSPEC_ICASE | \
17
+ PATHSPEC_EXCLUDE)
18
19
#define PATHSPEC_ONESTAR 1 /* the pathspec pattern satisfies GFNM_ONESTAR */
20
t/t6132-pathspec-exclude.sh
new
+184
@@ -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
tree-walk.c
+79
-4
@@ -662,9 +662,10 @@ static int match_wildcard_base(const struct pathspec_item *item,
662
* Pre-condition: either baselen == base_offset (i.e. empty path)
663
* or base[baselen-1] == '/' (i.e. with trailing slash).
664
*/
665
-enum interesting tree_entry_interesting(const struct name_entry *entry,
666
- struct strbuf *base, int base_offset,
667
- const struct pathspec *ps)
665
+static enum interesting do_match(const struct name_entry *entry,
666
+ struct strbuf *base, int base_offset,
667
+ const struct pathspec *ps,
668
+ int exclude)
669
{
670
int i;
671
int pathlen, baselen = base->len - base_offset;
@@ -676,7 +677,8 @@ enum interesting tree_entry_interesting(const struct name_entry *entry,
677
PATHSPEC_MAXDEPTH |
678
PATHSPEC_LITERAL |
679
PATHSPEC_GLOB |
679
- PATHSPEC_ICASE);
680
+ PATHSPEC_ICASE |
681
+ PATHSPEC_EXCLUDE);
682
683
if (!ps->nr) {
684
if (!ps->recursive ||
@@ -697,6 +699,10 @@ enum interesting tree_entry_interesting(const struct name_entry *entry,
699
const char *base_str = base->buf + base_offset;
700
int matchlen = item->len, matched = 0;
701
702
+ if ((!exclude && item->magic & PATHSPEC_EXCLUDE) ||
703
+ ( exclude && !(item->magic & PATHSPEC_EXCLUDE)))
704
+ continue;
705
+
706
if (baselen >= matchlen) {
707
/* If it doesn't match, move along... */
708
if (!match_dir_prefix(item, base_str, match, matchlen))
@@ -782,3 +788,72 @@ match_wildcards:
788
}
789
return never_interesting; /* No matches */
790
}
791
+
792
+/*
793
+ * Is a tree entry interesting given the pathspec we have?
794
+ *
795
+ * Pre-condition: either baselen == base_offset (i.e. empty path)
796
+ * or base[baselen-1] == '/' (i.e. with trailing slash).
797
+ */
798
+enum interesting tree_entry_interesting(const struct name_entry *entry,
799
+ struct strbuf *base, int base_offset,
800
+ const struct pathspec *ps)
801
+{
802
+ enum interesting positive, negative;
803
+ positive = do_match(entry, base, base_offset, ps, 0);
804
+
805
+ /*
806
+ * case | entry | positive | negative | result
807
+ * -----+-------+----------+----------+-------
808
+ * 1 | file | -1 | -1..2 | -1
809
+ * 2 | file | 0 | -1..2 | 0
810
+ * 3 | file | 1 | -1 | 1
811
+ * 4 | file | 1 | 0 | 1
812
+ * 5 | file | 1 | 1 | 0
813
+ * 6 | file | 1 | 2 | 0
814
+ * 7 | file | 2 | -1 | 2
815
+ * 8 | file | 2 | 0 | 2
816
+ * 9 | file | 2 | 1 | 0
817
+ * 10 | file | 2 | 2 | -1
818
+ * -----+-------+----------+----------+-------
819
+ * 11 | dir | -1 | -1..2 | -1
820
+ * 12 | dir | 0 | -1..2 | 0
821
+ * 13 | dir | 1 | -1 | 1
822
+ * 14 | dir | 1 | 0 | 1
823
+ * 15 | dir | 1 | 1 | 1 (*)
824
+ * 16 | dir | 1 | 2 | 0
825
+ * 17 | dir | 2 | -1 | 2
826
+ * 18 | dir | 2 | 0 | 2
827
+ * 19 | dir | 2 | 1 | 1 (*)
828
+ * 20 | dir | 2 | 2 | -1
829
+ *
830
+ * (*) An exclude pattern interested in a directory does not
831
+ * necessarily mean it will exclude all of the directory. In
832
+ * wildcard case, it can't decide until looking at individual
833
+ * files inside. So don't write such directories off yet.
834
+ */
835
+
836
+ if (!(ps->magic & PATHSPEC_EXCLUDE) ||
837
+ positive <= entry_not_interesting) /* #1, #2, #11, #12 */
838
+ return positive;
839
+
840
+ negative = do_match(entry, base, base_offset, ps, 1);
841
+
842
+ /* #3, #4, #7, #8, #13, #14, #17, #18 */
843
+ if (negative <= entry_not_interesting)
844
+ return positive;
845
+
846
+ /* #15, #19 */
847
+ if (S_ISDIR(entry->mode) &&
848
+ positive >= entry_interesting &&
849
+ negative == entry_interesting)
850
+ return entry_interesting;
851
+
852
+ if ((positive == entry_interesting &&
853
+ negative >= entry_interesting) || /* #5, #6, #16 */
854
+ (positive == all_entries_interesting &&
855
+ negative == entry_interesting)) /* #9 */
856
+ return entry_not_interesting;
857
+
858
+ return all_entries_not_interesting; /* #10, #20 */
859
+}