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;