Skip to content

Commit

Permalink
add global --literal-pathspecs option
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
peff authored and gitster committed Dec 19, 2012
1 parent 18499ba commit 823ab40
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 6 deletions.
15 changes: 15 additions & 0 deletions Documentation/git.txt
Expand Up @@ -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
------------
Expand Down Expand Up @@ -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]]
------------------------

Expand Down
3 changes: 3 additions & 0 deletions cache.h
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
25 changes: 19 additions & 6 deletions dir.c
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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)
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
8 changes: 8 additions & 0 deletions git.c
Expand Up @@ -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);
Expand Down
62 changes: 62 additions & 0 deletions t/t6130-pathspec-noglob.sh
@@ -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

0 comments on commit 823ab40

Please sign in to comment.