Skip to content

Commit 93d9353

Browse files
pcloudsgitster
authored andcommitted
parse_pathspec: accept :(icase)path syntax
Signed-off-by: Nguyễn Thái Ngọc Duy <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent bd30c2e commit 93d9353

File tree

11 files changed

+257
-28
lines changed

11 files changed

+257
-28
lines changed

Documentation/git.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,10 @@ help ...`.
466466
globbing on individual pathspecs can be done using pathspec
467467
magic ":(glob)"
468468

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+
469473
GIT COMMANDS
470474
------------
471475

@@ -879,6 +883,10 @@ GIT_NOGLOB_PATHSPECS::
879883
Setting this variable to `1` will cause Git to treat all
880884
pathspecs as literal (aka "literal" magic).
881885

886+
GIT_ICASE_PATHSPECS::
887+
Setting this variable to `1` will cause Git to treat all
888+
pathspecs as case-insensitive.
889+
882890

883891
Discussion[[Discussion]]
884892
------------------------

Documentation/glossary-content.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,9 @@ literal;;
334334
Wildcards in the pattern such as `*` or `?` are treated
335335
as literal characters.
336336

337+
icase;;
338+
Case insensitive match.
339+
337340
glob;;
338341
Git treats the pattern as a shell glob suitable for
339342
consumption by fnmatch(3) with the FNM_PATHNAME flag:

builtin/add.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -544,12 +544,14 @@ int cmd_add(int argc, const char **argv, const char *prefix)
544544
GUARD_PATHSPEC(&pathspec,
545545
PATHSPEC_FROMTOP |
546546
PATHSPEC_LITERAL |
547-
PATHSPEC_GLOB);
547+
PATHSPEC_GLOB |
548+
PATHSPEC_ICASE);
548549

549550
for (i = 0; i < pathspec.nr; i++) {
550551
const char *path = pathspec.items[i].match;
551552
if (!seen[i] &&
552-
((pathspec.items[i].magic & PATHSPEC_GLOB) ||
553+
((pathspec.items[i].magic &
554+
(PATHSPEC_GLOB | PATHSPEC_ICASE)) ||
553555
!file_exists(path))) {
554556
if (ignore_missing) {
555557
int dtype = DT_UNKNOWN;

builtin/ls-tree.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ int cmd_ls_tree(int argc, const char **argv, const char *prefix)
173173
* cannot be lifted until it is converted to use
174174
* match_pathspec_depth() or tree_entry_interesting()
175175
*/
176-
parse_pathspec(&pathspec, PATHSPEC_GLOB,
176+
parse_pathspec(&pathspec, PATHSPEC_GLOB | PATHSPEC_ICASE,
177177
PATHSPEC_PREFER_CWD,
178178
prefix, argv + 1);
179179
for (i = 0; i < pathspec.nr; i++)

cache.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ static inline enum object_type object_type(unsigned int mode)
369369
#define GIT_LITERAL_PATHSPECS_ENVIRONMENT "GIT_LITERAL_PATHSPECS"
370370
#define GIT_GLOB_PATHSPECS_ENVIRONMENT "GIT_GLOB_PATHSPECS"
371371
#define GIT_NOGLOB_PATHSPECS_ENVIRONMENT "GIT_NOGLOB_PATHSPECS"
372+
#define GIT_ICASE_PATHSPECS_ENVIRONMENT "GIT_ICASE_PATHSPECS"
372373

373374
/*
374375
* This environment variable is expected to contain a boolean indicating

dir.c

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ inline int git_fnmatch(const struct pathspec_item *item,
5757
int prefix)
5858
{
5959
if (prefix > 0) {
60-
if (strncmp(pattern, string, prefix))
60+
if (ps_strncmp(item, pattern, string, prefix))
6161
return FNM_NOMATCH;
6262
pattern += prefix;
6363
string += prefix;
@@ -66,14 +66,18 @@ inline int git_fnmatch(const struct pathspec_item *item,
6666
int pattern_len = strlen(++pattern);
6767
int string_len = strlen(string);
6868
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);
7171
}
7272
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);
7477
else
7578
/* 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);
7781
}
7882

7983
static int fnmatch_icase_mem(const char *pattern, int patternlen,
@@ -110,16 +114,27 @@ static size_t common_prefix_len(const struct pathspec *pathspec)
110114
int n;
111115
size_t max = 0;
112116

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+
*/
113124
GUARD_PATHSPEC(pathspec,
114125
PATHSPEC_FROMTOP |
115126
PATHSPEC_MAXDEPTH |
116127
PATHSPEC_LITERAL |
117-
PATHSPEC_GLOB);
128+
PATHSPEC_GLOB |
129+
PATHSPEC_ICASE);
118130

119131
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)) {
123138
char c = pathspec->items[n].match[i];
124139
if (c != pathspec->items[0].match[i])
125140
break;
@@ -196,11 +211,44 @@ static int match_pathspec_item(const struct pathspec_item *item, int prefix,
196211
const char *match = item->match + prefix;
197212
int matchlen = item->len - prefix;
198213

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+
199247
/* If the match was just the prefix, we matched */
200248
if (!*match)
201249
return MATCHED_RECURSIVELY;
202250

203-
if (matchlen <= namelen && !strncmp(match, name, matchlen)) {
251+
if (matchlen <= namelen && !ps_strncmp(item, match, name, matchlen)) {
204252
if (matchlen == namelen)
205253
return MATCHED_EXACTLY;
206254

@@ -241,7 +289,8 @@ int match_pathspec_depth(const struct pathspec *ps,
241289
PATHSPEC_FROMTOP |
242290
PATHSPEC_MAXDEPTH |
243291
PATHSPEC_LITERAL |
244-
PATHSPEC_GLOB);
292+
PATHSPEC_GLOB |
293+
PATHSPEC_ICASE);
245294

246295
if (!ps->nr) {
247296
if (!ps->recursive ||
@@ -1301,7 +1350,8 @@ int read_directory(struct dir_struct *dir, const char *path, int len, const stru
13011350
PATHSPEC_FROMTOP |
13021351
PATHSPEC_MAXDEPTH |
13031352
PATHSPEC_LITERAL |
1304-
PATHSPEC_GLOB);
1353+
PATHSPEC_GLOB |
1354+
PATHSPEC_ICASE);
13051355

13061356
if (has_symlink_leading_path(path, len))
13071357
return dir->nr;

git.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ static int handle_options(const char ***argv, int *argc, int *envchanged)
155155
setenv(GIT_NOGLOB_PATHSPECS_ENVIRONMENT, "1", 1);
156156
if (envchanged)
157157
*envchanged = 1;
158+
} else if (!strcmp(cmd, "--icase-pathspecs")) {
159+
setenv(GIT_ICASE_PATHSPECS_ENVIRONMENT, "1", 1);
160+
if (envchanged)
161+
*envchanged = 1;
158162
} else if (!strcmp(cmd, "--shallow-file")) {
159163
(*argv)++;
160164
(*argc)--;

pathspec.c

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ char *find_pathspecs_matching_against_index(const struct pathspec *pathspec)
5757
*
5858
* Possible future magic semantics include stuff like:
5959
*
60-
* { PATHSPEC_ICASE, '\0', "icase" },
6160
* { PATHSPEC_RECURSIVE, '*', "recursive" },
6261
* { PATHSPEC_REGEXP, '\0', "regexp" },
6362
*
@@ -71,6 +70,7 @@ static struct pathspec_magic {
7170
{ PATHSPEC_FROMTOP, '/', "top" },
7271
{ PATHSPEC_LITERAL, 0, "literal" },
7372
{ PATHSPEC_GLOB, '\0', "glob" },
73+
{ PATHSPEC_ICASE, '\0', "icase" },
7474
};
7575

7676
/*
@@ -95,6 +95,7 @@ static unsigned prefix_pathspec(struct pathspec_item *item,
9595
static int literal_global = -1;
9696
static int glob_global = -1;
9797
static int noglob_global = -1;
98+
static int icase_global = -1;
9899
unsigned magic = 0, short_magic = 0, global_magic = 0;
99100
const char *copyfrom = elt, *long_magic_end = NULL;
100101
char *match;
@@ -116,6 +117,12 @@ static unsigned prefix_pathspec(struct pathspec_item *item,
116117
if (glob_global && noglob_global)
117118
die(_("global 'glob' and 'noglob' pathspec settings are incompatible"));
118119

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+
119126
if ((global_magic & PATHSPEC_LITERAL) &&
120127
(global_magic & ~PATHSPEC_LITERAL))
121128
die(_("global 'literal' pathspec setting is incompatible "

pathspec.h

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
#define PATHSPEC_MAXDEPTH (1<<1)
77
#define PATHSPEC_LITERAL (1<<2)
88
#define PATHSPEC_GLOB (1<<3)
9+
#define PATHSPEC_ICASE (1<<4)
910
#define PATHSPEC_ALL_MAGIC \
1011
(PATHSPEC_FROMTOP | \
1112
PATHSPEC_MAXDEPTH | \
1213
PATHSPEC_LITERAL | \
13-
PATHSPEC_GLOB)
14+
PATHSPEC_GLOB | \
15+
PATHSPEC_ICASE)
1416

1517
#define PATHSPEC_ONESTAR 1 /* the pathspec pattern sastisfies GFNM_ONESTAR */
1618

@@ -65,6 +67,24 @@ extern void parse_pathspec(struct pathspec *pathspec,
6567
extern void copy_pathspec(struct pathspec *dst, const struct pathspec *src);
6668
extern void free_pathspec(struct pathspec *);
6769

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+
6888
extern char *find_pathspecs_matching_against_index(const struct pathspec *pathspec);
6989
extern void add_pathspec_matches_against_index(const struct pathspec *pathspec, char *seen);
7090
extern const char *check_path_for_gitlink(const char *path);

t/t6131-pathspec-icase.sh

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

0 commit comments

Comments
 (0)