parse_pathspec: accept :(icase)path syntax

Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Nguyễn Thái Ngọc Duy committed Jul 14, 2013 at 15:36 UTC 93d935371654faf2956a4c37c1ca46f3195ee832
11 files changed +257 -28
Documentation/git.txt
+8
@@ -466,6 +466,10 @@ help ...`.
466 globbing on individual pathspecs can be done using pathspec
467 magic ":(glob)"
468
469 +--icase-pathspecs:
470 + Add "icase" magic to all pathspec. This is equivalent to setting
471 + the `GIT_ICASE_PATHSPECS` environment variable to `1`.
472 +
473 GIT COMMANDS
474 ------------
475
@@ -879,6 +883,10 @@ GIT_NOGLOB_PATHSPECS::
883 Setting this variable to `1` will cause Git to treat all
884 pathspecs as literal (aka "literal" magic).
885
886 +GIT_ICASE_PATHSPECS::
887 + Setting this variable to `1` will cause Git to treat all
888 + pathspecs as case-insensitive.
889 +
890
891 Discussion[[Discussion]]
892 ------------------------
Documentation/glossary-content.txt
+3
@@ -334,6 +334,9 @@ literal;;
334 Wildcards in the pattern such as `*` or `?` are treated
335 as literal characters.
336
337 +icase;;
338 + Case insensitive match.
339 +
340 glob;;
341 Git treats the pattern as a shell glob suitable for
342 consumption by fnmatch(3) with the FNM_PATHNAME flag:
builtin/add.c
+4 -2
@@ -544,12 +544,14 @@ int cmd_add(int argc, const char **argv, const char *prefix)
544 GUARD_PATHSPEC(&pathspec,
545 PATHSPEC_FROMTOP |
546 PATHSPEC_LITERAL |
547 - PATHSPEC_GLOB);
547 + PATHSPEC_GLOB |
548 + PATHSPEC_ICASE);
549
550 for (i = 0; i < pathspec.nr; i++) {
551 const char *path = pathspec.items[i].match;
552 if (!seen[i] &&
552 - ((pathspec.items[i].magic & PATHSPEC_GLOB) ||
553 + ((pathspec.items[i].magic &
554 + (PATHSPEC_GLOB | PATHSPEC_ICASE)) ||
555 !file_exists(path))) {
556 if (ignore_missing) {
557 int dtype = DT_UNKNOWN;
builtin/ls-tree.c
+1 -1
@@ -173,7 +173,7 @@ int cmd_ls_tree(int argc, const char **argv, const char *prefix)
173 * cannot be lifted until it is converted to use
174 * match_pathspec_depth() or tree_entry_interesting()
175 */
176 - parse_pathspec(&pathspec, PATHSPEC_GLOB,
176 + parse_pathspec(&pathspec, PATHSPEC_GLOB | PATHSPEC_ICASE,
177 PATHSPEC_PREFER_CWD,
178 prefix, argv + 1);
179 for (i = 0; i < pathspec.nr; i++)
cache.h
+1
@@ -369,6 +369,7 @@ static inline enum object_type object_type(unsigned int mode)
369 #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"
372 +#define GIT_ICASE_PATHSPECS_ENVIRONMENT "GIT_ICASE_PATHSPECS"
373
374 /*
375 * This environment variable is expected to contain a boolean indicating
dir.c
+62 -12
@@ -57,7 +57,7 @@ inline int git_fnmatch(const struct pathspec_item *item,
57 int prefix)
58 {
59 if (prefix > 0) {
60 - if (strncmp(pattern, string, prefix))
60 + if (ps_strncmp(item, pattern, string, prefix))
61 return FNM_NOMATCH;
62 pattern += prefix;
63 string += prefix;
@@ -66,14 +66,18 @@ inline int git_fnmatch(const struct pathspec_item *item,
66 int pattern_len = strlen(++pattern);
67 int string_len = strlen(string);
68 return string_len < pattern_len ||
69 - strcmp(pattern,
70 - string + string_len - pattern_len);
69 + ps_strcmp(item, pattern,
70 + string + string_len - pattern_len);
71 }
72 if (item->magic & PATHSPEC_GLOB)
73 - return wildmatch(pattern, string, WM_PATHNAME, NULL);
73 + return wildmatch(pattern, string,
74 + WM_PATHNAME |
75 + (item->magic & PATHSPEC_ICASE ? WM_CASEFOLD : 0),
76 + NULL);
77 else
78 /* wildmatch has not learned no FNM_PATHNAME mode yet */
76 - return fnmatch(pattern, string, 0);
79 + return fnmatch(pattern, string,
80 + item->magic & PATHSPEC_ICASE ? FNM_CASEFOLD : 0);
81 }
82
83 static int fnmatch_icase_mem(const char *pattern, int patternlen,
@@ -110,16 +114,27 @@ static size_t common_prefix_len(const struct pathspec *pathspec)
114 int n;
115 size_t max = 0;
116
117 + /*
118 + * ":(icase)path" is treated as a pathspec full of
119 + * wildcard. In other words, only prefix is considered common
120 + * prefix. If the pathspec is abc/foo abc/bar, running in
121 + * subdir xyz, the common prefix is still xyz, not xuz/abc as
122 + * in non-:(icase).
123 + */
124 GUARD_PATHSPEC(pathspec,
125 PATHSPEC_FROMTOP |
126 PATHSPEC_MAXDEPTH |
127 PATHSPEC_LITERAL |
117 - PATHSPEC_GLOB);
128 + PATHSPEC_GLOB |
129 + PATHSPEC_ICASE);
130
131 for (n = 0; n < pathspec->nr; n++) {
120 - size_t i = 0, len = 0;
121 - while (i < pathspec->items[n].nowildcard_len &&
122 - (n == 0 || i < max)) {
132 + size_t i = 0, len = 0, item_len;
133 + if (pathspec->items[n].magic & PATHSPEC_ICASE)
134 + item_len = pathspec->items[n].prefix;
135 + else
136 + item_len = pathspec->items[n].nowildcard_len;
137 + while (i < item_len && (n == 0 || i < max)) {
138 char c = pathspec->items[n].match[i];
139 if (c != pathspec->items[0].match[i])
140 break;
@@ -196,11 +211,44 @@ static int match_pathspec_item(const struct pathspec_item *item, int prefix,
211 const char *match = item->match + prefix;
212 int matchlen = item->len - prefix;
213
214 + /*
215 + * The normal call pattern is:
216 + * 1. prefix = common_prefix_len(ps);
217 + * 2. prune something, or fill_directory
218 + * 3. match_pathspec_depth()
219 + *
220 + * 'prefix' at #1 may be shorter than the command's prefix and
221 + * it's ok for #2 to match extra files. Those extras will be
222 + * trimmed at #3.
223 + *
224 + * Suppose the pathspec is 'foo' and '../bar' running from
225 + * subdir 'xyz'. The common prefix at #1 will be empty, thanks
226 + * to "../". We may have xyz/foo _and_ XYZ/foo after #2. The
227 + * user does not want XYZ/foo, only the "foo" part should be
228 + * case-insensitive. We need to filter out XYZ/foo here. In
229 + * other words, we do not trust the caller on comparing the
230 + * prefix part when :(icase) is involved. We do exact
231 + * comparison ourselves.
232 + *
233 + * Normally the caller (common_prefix_len() in fact) does
234 + * _exact_ matching on name[-prefix+1..-1] and we do not need
235 + * to check that part. Be defensive and check it anyway, in
236 + * case common_prefix_len is changed, or a new caller is
237 + * introduced that does not use common_prefix_len.
238 + *
239 + * If the penalty turns out too high when prefix is really
240 + * long, maybe change it to
241 + * strncmp(match, name, item->prefix - prefix)
242 + */
243 + if (item->prefix && (item->magic & PATHSPEC_ICASE) &&
244 + strncmp(item->match, name - prefix, item->prefix))
245 + return 0;
246 +
247 /* If the match was just the prefix, we matched */
248 if (!*match)
249 return MATCHED_RECURSIVELY;
250
203 - if (matchlen <= namelen && !strncmp(match, name, matchlen)) {
251 + if (matchlen <= namelen && !ps_strncmp(item, match, name, matchlen)) {
252 if (matchlen == namelen)
253 return MATCHED_EXACTLY;
254
@@ -241,7 +289,8 @@ int match_pathspec_depth(const struct pathspec *ps,
289 PATHSPEC_FROMTOP |
290 PATHSPEC_MAXDEPTH |
291 PATHSPEC_LITERAL |
244 - PATHSPEC_GLOB);
292 + PATHSPEC_GLOB |
293 + PATHSPEC_ICASE);
294
295 if (!ps->nr) {
296 if (!ps->recursive ||
@@ -1301,7 +1350,8 @@ int read_directory(struct dir_struct *dir, const char *path, int len, const stru
1350 PATHSPEC_FROMTOP |
1351 PATHSPEC_MAXDEPTH |
1352 PATHSPEC_LITERAL |
1304 - PATHSPEC_GLOB);
1353 + PATHSPEC_GLOB |
1354 + PATHSPEC_ICASE);
1355
1356 if (has_symlink_leading_path(path, len))
1357 return dir->nr;
git.c
+4
@@ -155,6 +155,10 @@ static int handle_options(const char ***argv, int *argc, int *envchanged)
155 setenv(GIT_NOGLOB_PATHSPECS_ENVIRONMENT, "1", 1);
156 if (envchanged)
157 *envchanged = 1;
158 + } else if (!strcmp(cmd, "--icase-pathspecs")) {
159 + setenv(GIT_ICASE_PATHSPECS_ENVIRONMENT, "1", 1);
160 + if (envchanged)
161 + *envchanged = 1;
162 } else if (!strcmp(cmd, "--shallow-file")) {
163 (*argv)++;
164 (*argc)--;
pathspec.c
+8 -1
@@ -57,7 +57,6 @@ char *find_pathspecs_matching_against_index(const struct pathspec *pathspec)
57 *
58 * Possible future magic semantics include stuff like:
59 *
60 - * { PATHSPEC_ICASE, '\0', "icase" },
60 * { PATHSPEC_RECURSIVE, '*', "recursive" },
61 * { PATHSPEC_REGEXP, '\0', "regexp" },
62 *
@@ -71,6 +70,7 @@ static struct pathspec_magic {
70 { PATHSPEC_FROMTOP, '/', "top" },
71 { PATHSPEC_LITERAL, 0, "literal" },
72 { PATHSPEC_GLOB, '\0', "glob" },
73 + { PATHSPEC_ICASE, '\0', "icase" },
74 };
75
76 /*
@@ -95,6 +95,7 @@ static unsigned prefix_pathspec(struct pathspec_item *item,
95 static int literal_global = -1;
96 static int glob_global = -1;
97 static int noglob_global = -1;
98 + static int icase_global = -1;
99 unsigned magic = 0, short_magic = 0, global_magic = 0;
100 const char *copyfrom = elt, *long_magic_end = NULL;
101 char *match;
@@ -116,6 +117,12 @@ static unsigned prefix_pathspec(struct pathspec_item *item,
117 if (glob_global && noglob_global)
118 die(_("global 'glob' and 'noglob' pathspec settings are incompatible"));
119
120 +
121 + if (icase_global < 0)
122 + icase_global = git_env_bool(GIT_ICASE_PATHSPECS_ENVIRONMENT, 0);
123 + if (icase_global)
124 + global_magic |= PATHSPEC_ICASE;
125 +
126 if ((global_magic & PATHSPEC_LITERAL) &&
127 (global_magic & ~PATHSPEC_LITERAL))
128 die(_("global 'literal' pathspec setting is incompatible "
pathspec.h
+21 -1
@@ -6,11 +6,13 @@
6 #define PATHSPEC_MAXDEPTH (1<<1)
7 #define PATHSPEC_LITERAL (1<<2)
8 #define PATHSPEC_GLOB (1<<3)
9 +#define PATHSPEC_ICASE (1<<4)
10 #define PATHSPEC_ALL_MAGIC \
11 (PATHSPEC_FROMTOP | \
12 PATHSPEC_MAXDEPTH | \
13 PATHSPEC_LITERAL | \
13 - PATHSPEC_GLOB)
14 + PATHSPEC_GLOB | \
15 + PATHSPEC_ICASE)
16
17 #define PATHSPEC_ONESTAR 1 /* the pathspec pattern sastisfies GFNM_ONESTAR */
18
@@ -65,6 +67,24 @@ extern void parse_pathspec(struct pathspec *pathspec,
67 extern void copy_pathspec(struct pathspec *dst, const struct pathspec *src);
68 extern void free_pathspec(struct pathspec *);
69
70 +static inline int ps_strncmp(const struct pathspec_item *item,
71 + const char *s1, const char *s2, size_t n)
72 +{
73 + if (item->magic & PATHSPEC_ICASE)
74 + return strncasecmp(s1, s2, n);
75 + else
76 + return strncmp(s1, s2, n);
77 +}
78 +
79 +static inline int ps_strcmp(const struct pathspec_item *item,
80 + const char *s1, const char *s2)
81 +{
82 + if (item->magic & PATHSPEC_ICASE)
83 + return strcasecmp(s1, s2);
84 + else
85 + return strcmp(s1, s2);
86 +}
87 +
88 extern char *find_pathspecs_matching_against_index(const struct pathspec *pathspec);
89 extern void add_pathspec_matches_against_index(const struct pathspec *pathspec, char *seen);
90 extern const char *check_path_for_gitlink(const char *path);
t/t6131-pathspec-icase.sh new
+97
@@ -0,0 +1,97 @@
1 +#!/bin/sh
2 +
3 +test_description='test case insensitive pathspec limiting'
4 +. ./test-lib.sh
5 +
6 +test_expect_success 'create commits with glob characters' '
7 + test_commit bar bar &&
8 + test_commit bAr bAr &&
9 + test_commit BAR BAR &&
10 + mkdir foo &&
11 + test_commit foo/bar foo/bar &&
12 + test_commit foo/bAr foo/bAr &&
13 + test_commit foo/BAR foo/BAR &&
14 + mkdir fOo &&
15 + test_commit fOo/bar fOo/bar &&
16 + test_commit fOo/bAr fOo/bAr &&
17 + test_commit fOo/BAR fOo/BAR &&
18 + mkdir FOO &&
19 + test_commit FOO/bar FOO/bar &&
20 + test_commit FOO/bAr FOO/bAr &&
21 + test_commit FOO/BAR FOO/BAR
22 +'
23 +
24 +test_expect_success 'tree_entry_interesting matches bar' '
25 + echo bar >expect &&
26 + git log --format=%s -- "bar" >actual &&
27 + test_cmp expect actual
28 +'
29 +
30 +test_expect_success 'tree_entry_interesting matches :(icase)bar' '
31 + cat <<-EOF >expect &&
32 + BAR
33 + bAr
34 + bar
35 + EOF
36 + git log --format=%s -- ":(icase)bar" >actual &&
37 + test_cmp expect actual
38 +'
39 +
40 +test_expect_success 'tree_entry_interesting matches :(icase)bar with prefix' '
41 + cat <<-EOF >expect &&
42 + fOo/BAR
43 + fOo/bAr
44 + fOo/bar
45 + EOF
46 + ( cd fOo && git log --format=%s -- ":(icase)bar" ) >actual &&
47 + test_cmp expect actual
48 +'
49 +
50 +test_expect_success 'tree_entry_interesting matches :(icase)bar with empty prefix' '
51 + cat <<-EOF >expect &&
52 + FOO/BAR
53 + FOO/bAr
54 + FOO/bar
55 + fOo/BAR
56 + fOo/bAr
57 + fOo/bar
58 + foo/BAR
59 + foo/bAr
60 + foo/bar
61 + EOF
62 + ( cd fOo && git log --format=%s -- ":(icase)../foo/bar" ) >actual &&
63 + test_cmp expect actual
64 +'
65 +
66 +test_expect_success 'match_pathspec_depth matches :(icase)bar' '
67 + cat <<-EOF >expect &&
68 + BAR
69 + bAr
70 + bar
71 + EOF
72 + git ls-files ":(icase)bar" >actual &&
73 + test_cmp expect actual
74 +'
75 +
76 +test_expect_success 'match_pathspec_depth matches :(icase)bar with prefix' '
77 + cat <<-EOF >expect &&
78 + fOo/BAR
79 + fOo/bAr
80 + fOo/bar
81 + EOF
82 + ( cd fOo && git ls-files --full-name ":(icase)bar" ) >actual &&
83 + test_cmp expect actual
84 +'
85 +
86 +test_expect_success 'match_pathspec_depth matches :(icase)bar with empty prefix' '
87 + cat <<-EOF >expect &&
88 + bar
89 + fOo/BAR
90 + fOo/bAr
91 + fOo/bar
92 + EOF
93 + ( cd fOo && git ls-files --full-name ":(icase)bar" ../bar ) >actual &&
94 + test_cmp expect actual
95 +'
96 +
97 +test_done
tree-walk.c
+48 -11
@@ -489,13 +489,25 @@ int get_tree_entry(const unsigned char *tree_sha1, const char *name, unsigned ch
489 return retval;
490 }
491
492 -static int match_entry(const struct name_entry *entry, int pathlen,
492 +static int match_entry(const struct pathspec_item *item,
493 + const struct name_entry *entry, int pathlen,
494 const char *match, int matchlen,
495 enum interesting *never_interesting)
496 {
497 int m = -1; /* signals that we haven't called strncmp() */
498
498 - if (*never_interesting != entry_not_interesting) {
499 + if (item->magic & PATHSPEC_ICASE)
500 + /*
501 + * "Never interesting" trick requires exact
502 + * matching. We could do something clever with inexact
503 + * matching, but it's trickier (and not to forget that
504 + * strcasecmp is locale-dependent, at least in
505 + * glibc). Just disable it for now. It can't be worse
506 + * than the wildcard's codepath of '[Tt][Hi][Is][Ss]'
507 + * pattern.
508 + */
509 + *never_interesting = entry_not_interesting;
510 + else if (*never_interesting != entry_not_interesting) {
511 /*
512 * We have not seen any match that sorts later
513 * than the current path.
@@ -541,7 +553,7 @@ static int match_entry(const struct name_entry *entry, int pathlen,
553 * we cheated and did not do strncmp(), so we do
554 * that here.
555 */
544 - m = strncmp(match, entry->path, pathlen);
556 + m = ps_strncmp(item, match, entry->path, pathlen);
557
558 /*
559 * If common part matched earlier then it is a hit,
@@ -549,15 +561,39 @@ static int match_entry(const struct name_entry *entry, int pathlen,
561 * leading directory and is shorter than match.
562 */
563 if (!m)
564 + /*
565 + * match_entry does not check if the prefix part is
566 + * matched case-sensitively. If the entry is a
567 + * directory and part of prefix, it'll be rematched
568 + * eventually by basecmp with special treatment for
569 + * the prefix.
570 + */
571 return 1;
572
573 return 0;
574 }
575
557 -static int match_dir_prefix(const char *base,
576 +/* :(icase)-aware string compare */
577 +static int basecmp(const struct pathspec_item *item,
578 + const char *base, const char *match, int len)
579 +{
580 + if (item->magic & PATHSPEC_ICASE) {
581 + int ret, n = len > item->prefix ? item->prefix : len;
582 + ret = strncmp(base, match, n);
583 + if (ret)
584 + return ret;
585 + base += n;
586 + match += n;
587 + len -= n;
588 + }
589 + return ps_strncmp(item, base, match, len);
590 +}
591 +
592 +static int match_dir_prefix(const struct pathspec_item *item,
593 + const char *base,
594 const char *match, int matchlen)
595 {
560 - if (strncmp(base, match, matchlen))
596 + if (basecmp(item, base, match, matchlen))
597 return 0;
598
599 /*
@@ -594,7 +630,7 @@ static int match_wildcard_base(const struct pathspec_item *item,
630 */
631 if (baselen >= matchlen) {
632 *matched = matchlen;
597 - return !strncmp(base, match, matchlen);
633 + return !basecmp(item, base, match, matchlen);
634 }
635
636 dirlen = matchlen;
@@ -607,7 +643,7 @@ static int match_wildcard_base(const struct pathspec_item *item,
643 * base ends with '/' so we are sure it really matches
644 * directory
645 */
610 - if (strncmp(base, match, baselen))
646 + if (basecmp(item, base, match, baselen))
647 return 0;
648 *matched = baselen;
649 } else
@@ -640,7 +676,8 @@ enum interesting tree_entry_interesting(const struct name_entry *entry,
676 PATHSPEC_FROMTOP |
677 PATHSPEC_MAXDEPTH |
678 PATHSPEC_LITERAL |
643 - PATHSPEC_GLOB);
679 + PATHSPEC_GLOB |
680 + PATHSPEC_ICASE);
681
682 if (!ps->nr) {
683 if (!ps->recursive ||
@@ -663,7 +700,7 @@ enum interesting tree_entry_interesting(const struct name_entry *entry,
700
701 if (baselen >= matchlen) {
702 /* If it doesn't match, move along... */
666 - if (!match_dir_prefix(base_str, match, matchlen))
703 + if (!match_dir_prefix(item, base_str, match, matchlen))
704 goto match_wildcards;
705
706 if (!ps->recursive ||
@@ -679,8 +716,8 @@ enum interesting tree_entry_interesting(const struct name_entry *entry,
716 }
717
718 /* Either there must be no base, or the base must match. */
682 - if (baselen == 0 || !strncmp(base_str, match, baselen)) {
683 - if (match_entry(entry, pathlen,
719 + if (baselen == 0 || !basecmp(item, base_str, match, baselen)) {
720 + if (match_entry(item, entry, pathlen,
721 match + baselen, matchlen - baselen,
722 &never_interesting))
723 return entry_interesting;