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>

Jeff King committed Dec 19, 2012 at 17:37 UTC 823ab40fd4adc5bf5b5c66b6d189deae34d2a138
5 files changed +107 -6
Documentation/git.txt
+15
@@ -422,6 +422,11 @@ help ...`.
422 Do not use replacement refs to replace git objects. See
423 linkgit:git-replace[1] for more information.
424
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 +
430
431 GIT COMMANDS
432 ------------
@@ -790,6 +795,16 @@ for further details.
795 as a file path and will try to write the trace messages
796 into it.
797
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 +
808 Discussion[[Discussion]]
809 ------------------------
810
cache.h
+3
@@ -362,6 +362,7 @@ static inline enum object_type object_type(unsigned int mode)
362 #define GIT_NOTES_DISPLAY_REF_ENVIRONMENT "GIT_NOTES_DISPLAY_REF"
363 #define GIT_NOTES_REWRITE_REF_ENVIRONMENT "GIT_NOTES_REWRITE_REF"
364 #define GIT_NOTES_REWRITE_MODE_ENVIRONMENT "GIT_NOTES_REWRITE_MODE"
365 +#define GIT_LITERAL_PATHSPECS_ENVIRONMENT "GIT_LITERAL_PATHSPECS"
366
367 /*
368 * Repository-local GIT_* environment variables
@@ -490,6 +491,8 @@ extern int init_pathspec(struct pathspec *, const char **);
491 extern void free_pathspec(struct pathspec *);
492 extern int ce_path_match(const struct cache_entry *ce, const struct pathspec *pathspec);
493
494 +extern int limit_pathspec_to_literal(void);
495 +
496 #define HASH_WRITE_OBJECT 1
497 #define HASH_FORMAT_CHECK 2
498 extern int index_fd(unsigned char *sha1, int fd, struct stat *st, enum object_type type, const char *path, unsigned flags);
dir.c
+19 -6
@@ -38,6 +38,7 @@ static size_t common_prefix_len(const char **pathspec)
38 {
39 const char *n, *first;
40 size_t max = 0;
41 + int literal = limit_pathspec_to_literal();
42
43 if (!pathspec)
44 return max;
@@ -47,7 +48,7 @@ static size_t common_prefix_len(const char **pathspec)
48 size_t i, len = 0;
49 for (i = 0; first == n || i < max; i++) {
50 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)))
52 break;
53 if (c == '/')
54 len = i + 1;
@@ -117,6 +118,7 @@ int within_depth(const char *name, int namelen,
118 static int match_one(const char *match, const char *name, int namelen)
119 {
120 int matchlen;
121 + int literal = limit_pathspec_to_literal();
122
123 /* If the match was just the prefix, we matched */
124 if (!*match)
@@ -126,7 +128,7 @@ static int match_one(const char *match, const char *name, int namelen)
128 for (;;) {
129 unsigned char c1 = tolower(*match);
130 unsigned char c2 = tolower(*name);
129 - if (c1 == '\0' || is_glob_special(c1))
131 + if (c1 == '\0' || (!literal && is_glob_special(c1)))
132 break;
133 if (c1 != c2)
134 return 0;
@@ -138,7 +140,7 @@ static int match_one(const char *match, const char *name, int namelen)
140 for (;;) {
141 unsigned char c1 = *match;
142 unsigned char c2 = *name;
141 - if (c1 == '\0' || is_glob_special(c1))
143 + if (c1 == '\0' || (!literal && is_glob_special(c1)))
144 break;
145 if (c1 != c2)
146 return 0;
@@ -148,14 +150,16 @@ static int match_one(const char *match, const char *name, int namelen)
150 }
151 }
152
151 -
153 /*
154 * If we don't match the matchstring exactly,
155 * we need to match by fnmatch
156 */
157 matchlen = strlen(match);
157 - if (strncmp_icase(match, name, matchlen))
158 + if (strncmp_icase(match, name, matchlen)) {
159 + if (literal)
160 + return 0;
161 return !fnmatch_icase(match, name, 0) ? MATCHED_FNMATCH : 0;
162 + }
163
164 if (namelen == matchlen)
165 return MATCHED_EXACTLY;
@@ -1429,7 +1433,8 @@ int init_pathspec(struct pathspec *pathspec, const char **paths)
1433
1434 item->match = path;
1435 item->len = strlen(path);
1432 - item->use_wildcard = !no_wildcard(path);
1436 + item->use_wildcard = !limit_pathspec_to_literal() &&
1437 + !no_wildcard(path);
1438 if (item->use_wildcard)
1439 pathspec->has_wildcard = 1;
1440 }
@@ -1445,3 +1450,11 @@ void free_pathspec(struct pathspec *pathspec)
1450 free(pathspec->items);
1451 pathspec->items = NULL;
1452 }
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
+8
@@ -135,6 +135,14 @@ static int handle_options(const char ***argv, int *argc, int *envchanged)
135 git_config_push_parameter((*argv)[1]);
136 (*argv)++;
137 (*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;
146 } else {
147 fprintf(stderr, "Unknown option: %s\n", cmd);
148 usage(git_usage_string);
t/t6130-pathspec-noglob.sh new
+62
@@ -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