Skip to content

Commit 823ab40

Browse files
peffgitster
authored andcommitted
add global --literal-pathspecs option
Git takes pathspec arguments in many places to limit the scope of an operation. These pathspecs are treated not as literal paths, but as glob patterns that can be fed to fnmatch. When a user is giving a specific pattern, this is a nice feature. However, when programatically providing pathspecs, it can be a nuisance. For example, to find the latest revision which modified "$foo", one can use "git rev-list -- $foo". But if "$foo" contains glob characters (e.g., "f*"), it will erroneously match more entries than desired. The caller needs to quote the characters in $foo, and even then, the results may not be exactly the same as with a literal pathspec. For instance, the depth checks in match_pathspec_depth do not kick in if we match via fnmatch. This patch introduces a global command-line option (i.e., one for "git" itself, not for specific commands) to turn this behavior off. It also has a matching environment variable, which can make it easier if you are a script or porcelain interface that is going to issue many such commands. This option cannot turn off globbing for particular pathspecs. That could eventually be done with a ":(noglob)" magic pathspec prefix. However, that level of granularity is more cumbersome to use for many cases, and doing ":(noglob)" right would mean converting the whole codebase to use "struct pathspec", as the usual "const char **pathspec" cannot represent extra per-item flags. Signed-off-by: Jeff King <peff@peff.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent 18499ba commit 823ab40

5 files changed

Lines changed: 107 additions & 6 deletions

File tree

