Skip to content

Commit bd30c2e

Browse files
pcloudsgitster
authored andcommitted
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 <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent a16bf9d commit bd30c2e

File tree

12 files changed

+198
-31
lines changed

12 files changed

+198
-31
lines changed

Documentation/git.txt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,17 @@ help ...`.
454454
This is equivalent to setting the `GIT_LITERAL_PATHSPECS` environment
455455
variable to `1`.
456456

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)"
457468

458469
GIT COMMANDS
459470
------------
@@ -860,6 +871,14 @@ GIT_LITERAL_PATHSPECS::
860871
literal paths to Git (e.g., paths previously given to you by
861872
`git ls-tree`, `--raw` diff output, etc).
862873

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

864883
Discussion[[Discussion]]
865884
------------------------

Documentation/glossary-content.txt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,35 @@ top `/`;;
333333
literal;;
334334
Wildcards in the pattern such as `*` or `?` are treated
335335
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.
336365
--
337366
+
338367
Currently only the slash `/` is recognized as the "magic signature",

builtin/add.c

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -541,11 +541,16 @@ int cmd_add(int argc, const char **argv, const char *prefix)
541541
/*
542542
* file_exists() assumes exact match
543543
*/
544-
GUARD_PATHSPEC(&pathspec, PATHSPEC_FROMTOP | PATHSPEC_LITERAL);
544+
GUARD_PATHSPEC(&pathspec,
545+
PATHSPEC_FROMTOP |
546+
PATHSPEC_LITERAL |
547+
PATHSPEC_GLOB);
545548

546549
for (i = 0; i < pathspec.nr; i++) {
547550
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))) {
549554
if (ignore_missing) {
550555
int dtype = DT_UNKNOWN;
551556
if (is_excluded(&dir, path, &dtype))

builtin/ls-tree.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ int cmd_ls_tree(int argc, const char **argv, const char *prefix)
173173
* cannot be lifted until it is converted to use
174174
* match_pathspec_depth() or tree_entry_interesting()
175175
*/
176-
parse_pathspec(&pathspec, 0,
176+
parse_pathspec(&pathspec, PATHSPEC_GLOB,
177177
PATHSPEC_PREFER_CWD,
178178
prefix, argv + 1);
179179
for (i = 0; i < pathspec.nr; i++)

cache.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,8 @@ static inline enum object_type object_type(unsigned int mode)
367367
#define GIT_NOTES_REWRITE_REF_ENVIRONMENT "GIT_NOTES_REWRITE_REF"
368368
#define GIT_NOTES_REWRITE_MODE_ENVIRONMENT "GIT_NOTES_REWRITE_MODE"
369369
#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"
370372

371373
/*
372374
* This environment variable is expected to contain a boolean indicating

dir.c

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,26 +52,28 @@ int fnmatch_icase(const char *pattern, const char *string, int flags)
5252
return fnmatch(pattern, string, flags | (ignore_case ? FNM_CASEFOLD : 0));
5353
}
5454

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)
5758
{
58-
int fnm_flags = 0;
59-
if (flags & GFNM_PATHNAME)
60-
fnm_flags |= FNM_PATHNAME;
6159
if (prefix > 0) {
6260
if (strncmp(pattern, string, prefix))
6361
return FNM_NOMATCH;
6462
pattern += prefix;
6563
string += prefix;
6664
}
67-
if (flags & GFNM_ONESTAR) {
65+
if (item->flags & PATHSPEC_ONESTAR) {
6866
int pattern_len = strlen(++pattern);
6967
int string_len = strlen(string);
7068
return string_len < pattern_len ||
7169
strcmp(pattern,
7270
string + string_len - pattern_len);
7371
}
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);
7577
}
7678

7779
static int fnmatch_icase_mem(const char *pattern, int patternlen,
@@ -111,7 +113,8 @@ static size_t common_prefix_len(const struct pathspec *pathspec)
111113
GUARD_PATHSPEC(pathspec,
112114
PATHSPEC_FROMTOP |
113115
PATHSPEC_MAXDEPTH |
114-
PATHSPEC_LITERAL);
116+
PATHSPEC_LITERAL |
117+
PATHSPEC_GLOB);
115118

116119
for (n = 0; n < pathspec->nr; n++) {
117120
size_t i = 0, len = 0;
@@ -206,8 +209,7 @@ static int match_pathspec_item(const struct pathspec_item *item, int prefix,
206209
}
207210

208211
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,
211213
item->nowildcard_len - prefix))
212214
return MATCHED_FNMATCH;
213215

@@ -238,7 +240,8 @@ int match_pathspec_depth(const struct pathspec *ps,
238240
GUARD_PATHSPEC(ps,
239241
PATHSPEC_FROMTOP |
240242
PATHSPEC_MAXDEPTH |
241-
PATHSPEC_LITERAL);
243+
PATHSPEC_LITERAL |
244+
PATHSPEC_GLOB);
242245

243246
if (!ps->nr) {
244247
if (!ps->recursive ||
@@ -1297,7 +1300,8 @@ int read_directory(struct dir_struct *dir, const char *path, int len, const stru
12971300
GUARD_PATHSPEC(pathspec,
12981301
PATHSPEC_FROMTOP |
12991302
PATHSPEC_MAXDEPTH |
1300-
PATHSPEC_LITERAL);
1303+
PATHSPEC_LITERAL |
1304+
PATHSPEC_GLOB);
13011305

13021306
if (has_symlink_leading_path(path, len))
13031307
return dir->nr;

dir.h

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -199,10 +199,9 @@ extern int fnmatch_icase(const char *pattern, const char *string, int flags);
199199
/*
200200
* The prefix part of pattern must not contains wildcards.
201201
*/
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);
207206

208207
#endif

git.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,14 @@ static int handle_options(const char ***argv, int *argc, int *envchanged)
147147
setenv(GIT_LITERAL_PATHSPECS_ENVIRONMENT, "0", 1);
148148
if (envchanged)
149149
*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;
150158
} else if (!strcmp(cmd, "--shallow-file")) {
151159
(*argv)++;
152160
(*argc)--;

pathspec.c

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ char *find_pathspecs_matching_against_index(const struct pathspec *pathspec)
5757
*
5858
* Possible future magic semantics include stuff like:
5959
*
60-
* { PATHSPEC_NOGLOB, '!', "noglob" },
6160
* { PATHSPEC_ICASE, '\0', "icase" },
6261
* { PATHSPEC_RECURSIVE, '*', "recursive" },
6362
* { PATHSPEC_REGEXP, '\0', "regexp" },
@@ -71,6 +70,7 @@ static struct pathspec_magic {
7170
} pathspec_magic[] = {
7271
{ PATHSPEC_FROMTOP, '/', "top" },
7372
{ PATHSPEC_LITERAL, 0, "literal" },
73+
{ PATHSPEC_GLOB, '\0', "glob" },
7474
};
7575

7676
/*
@@ -93,6 +93,8 @@ static unsigned prefix_pathspec(struct pathspec_item *item,
9393
const char *elt)
9494
{
9595
static int literal_global = -1;
96+
static int glob_global = -1;
97+
static int noglob_global = -1;
9698
unsigned magic = 0, short_magic = 0, global_magic = 0;
9799
const char *copyfrom = elt, *long_magic_end = NULL;
98100
char *match;
@@ -103,6 +105,22 @@ static unsigned prefix_pathspec(struct pathspec_item *item,
103105
if (literal_global)
104106
global_magic |= PATHSPEC_LITERAL;
105107

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+
106124
if (elt[0] != ':' || literal_global) {
107125
; /* nothing to do */
108126
} else if (elt[1] == '(') {
@@ -167,12 +185,24 @@ static unsigned prefix_pathspec(struct pathspec_item *item,
167185

168186
magic |= short_magic;
169187
*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+
170197
magic |= global_magic;
171198

172199
if (pathspec_prefix >= 0 &&
173200
(prefixlen || (prefix && *prefix)))
174201
die("BUG: 'prefix' magic is supposed to be used at worktree's root");
175202

203+
if ((magic & PATHSPEC_LITERAL) && (magic & PATHSPEC_GLOB))
204+
die(_("%s: 'literal' and 'glob' are incompatible"), elt);
205+
176206
if (pathspec_prefix >= 0) {
177207
match = xstrdup(copyfrom);
178208
prefixlen = pathspec_prefix;
@@ -248,10 +278,17 @@ static unsigned prefix_pathspec(struct pathspec_item *item,
248278
item->nowildcard_len = prefixlen;
249279
}
250280
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+
}
255292

256293
/* sanity checks, pathspec matchers assume these are sane */
257294
assert(item->nowildcard_len <= item->len &&

pathspec.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
#define PATHSPEC_FROMTOP (1<<0)
66
#define PATHSPEC_MAXDEPTH (1<<1)
77
#define PATHSPEC_LITERAL (1<<2)
8+
#define PATHSPEC_GLOB (1<<3)
89
#define PATHSPEC_ALL_MAGIC \
910
(PATHSPEC_FROMTOP | \
1011
PATHSPEC_MAXDEPTH | \
11-
PATHSPEC_LITERAL)
12+
PATHSPEC_LITERAL | \
13+
PATHSPEC_GLOB)
1214

1315
#define PATHSPEC_ONESTAR 1 /* the pathspec pattern sastisfies GFNM_ONESTAR */
1416

0 commit comments

Comments
 (0)