pathspec: support :(glob) syntax

:(glob)path differs from plain pathspec that it uses wildmatch with WM_PATHNAME while the other uses fnmatch without FNM_PATHNAME. The difference lies in how '*' (and '**') is processed. With the introduction of :(glob) and :(literal) and their global options --[no]glob-pathspecs, the user can: - make everything literal by default via --noglob-pathspecs --literal-pathspecs cannot be used for this purpose as it disables _all_ pathspec magic. - individually turn on globbing with :(glob) - make everything globbing by default via --glob-pathspecs - individually turn off globbing with :(literal) The implication behind this is, there is no way to gain the default matching behavior (i.e. fnmatch without FNM_PATHNAME). You either get new globbing or literal. The old fnmatch behavior is considered deprecated and discouraged to use. 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 Jul 14, 2013 at 15:36 UTC bd30c2e48432c692f9e77d3529c9cf25117066bb
12 files changed +198 -31
Documentation/git.txt
+19
@@ -454,6 +454,17 @@ help ...`.
454 This is equivalent to setting the `GIT_LITERAL_PATHSPECS` environment
455 variable to `1`.
456
457 +--glob-pathspecs:
458 + Add "glob" magic to all pathspec. This is equivalent to setting
459 + the `GIT_GLOB_PATHSPECS` environment variable to `1`. Disabling
460 + globbing on individual pathspecs can be done using pathspec
461 + magic ":(literal)"
462 +
463 +--noglob-pathspecs:
464 + Add "literal" magic to all pathspec. This is equivalent to setting
465 + the `GIT_NOGLOB_PATHSPECS` environment variable to `1`. Enabling
466 + globbing on individual pathspecs can be done using pathspec
467 + magic ":(glob)"
468
469 GIT COMMANDS
470 ------------
@@ -860,6 +871,14 @@ GIT_LITERAL_PATHSPECS::
871 literal paths to Git (e.g., paths previously given to you by
872 `git ls-tree`, `--raw` diff output, etc).
873
874 +GIT_GLOB_PATHSPECS::
875 + Setting this variable to `1` will cause Git to treat all
876 + pathspecs as glob patterns (aka "glob" magic).
877 +
878 +GIT_NOGLOB_PATHSPECS::
879 + Setting this variable to `1` will cause Git to treat all
880 + pathspecs as literal (aka "literal" magic).
881 +
882
883 Discussion[[Discussion]]
884 ------------------------
Documentation/glossary-content.txt
+29
@@ -333,6 +333,35 @@ top `/`;;
333 literal;;
334 Wildcards in the pattern such as `*` or `?` are treated
335 as literal characters.
336 +
337 +glob;;
338 + Git treats the pattern as a shell glob suitable for
339 + consumption by fnmatch(3) with the FNM_PATHNAME flag:
340 + wildcards in the pattern will not match a / in the pathname.
341 + For example, "Documentation/{asterisk}.html" matches
342 + "Documentation/git.html" but not "Documentation/ppc/ppc.html"
343 + or "tools/perf/Documentation/perf.html".
344 ++
345 +Two consecutive asterisks ("`**`") in patterns matched against
346 +full pathname may have special meaning:
347 +
348 + - A leading "`**`" followed by a slash means match in all
349 + directories. For example, "`**/foo`" matches file or directory
350 + "`foo`" anywhere, the same as pattern "`foo`". "**/foo/bar"
351 + matches file or directory "`bar`" anywhere that is directly
352 + under directory "`foo`".
353 +
354 + - A trailing "/**" matches everything inside. For example,
355 + "abc/**" matches all files inside directory "abc", relative
356 + to the location of the `.gitignore` file, with infinite depth.
357 +
358 + - A slash followed by two consecutive asterisks then a slash
359 + matches zero or more directories. For example, "`a/**/b`"
360 + matches "`a/b`", "`a/x/b`", "`a/x/y/b`" and so on.
361 +
362 + - Other consecutive asterisks are considered invalid.
363 ++
364 +Glob magic is incompatible with literal magic.
365 --
366 +
367 Currently only the slash `/` is recognized as the "magic signature",
builtin/add.c
+7 -2
@@ -541,11 +541,16 @@ int cmd_add(int argc, const char **argv, const char *prefix)
541 /*
542 * file_exists() assumes exact match
543 */
544 - GUARD_PATHSPEC(&pathspec, PATHSPEC_FROMTOP | PATHSPEC_LITERAL);
544 + GUARD_PATHSPEC(&pathspec,
545 + PATHSPEC_FROMTOP |
546 + PATHSPEC_LITERAL |
547 + PATHSPEC_GLOB);
548
549 for (i = 0; i < pathspec.nr; i++) {
550 const char *path = pathspec.items[i].match;
548 - if (!seen[i] && !file_exists(path)) {
551 + if (!seen[i] &&
552 + ((pathspec.items[i].magic & PATHSPEC_GLOB) ||
553 + !file_exists(path))) {
554 if (ignore_missing) {
555 int dtype = DT_UNKNOWN;
556 if (is_excluded(&dir, path, &dtype))
builtin/ls-tree.c
+1 -1
@@ -173,7 +173,7 @@ int cmd_ls_tree(int argc, const char **argv, const char *prefix)
173 * cannot be lifted until it is converted to use
174 * match_pathspec_depth() or tree_entry_interesting()
175 */
176 - parse_pathspec(&pathspec, 0,
176 + parse_pathspec(&pathspec, PATHSPEC_GLOB,
177 PATHSPEC_PREFER_CWD,
178 prefix, argv + 1);
179 for (i = 0; i < pathspec.nr; i++)
cache.h
+2
@@ -367,6 +367,8 @@ static inline enum object_type object_type(unsigned int mode)
367 #define GIT_NOTES_REWRITE_REF_ENVIRONMENT "GIT_NOTES_REWRITE_REF"
368 #define GIT_NOTES_REWRITE_MODE_ENVIRONMENT "GIT_NOTES_REWRITE_MODE"
369 #define GIT_LITERAL_PATHSPECS_ENVIRONMENT "GIT_LITERAL_PATHSPECS"
370 +#define GIT_GLOB_PATHSPECS_ENVIRONMENT "GIT_GLOB_PATHSPECS"
371 +#define GIT_NOGLOB_PATHSPECS_ENVIRONMENT "GIT_NOGLOB_PATHSPECS"
372
373 /*
374 * This environment variable is expected to contain a boolean indicating
dir.c
+16 -12
@@ -52,26 +52,28 @@ int fnmatch_icase(const char *pattern, const char *string, int flags)
52 return fnmatch(pattern, string, flags | (ignore_case ? FNM_CASEFOLD : 0));
53 }
54
55 -inline int git_fnmatch(const char *pattern, const char *string,
56 - int flags, int prefix)
55 +inline int git_fnmatch(const struct pathspec_item *item,
56 + const char *pattern, const char *string,
57 + int prefix)
58 {
58 - int fnm_flags = 0;
59 - if (flags & GFNM_PATHNAME)
60 - fnm_flags |= FNM_PATHNAME;
59 if (prefix > 0) {
60 if (strncmp(pattern, string, prefix))
61 return FNM_NOMATCH;
62 pattern += prefix;
63 string += prefix;
64 }
67 - if (flags & GFNM_ONESTAR) {
65 + if (item->flags & PATHSPEC_ONESTAR) {
66 int pattern_len = strlen(++pattern);
67 int string_len = strlen(string);
68 return string_len < pattern_len ||
69 strcmp(pattern,
70 string + string_len - pattern_len);
71 }
74 - return fnmatch(pattern, string, fnm_flags);
72 + if (item->magic & PATHSPEC_GLOB)
73 + return wildmatch(pattern, string, WM_PATHNAME, NULL);
74 + else
75 + /* wildmatch has not learned no FNM_PATHNAME mode yet */
76 + return fnmatch(pattern, string, 0);
77 }
78
79 static int fnmatch_icase_mem(const char *pattern, int patternlen,
@@ -111,7 +113,8 @@ static size_t common_prefix_len(const struct pathspec *pathspec)
113 GUARD_PATHSPEC(pathspec,
114 PATHSPEC_FROMTOP |
115 PATHSPEC_MAXDEPTH |
114 - PATHSPEC_LITERAL);
116 + PATHSPEC_LITERAL |
117 + PATHSPEC_GLOB);
118
119 for (n = 0; n < pathspec->nr; n++) {
120 size_t i = 0, len = 0;
@@ -206,8 +209,7 @@ static int match_pathspec_item(const struct pathspec_item *item, int prefix,
209 }
210
211 if (item->nowildcard_len < item->len &&
209 - !git_fnmatch(match, name,
210 - item->flags & PATHSPEC_ONESTAR ? GFNM_ONESTAR : 0,
212 + !git_fnmatch(item, match, name,
213 item->nowildcard_len - prefix))
214 return MATCHED_FNMATCH;
215
@@ -238,7 +240,8 @@ int match_pathspec_depth(const struct pathspec *ps,
240 GUARD_PATHSPEC(ps,
241 PATHSPEC_FROMTOP |
242 PATHSPEC_MAXDEPTH |
241 - PATHSPEC_LITERAL);
243 + PATHSPEC_LITERAL |
244 + PATHSPEC_GLOB);
245
246 if (!ps->nr) {
247 if (!ps->recursive ||
@@ -1297,7 +1300,8 @@ int read_directory(struct dir_struct *dir, const char *path, int len, const stru
1300 GUARD_PATHSPEC(pathspec,
1301 PATHSPEC_FROMTOP |
1302 PATHSPEC_MAXDEPTH |
1300 - PATHSPEC_LITERAL);
1303 + PATHSPEC_LITERAL |
1304 + PATHSPEC_GLOB);
1305
1306 if (has_symlink_leading_path(path, len))
1307 return dir->nr;
dir.h
+4 -5
@@ -199,10 +199,9 @@ extern int fnmatch_icase(const char *pattern, const char *string, int flags);
199 /*
200 * The prefix part of pattern must not contains wildcards.
201 */
202 -#define GFNM_PATHNAME 1 /* similar to FNM_PATHNAME */
203 -#define GFNM_ONESTAR 2 /* there is only _one_ wildcard, a star */
204 -
205 -extern int git_fnmatch(const char *pattern, const char *string,
206 - int flags, int prefix);
202 +struct pathspec_item;
203 +extern int git_fnmatch(const struct pathspec_item *item,
204 + const char *pattern, const char *string,
205 + int prefix);
206
207 #endif
git.c
+8
@@ -147,6 +147,14 @@ static int handle_options(const char ***argv, int *argc, int *envchanged)
147 setenv(GIT_LITERAL_PATHSPECS_ENVIRONMENT, "0", 1);
148 if (envchanged)
149 *envchanged = 1;
150 + } else if (!strcmp(cmd, "--glob-pathspecs")) {
151 + setenv(GIT_GLOB_PATHSPECS_ENVIRONMENT, "1", 1);
152 + if (envchanged)
153 + *envchanged = 1;
154 + } else if (!strcmp(cmd, "--noglob-pathspecs")) {
155 + setenv(GIT_NOGLOB_PATHSPECS_ENVIRONMENT, "1", 1);
156 + if (envchanged)
157 + *envchanged = 1;
158 } else if (!strcmp(cmd, "--shallow-file")) {
159 (*argv)++;
160 (*argc)--;
pathspec.c
+42 -5
@@ -57,7 +57,6 @@ char *find_pathspecs_matching_against_index(const struct pathspec *pathspec)
57 *
58 * Possible future magic semantics include stuff like:
59 *
60 - * { PATHSPEC_NOGLOB, '!', "noglob" },
60 * { PATHSPEC_ICASE, '\0', "icase" },
61 * { PATHSPEC_RECURSIVE, '*', "recursive" },
62 * { PATHSPEC_REGEXP, '\0', "regexp" },
@@ -71,6 +70,7 @@ static struct pathspec_magic {
70 } pathspec_magic[] = {
71 { PATHSPEC_FROMTOP, '/', "top" },
72 { PATHSPEC_LITERAL, 0, "literal" },
73 + { PATHSPEC_GLOB, '\0', "glob" },
74 };
75
76 /*
@@ -93,6 +93,8 @@ static unsigned prefix_pathspec(struct pathspec_item *item,
93 const char *elt)
94 {
95 static int literal_global = -1;
96 + static int glob_global = -1;
97 + static int noglob_global = -1;
98 unsigned magic = 0, short_magic = 0, global_magic = 0;
99 const char *copyfrom = elt, *long_magic_end = NULL;
100 char *match;
@@ -103,6 +105,22 @@ static unsigned prefix_pathspec(struct pathspec_item *item,
105 if (literal_global)
106 global_magic |= PATHSPEC_LITERAL;
107
108 + if (glob_global < 0)
109 + glob_global = git_env_bool(GIT_GLOB_PATHSPECS_ENVIRONMENT, 0);
110 + if (glob_global)
111 + global_magic |= PATHSPEC_GLOB;
112 +
113 + if (noglob_global < 0)
114 + noglob_global = git_env_bool(GIT_NOGLOB_PATHSPECS_ENVIRONMENT, 0);
115 +
116 + if (glob_global && noglob_global)
117 + die(_("global 'glob' and 'noglob' pathspec settings are incompatible"));
118 +
119 + if ((global_magic & PATHSPEC_LITERAL) &&
120 + (global_magic & ~PATHSPEC_LITERAL))
121 + die(_("global 'literal' pathspec setting is incompatible "
122 + "with all other global pathspec settings"));
123 +
124 if (elt[0] != ':' || literal_global) {
125 ; /* nothing to do */
126 } else if (elt[1] == '(') {
@@ -167,12 +185,24 @@ static unsigned prefix_pathspec(struct pathspec_item *item,
185
186 magic |= short_magic;
187 *p_short_magic = short_magic;
188 +
189 + /* --noglob-pathspec adds :(literal) _unless_ :(glob) is specifed */
190 + if (noglob_global && !(magic & PATHSPEC_GLOB))
191 + global_magic |= PATHSPEC_LITERAL;
192 +
193 + /* --glob-pathspec is overriden by :(literal) */
194 + if ((global_magic & PATHSPEC_GLOB) && (magic & PATHSPEC_LITERAL))
195 + global_magic &= ~PATHSPEC_GLOB;
196 +
197 magic |= global_magic;
198
199 if (pathspec_prefix >= 0 &&
200 (prefixlen || (prefix && *prefix)))
201 die("BUG: 'prefix' magic is supposed to be used at worktree's root");
202
203 + if ((magic & PATHSPEC_LITERAL) && (magic & PATHSPEC_GLOB))
204 + die(_("%s: 'literal' and 'glob' are incompatible"), elt);
205 +
206 if (pathspec_prefix >= 0) {
207 match = xstrdup(copyfrom);
208 prefixlen = pathspec_prefix;
@@ -248,10 +278,17 @@ static unsigned prefix_pathspec(struct pathspec_item *item,
278 item->nowildcard_len = prefixlen;
279 }
280 item->flags = 0;
251 - if (item->nowildcard_len < item->len &&
252 - item->match[item->nowildcard_len] == '*' &&
253 - no_wildcard(item->match + item->nowildcard_len + 1))
254 - item->flags |= PATHSPEC_ONESTAR;
281 + if (magic & PATHSPEC_GLOB) {
282 + /*
283 + * FIXME: should we enable ONESTAR in _GLOB for
284 + * pattern "* * / * . c"?
285 + */
286 + } else {
287 + if (item->nowildcard_len < item->len &&
288 + item->match[item->nowildcard_len] == '*' &&
289 + no_wildcard(item->match + item->nowildcard_len + 1))
290 + item->flags |= PATHSPEC_ONESTAR;
291 + }
292
293 /* sanity checks, pathspec matchers assume these are sane */
294 assert(item->nowildcard_len <= item->len &&
pathspec.h
+3 -1
@@ -5,10 +5,12 @@
5 #define PATHSPEC_FROMTOP (1<<0)
6 #define PATHSPEC_MAXDEPTH (1<<1)
7 #define PATHSPEC_LITERAL (1<<2)
8 +#define PATHSPEC_GLOB (1<<3)
9 #define PATHSPEC_ALL_MAGIC \
10 (PATHSPEC_FROMTOP | \
11 PATHSPEC_MAXDEPTH | \
11 - PATHSPEC_LITERAL)
12 + PATHSPEC_LITERAL | \
13 + PATHSPEC_GLOB)
14
15 #define PATHSPEC_ONESTAR 1 /* the pathspec pattern sastisfies GFNM_ONESTAR */
16
t/t6130-pathspec-noglob.sh
+63
@@ -32,6 +32,16 @@ test_expect_success 'star pathspec globs' '
32 test_cmp expect actual
33 '
34
35 +test_expect_success 'star pathspec globs' '
36 + cat >expect <<-\EOF &&
37 + bracket
38 + star
39 + vanilla
40 + EOF
41 + git log --format=%s -- ":(glob)f*" >actual &&
42 + test_cmp expect actual
43 +'
44 +
45 test_expect_success 'bracket pathspec globs and matches literal brackets' '
46 cat >expect <<-\EOF &&
47 bracket
@@ -41,6 +51,15 @@ test_expect_success 'bracket pathspec globs and matches literal brackets' '
51 test_cmp expect actual
52 '
53
54 +test_expect_success 'bracket pathspec globs and matches literal brackets' '
55 + cat >expect <<-\EOF &&
56 + bracket
57 + vanilla
58 + EOF
59 + git log --format=%s -- ":(glob)f[o][o]" >actual &&
60 + test_cmp expect actual
61 +'
62 +
63 test_expect_success 'no-glob option matches literally (vanilla)' '
64 echo vanilla >expect &&
65 git --literal-pathspecs log --format=%s -- foo >actual &&
@@ -89,4 +108,48 @@ test_expect_success 'no-glob environment variable works' '
108 test_cmp expect actual
109 '
110
111 +test_expect_success 'setup xxx/bar' '
112 + mkdir xxx &&
113 + test_commit xxx xxx/bar
114 +'
115 +
116 +test_expect_success '**/ works with :(glob)' '
117 + cat >expect <<-\EOF &&
118 + xxx
119 + unrelated
120 + EOF
121 + git log --format=%s -- ":(glob)**/bar" >actual &&
122 + test_cmp expect actual
123 +'
124 +
125 +test_expect_success '**/ does not work with --noglob-pathspecs' '
126 + : >expect &&
127 + git --noglob-pathspecs log --format=%s -- "**/bar" >actual &&
128 + test_cmp expect actual
129 +'
130 +
131 +test_expect_success '**/ works with :(glob) and --noglob-pathspecs' '
132 + cat >expect <<-\EOF &&
133 + xxx
134 + unrelated
135 + EOF
136 + git --noglob-pathspecs log --format=%s -- ":(glob)**/bar" >actual &&
137 + test_cmp expect actual
138 +'
139 +
140 +test_expect_success '**/ works with --glob-pathspecs' '
141 + cat >expect <<-\EOF &&
142 + xxx
143 + unrelated
144 + EOF
145 + git --glob-pathspecs log --format=%s -- "**/bar" >actual &&
146 + test_cmp expect actual
147 +'
148 +
149 +test_expect_success '**/ does not work with :(literal) and --glob-pathspecs' '
150 + : >expect &&
151 + git --glob-pathspecs log --format=%s -- ":(literal)**/bar" >actual &&
152 + test_cmp expect actual
153 +'
154 +
155 test_done
tree-walk.c
+4 -5
@@ -639,7 +639,8 @@ enum interesting tree_entry_interesting(const struct name_entry *entry,
639 GUARD_PATHSPEC(ps,
640 PATHSPEC_FROMTOP |
641 PATHSPEC_MAXDEPTH |
642 - PATHSPEC_LITERAL);
642 + PATHSPEC_LITERAL |
643 + PATHSPEC_GLOB);
644
645 if (!ps->nr) {
646 if (!ps->recursive ||
@@ -685,8 +686,7 @@ enum interesting tree_entry_interesting(const struct name_entry *entry,
686 return entry_interesting;
687
688 if (item->nowildcard_len < item->len) {
688 - if (!git_fnmatch(match + baselen, entry->path,
689 - item->flags & PATHSPEC_ONESTAR ? GFNM_ONESTAR : 0,
689 + if (!git_fnmatch(item, match + baselen, entry->path,
690 item->nowildcard_len - baselen))
691 return entry_interesting;
692
@@ -727,8 +727,7 @@ match_wildcards:
727
728 strbuf_add(base, entry->path, pathlen);
729
730 - if (!git_fnmatch(match, base->buf + base_offset,
731 - item->flags & PATHSPEC_ONESTAR ? GFNM_ONESTAR : 0,
730 + if (!git_fnmatch(item, match, base->buf + base_offset,
731 item->nowildcard_len)) {
732 strbuf_setlen(base, base_offset + baselen);
733 return entry_interesting;