Documentation/git.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,11 @@ help ...`.
422422
Do not use replacement refs to replace git objects. See
423423
linkgit:git-replace[1] for more information.
424424

425+
--literal-pathspecs::
426+
Treat pathspecs literally, rather than as glob patterns. This is
427+
equivalent to setting the `GIT_LITERAL_PATHSPECS` environment
428+
variable to `1`.
429+
425430

426431
GIT COMMANDS
427432
------------
@@ -790,6 +795,16 @@ for further details.
790795
as a file path and will try to write the trace messages
791796
into it.
792797

798+
GIT_LITERAL_PATHSPECS::
799+
Setting this variable to `1` will cause git to treat all
800+
pathspecs literally, rather than as glob patterns. For example,
801+
running `GIT_LITERAL_PATHSPECS=1 git log -- '*.c'` will search
802+
for commits that touch the path `*.c`, not any paths that the
803+
glob `*.c` matches. You might want this if you are feeding
804+
literal paths to git (e.g., paths previously given to you by
805+
`git ls-tree`, `--raw` diff output, etc).
806+
807+
793808
Discussion[[Discussion]]
794809
------------------------
795810

cache.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@ static inline enum object_type object_type(unsigned int mode)
362362
#define GIT_NOTES_DISPLAY_REF_ENVIRONMENT "GIT_NOTES_DISPLAY_REF"
363363
#define GIT_NOTES_REWRITE_REF_ENVIRONMENT "GIT_NOTES_REWRITE_REF"
364364
#define GIT_NOTES_REWRITE_MODE_ENVIRONMENT "GIT_NOTES_REWRITE_MODE"
365+
#define GIT_LITERAL_PATHSPECS_ENVIRONMENT "GIT_LITERAL_PATHSPECS"
365366

366367
/*
367368
* Repository-local GIT_* environment variables
@@ -490,6 +491,8 @@ extern int init_pathspec(struct pathspec *, const char **);
490491
extern void free_pathspec(struct pathspec *);
491492
extern int ce_path_match(const struct cache_entry *ce, const struct pathspec *pathspec);
492493

494+
extern int limit_pathspec_to_literal(void);
495+
493496
#define HASH_WRITE_OBJECT 1
494497
#define HASH_FORMAT_CHECK 2
495498
extern int index_fd(unsigned char *sha1, int fd, struct stat *st, enum object_type type, const char *path, unsigned flags);

dir.c

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ static size_t common_prefix_len(const char **pathspec)
3838
{
3939
const char *n, *first;
4040
size_t max = 0;
41+
int literal = limit_pathspec_to_literal();
4142

4243
if (!pathspec)
4344
return max;
@@ -47,7 +48,7 @@ static size_t common_prefix_len(const char **pathspec)
4748
size_t i, len = 0;
4849
for (i = 0; first == n || i < max; i++) {
4950
char c = n[i];
50-
if (!c || c != first[i] || is_glob_special(c))
51+
if (!c || c != first[i] || (!literal && is_glob_special(c)))
5152
break;
5253
if (c == '/')
5354
len = i + 1;
@@ -117,6 +118,7 @@ int within_depth(const char *name, int namelen,
117118
static int match_one(const char *match, const char *name, int namelen)
118119
{
119120
int matchlen;
121+
int literal = limit_pathspec_to_literal();
120122

121123
/* If the match was just the prefix, we matched */
122124
if (!*match)
@@ -126,7 +128,7 @@ static int match_one(const char *match, const char *name, int namelen)
126128
for (;;) {
127129
unsigned char c1 = tolower(*match);
128130
unsigned char c2 = tolower(*name);
129-
if (c1 == '\0' || is_glob_special(c1))
131+
if (c1 == '\0' || (!literal && is_glob_special(c1)))
130132
break;
131133
if (c1 != c2)
132134
return 0;
@@ -138,7 +140,7 @@ static int match_one(const char *match, const char *name, int namelen)
138140
for (;;) {
139141
unsigned char c1 = *match;
140142
unsigned char c2 = *name;
141-
if (c1 == '\0' || is_glob_special(c1))
143+
if (c1 == '\0' || (!literal && is_glob_special(c1)))
142144
break;
143145
if (c1 != c2)
144146
return 0;
@@ -148,14 +150,16 @@ static int match_one(const char *match, const char *name, int namelen)
148150
}
149151
}
150152

151-
152153
/*
153154
* If we don't match the matchstring exactly,
154155
* we need to match by fnmatch
155156
*/
156157
matchlen = strlen(match);
157-
if (strncmp_icase(match, name, matchlen))
158+
if (strncmp_icase(match, name, matchlen)) {
159+
if (literal)
160+
return 0;
158161
return !fnmatch_icase(match, name, 0) ? MATCHED_FNMATCH : 0;
162+
}
159163

160164
if (namelen == matchlen)
161165
return MATCHED_EXACTLY;
@@ -1429,7 +1433,8 @@ int init_pathspec(struct pathspec *pathspec, const char **paths)
14291433

14301434
item->match = path;
14311435
item->len = strlen(path);
1432-
item->use_wildcard = !no_wildcard(path);
1436+
item->use_wildcard = !limit_pathspec_to_literal() &&
1437+
!no_wildcard(path);
14331438
if (item->use_wildcard)
14341439
pathspec->has_wildcard = 1;
14351440
}
@@ -1445,3 +1450,11 @@ void free_pathspec(struct pathspec *pathspec)
14451450
free(pathspec->items);
14461451
pathspec->items = NULL;
14471452
}
1453+
1454+
int limit_pathspec_to_literal(void)
1455+
{
1456+
static int flag = -1;
1457+
if (flag < 0)
1458+
flag = git_env_bool(GIT_LITERAL_PATHSPECS_ENVIRONMENT, 0);
1459+
return flag;
1460+
}

git.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,14 @@ static int handle_options(const char ***argv, int *argc, int *envchanged)
135135
git_config_push_parameter((*argv)[1]);
136136
(*argv)++;
137137
(*argc)--;
138+
} else if (!strcmp(cmd, "--literal-pathspecs")) {
139+
setenv(GIT_LITERAL_PATHSPECS_ENVIRONMENT, "1", 1);
140+
if (envchanged)
141+
*envchanged = 1;
142+
} else if (!strcmp(cmd, "--no-literal-pathspecs")) {
143+
setenv(GIT_LITERAL_PATHSPECS_ENVIRONMENT, "0", 1);
144+
if (envchanged)
145+
*envchanged = 1;
138146
} else {
139147
fprintf(stderr, "Unknown option: %s\n", cmd);
140148
usage(git_usage_string);

t/t6130-pathspec-noglob.sh

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/bin/sh
2+
3+
test_description='test globbing (and noglob) of pathspec limiting'
4+
. ./test-lib.sh
5+
6+
test_expect_success 'create commits with glob characters' '
7+
test_commit unrelated bar &&
8+
test_commit vanilla foo &&
9+
test_commit star "f*" &&
10+
test_commit bracket "f[o][o]"
11+
'
12+
13+
test_expect_success 'vanilla pathspec matches literally' '
14+
echo vanilla >expect &&
15+
git log --format=%s -- foo >actual &&
16+
test_cmp expect actual
17+
'
18+
19+
test_expect_success 'star pathspec globs' '
20+
cat >expect <<-\EOF &&
21+
bracket
22+
star
23+
vanilla
24+
EOF
25+
git log --format=%s -- "f*" >actual &&
26+
test_cmp expect actual
27+
'
28+
29+
test_expect_success 'bracket pathspec globs and matches literal brackets' '
30+
cat >expect <<-\EOF &&
31+
bracket
32+
vanilla
33+
EOF
34+
git log --format=%s -- "f[o][o]" >actual &&
35+
test_cmp expect actual
36+
'
37+
38+
test_expect_success 'no-glob option matches literally (vanilla)' '
39+
echo vanilla >expect &&
40+
git --literal-pathspecs log --format=%s -- foo >actual &&
41+
test_cmp expect actual
42+
'
43+
44+
test_expect_success 'no-glob option matches literally (star)' '
45+
echo star >expect &&
46+
git --literal-pathspecs log --format=%s -- "f*" >actual &&
47+
test_cmp expect actual
48+
'
49+
50+
test_expect_success 'no-glob option matches literally (bracket)' '
51+
echo bracket >expect &&
52+
git --literal-pathspecs log --format=%s -- "f[o][o]" >actual &&
53+
test_cmp expect actual
54+
'
55+
56+
test_expect_success 'no-glob environment variable works' '
57+
echo star >expect &&
58+
GIT_LITERAL_PATHSPECS=1 git log --format=%s -- "f*" >actual &&
59+
test_cmp expect actual
60+
'
61+
62+
test_done

0 commit comments

Comments
 (0)