add global --literal-pathspecs option
authorJeff King <peff@peff.net>
Wed, 19 Dec 2012 22:37:30 +0000 (17:37 -0500)
committerJunio C Hamano <gitster@pobox.com>
Wed, 19 Dec 2012 22:58:59 +0000 (14:58 -0800)
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>
Documentation/git.txt
cache.h
dir.c
git.c
t/t6130-pathspec-noglob.sh [new file with mode: 0755]

index 1d797f27ed7682cf7308949b99d4bc27f9767f1a..8d869db68281883161d6694ff87b04952cd403e1 100644 (file)
@@ -422,6 +422,11 @@ help ...`.
        Do not use replacement refs to replace git objects. See
        linkgit:git-replace[1] for more information.
 
+--literal-pathspecs::
+       Treat pathspecs literally, rather than as glob patterns. This is
+       equivalent to setting the `GIT_LITERAL_PATHSPECS` environment
+       variable to `1`.
+
 
 GIT COMMANDS
 ------------
@@ -790,6 +795,16 @@ for further details.
        as a file path and will try to write the trace messages
        into it.
 
+GIT_LITERAL_PATHSPECS::
+       Setting this variable to `1` will cause git to treat all
+       pathspecs literally, rather than as glob patterns. For example,
+       running `GIT_LITERAL_PATHSPECS=1 git log -- '*.c'` will search
+       for commits that touch the path `*.c`, not any paths that the
+       glob `*.c` matches. You might want this if you are feeding
+       literal paths to git (e.g., paths previously given to you by
+       `git ls-tree`, `--raw` diff output, etc).
+
+
 Discussion[[Discussion]]
 ------------------------
 
diff --git a/cache.h b/cache.h
index 18fdd18f3674b1d7c62de65509bdb7bc57743395..95608d9dc5ef3af8f19a33155cc7f8b5d3928a56 100644 (file)
--- a/cache.h
+++ b/cache.h
@@ -362,6 +362,7 @@ static inline enum object_type object_type(unsigned int mode)
 #define GIT_NOTES_DISPLAY_REF_ENVIRONMENT "GIT_NOTES_DISPLAY_REF"
 #define GIT_NOTES_REWRITE_REF_ENVIRONMENT "GIT_NOTES_REWRITE_REF"
 #define GIT_NOTES_REWRITE_MODE_ENVIRONMENT "GIT_NOTES_REWRITE_MODE"
+#define GIT_LITERAL_PATHSPECS_ENVIRONMENT "GIT_LITERAL_PATHSPECS"
 
 /*
  * Repository-local GIT_* environment variables
@@ -490,6 +491,8 @@ extern int init_pathspec(struct pathspec *, const char **);
 extern void free_pathspec(struct pathspec *);
 extern int ce_path_match(const struct cache_entry *ce, const struct pathspec *pathspec);
 
+extern int limit_pathspec_to_literal(void);
+
 #define HASH_WRITE_OBJECT 1
 #define HASH_FORMAT_CHECK 2
 extern int index_fd(unsigned char *sha1, int fd, struct stat *st, enum object_type type, const char *path, unsigned flags);
diff --git a/dir.c b/dir.c
index 5a83aa7897f270279c403778f43aea6db1efc5af..03ff36bc6eb24d716fc3fa49ff745575fa79683b 100644 (file)
--- a/dir.c
+++ b/dir.c
@@ -38,6 +38,7 @@ static size_t common_prefix_len(const char **pathspec)
 {
        const char *n, *first;
        size_t max = 0;
+       int literal = limit_pathspec_to_literal();
 
        if (!pathspec)
                return max;
@@ -47,7 +48,7 @@ static size_t common_prefix_len(const char **pathspec)
                size_t i, len = 0;
                for (i = 0; first == n || i < max; i++) {
                        char c = n[i];
-                       if (!c || c != first[i] || is_glob_special(c))
+                       if (!c || c != first[i] || (!literal && is_glob_special(c)))
                                break;
                        if (c == '/')
                                len = i + 1;
@@ -117,6 +118,7 @@ int within_depth(const char *name, int namelen,
 static int match_one(const char *match, const char *name, int namelen)
 {
        int matchlen;
+       int literal = limit_pathspec_to_literal();
 
        /* If the match was just the prefix, we matched */
        if (!*match)
@@ -126,7 +128,7 @@ static int match_one(const char *match, const char *name, int namelen)
                for (;;) {
                        unsigned char c1 = tolower(*match);
                        unsigned char c2 = tolower(*name);
-                       if (c1 == '\0' || is_glob_special(c1))
+                       if (c1 == '\0' || (!literal && is_glob_special(c1)))
                                break;
                        if (c1 != c2)
                                return 0;
@@ -138,7 +140,7 @@ static int match_one(const char *match, const char *name, int namelen)
                for (;;) {
                        unsigned char c1 = *match;
                        unsigned char c2 = *name;
-                       if (c1 == '\0' || is_glob_special(c1))
+                       if (c1 == '\0' || (!literal && is_glob_special(c1)))
                                break;
                        if (c1 != c2)
                                return 0;
@@ -148,14 +150,16 @@ static int match_one(const char *match, const char *name, int namelen)
                }
        }
 
-
        /*
         * If we don't match the matchstring exactly,
         * we need to match by fnmatch
         */
        matchlen = strlen(match);
-       if (strncmp_icase(match, name, matchlen))
+       if (strncmp_icase(match, name, matchlen)) {
+               if (literal)
+                       return 0;
                return !fnmatch_icase(match, name, 0) ? MATCHED_FNMATCH : 0;
+       }
 
        if (namelen == matchlen)
                return MATCHED_EXACTLY;
@@ -1429,7 +1433,8 @@ int init_pathspec(struct pathspec *pathspec, const char **paths)
 
                item->match = path;
                item->len = strlen(path);
-               item->use_wildcard = !no_wildcard(path);
+               item->use_wildcard = !limit_pathspec_to_literal() &&
+                                    !no_wildcard(path);
                if (item->use_wildcard)
                        pathspec->has_wildcard = 1;
        }
@@ -1445,3 +1450,11 @@ void free_pathspec(struct pathspec *pathspec)
        free(pathspec->items);
        pathspec->items = NULL;
 }
+
+int limit_pathspec_to_literal(void)
+{
+       static int flag = -1;
+       if (flag < 0)
+               flag = git_env_bool(GIT_LITERAL_PATHSPECS_ENVIRONMENT, 0);
+       return flag;
+}
diff --git a/git.c b/git.c
index d33f9b32a2895ef9f062f9fda487588daac6f5bc..ed66c660d15010af9a8b0c2ea1da8839e316e1db 100644 (file)
--- a/git.c
+++ b/git.c
@@ -135,6 +135,14 @@ static int handle_options(const char ***argv, int *argc, int *envchanged)
                        git_config_push_parameter((*argv)[1]);
                        (*argv)++;
                        (*argc)--;
+               } else if (!strcmp(cmd, "--literal-pathspecs")) {
+                       setenv(GIT_LITERAL_PATHSPECS_ENVIRONMENT, "1", 1);
+                       if (envchanged)
+                               *envchanged = 1;
+               } else if (!strcmp(cmd, "--no-literal-pathspecs")) {
+                       setenv(GIT_LITERAL_PATHSPECS_ENVIRONMENT, "0", 1);
+                       if (envchanged)
+                               *envchanged = 1;
                } else {
                        fprintf(stderr, "Unknown option: %s\n", cmd);
                        usage(git_usage_string);
diff --git a/t/t6130-pathspec-noglob.sh b/t/t6130-pathspec-noglob.sh
new file mode 100755 (executable)
index 0000000..bb5e710
--- /dev/null
@@ -0,0 +1,62 @@
+#!/bin/sh
+
+test_description='test globbing (and noglob) of pathspec limiting'
+. ./test-lib.sh
+
+test_expect_success 'create commits with glob characters' '
+       test_commit unrelated bar &&
+       test_commit vanilla foo &&
+       test_commit star "f*" &&
+       test_commit bracket "f[o][o]"
+'
+
+test_expect_success 'vanilla pathspec matches literally' '
+       echo vanilla >expect &&
+       git log --format=%s -- foo >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'star pathspec globs' '
+       cat >expect <<-\EOF &&
+       bracket
+       star
+       vanilla
+       EOF
+       git log --format=%s -- "f*" >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'bracket pathspec globs and matches literal brackets' '
+       cat >expect <<-\EOF &&
+       bracket
+       vanilla
+       EOF
+       git log --format=%s -- "f[o][o]" >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'no-glob option matches literally (vanilla)' '
+       echo vanilla >expect &&
+       git --literal-pathspecs log --format=%s -- foo >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'no-glob option matches literally (star)' '
+       echo star >expect &&
+       git --literal-pathspecs log --format=%s -- "f*" >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'no-glob option matches literally (bracket)' '
+       echo bracket >expect &&
+       git --literal-pathspecs log --format=%s -- "f[o][o]" >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'no-glob environment variable works' '
+       echo star >expect &&
+       GIT_LITERAL_PATHSPECS=1 git log --format=%s -- "f*" >actual &&
+       test_cmp expect actual
+'
+
+test_done