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 +}