Skip to content

Commit be5ab43

Browse files
committed
Merge branch 'jc/magic-pathspec'
* jc/magic-pathspec: setup.c: Fix some "symbol not declared" sparse warnings t3703: Skip tests using directory name ":" on Windows revision.c: leave a note for "a lone :" enhancement t3703, t4208: add test cases for magic pathspec rev/path disambiguation: further restrict "misspelled index entry" diag fix overslow :/no-such-string-ever-existed diagnostics fix overstrict :<path> diagnosis grep: use get_pathspec() correctly pathspec: drop "lone : means no pathspec" from get_pathspec() Revert "magic pathspec: add ":(icase)path" to match case insensitively" magic pathspec: add ":(icase)path" to match case insensitively magic pathspec: futureproof shorthand form magic pathspec: add tentative ":/path/from/top/level" pathspec support
2 parents b7aba2e + 488201c commit be5ab43

10 files changed

Lines changed: 276 additions & 31 deletions

File tree

Documentation/glossary-content.txt

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,8 @@ This commit is referred to as a "merge commit", or sometimes just a
277277
Pattern used to specify paths.
278278
+
279279
Pathspecs are used on the command line of "git ls-files", "git
280-
ls-tree", "git grep", "git checkout", and many other commands to
280+
ls-tree", "git add", "git grep", "git diff", "git checkout",
281+
and many other commands to
281282
limit the scope of operations to some subset of the tree or
282283
worktree. See the documentation of each command for whether
283284
paths are relative to the current directory or toplevel. The
@@ -296,6 +297,37 @@ For example, Documentation/*.jpg will match all .jpg files
296297
in the Documentation subtree,
297298
including Documentation/chapter_1/figure_1.jpg.
298299

300+
+
301+
A pathspec that begins with a colon `:` has special meaning. In the
302+
short form, the leading colon `:` is followed by zero or more "magic
303+
signature" letters (which optionally is terminated by another colon `:`),
304+
and the remainder is the pattern to match against the path. The optional
305+
colon that terminates the "magic signature" can be omitted if the pattern
306+
begins with a character that cannot be a "magic signature" and is not a
307+
colon.
308+
+
309+
In the long form, the leading colon `:` is followed by a open
310+
parenthesis `(`, a comma-separated list of zero or more "magic words",
311+
and a close parentheses `)`, and the remainder is the pattern to match
312+
against the path.
313+
+
314+
The "magic signature" consists of an ASCII symbol that is not
315+
alphanumeric.
316+
+
317+
--
318+
top `/`;;
319+
The magic word `top` (mnemonic: `/`) makes the pattern match
320+
from the root of the working tree, even when you are running
321+
the command from inside a subdirectory.
322+
--
323+
+
324+
Currently only the slash `/` is recognized as the "magic signature",
325+
but it is envisioned that we will support more types of magic in later
326+
versions of git.
327+
+
328+
A pathspec with only a colon means "there is no pathspec". This form
329+
should not be combined with other pathspec.
330+
299331
[[def_parent]]parent::
300332
A <<def_commit_object,commit object>> contains a (possibly empty) list
301333
of the logical predecessor(s) in the line of development, i.e. its

builtin/grep.c

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -969,13 +969,7 @@ int cmd_grep(int argc, const char **argv, const char *prefix)
969969
verify_filename(prefix, argv[j]);
970970
}
971971

972-
if (i < argc)
973-
paths = get_pathspec(prefix, argv + i);
974-
else if (prefix) {
975-
paths = xcalloc(2, sizeof(const char *));
976-
paths[0] = prefix;
977-
paths[1] = NULL;
978-
}
972+
paths = get_pathspec(prefix, argv + i);
979973
init_pathspec(&pathspec, paths);
980974
pathspec.max_depth = opt.max_depth;
981975
pathspec.recursive = 1;

cache.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -810,15 +810,15 @@ struct object_context {
810810
};
811811

812812
extern int get_sha1(const char *str, unsigned char *sha1);
813-
extern int get_sha1_with_mode_1(const char *str, unsigned char *sha1, unsigned *mode, int gently, const char *prefix);
813+
extern int get_sha1_with_mode_1(const char *str, unsigned char *sha1, unsigned *mode, int only_to_die, const char *prefix);
814814
static inline int get_sha1_with_mode(const char *str, unsigned char *sha1, unsigned *mode)
815815
{
816-
return get_sha1_with_mode_1(str, sha1, mode, 1, NULL);
816+
return get_sha1_with_mode_1(str, sha1, mode, 0, NULL);
817817
}
818-
extern int get_sha1_with_context_1(const char *name, unsigned char *sha1, struct object_context *orc, int gently, const char *prefix);
818+
extern int get_sha1_with_context_1(const char *name, unsigned char *sha1, struct object_context *orc, int only_to_die, const char *prefix);
819819
static inline int get_sha1_with_context(const char *str, unsigned char *sha1, struct object_context *orc)
820820
{
821-
return get_sha1_with_context_1(str, sha1, orc, 1, NULL);
821+
return get_sha1_with_context_1(str, sha1, orc, 0, NULL);
822822
}
823823
extern int get_sha1_hex(const char *hex, unsigned char *sha1);
824824
extern char *sha1_to_hex(const unsigned char *sha1); /* static buffer result! */

ctype.c

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,18 @@ enum {
1010
A = GIT_ALPHA,
1111
D = GIT_DIGIT,
1212
G = GIT_GLOB_SPECIAL, /* *, ?, [, \\ */
13-
R = GIT_REGEX_SPECIAL /* $, (, ), +, ., ^, {, | */
13+
R = GIT_REGEX_SPECIAL, /* $, (, ), +, ., ^, {, | */
14+
P = GIT_PATHSPEC_MAGIC /* other non-alnum, except for ] and } */
1415
};
1516

1617
unsigned char sane_ctype[256] = {
1718
0, 0, 0, 0, 0, 0, 0, 0, 0, S, S, 0, 0, S, 0, 0, /* 0.. 15 */
1819
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 16.. 31 */
19-
S, 0, 0, 0, R, 0, 0, 0, R, R, G, R, 0, 0, R, 0, /* 32.. 47 */
20-
D, D, D, D, D, D, D, D, D, D, 0, 0, 0, 0, 0, G, /* 48.. 63 */
21-
0, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, /* 64.. 79 */
22-
A, A, A, A, A, A, A, A, A, A, A, G, G, 0, R, 0, /* 80.. 95 */
23-
0, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, /* 96..111 */
24-
A, A, A, A, A, A, A, A, A, A, A, R, R, 0, 0, 0, /* 112..127 */
20+
S, P, P, P, R, P, P, P, R, R, G, R, P, P, R, P, /* 32.. 47 */
21+
D, D, D, D, D, D, D, D, D, D, P, P, P, P, P, G, /* 48.. 63 */
22+
P, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, /* 64.. 79 */
23+
A, A, A, A, A, A, A, A, A, A, A, G, G, 0, R, P, /* 80.. 95 */
24+
P, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, /* 96..111 */
25+
A, A, A, A, A, A, A, A, A, A, A, R, R, 0, P, 0, /* 112..127 */
2526
/* Nothing in the 128.. range */
2627
};

git-compat-util.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,7 @@ extern unsigned char sane_ctype[256];
463463
#define GIT_ALPHA 0x04
464464
#define GIT_GLOB_SPECIAL 0x08
465465
#define GIT_REGEX_SPECIAL 0x10
466+
#define GIT_PATHSPEC_MAGIC 0x20
466467
#define sane_istest(x,mask) ((sane_ctype[(unsigned char)(x)] & (mask)) != 0)
467468
#define isascii(x) (((x) & ~0x7f) == 0)
468469
#define isspace(x) sane_istest(x,GIT_SPACE)
@@ -473,6 +474,7 @@ extern unsigned char sane_ctype[256];
473474
#define is_regex_special(x) sane_istest(x,GIT_GLOB_SPECIAL | GIT_REGEX_SPECIAL)
474475
#define tolower(x) sane_case((unsigned char)(x), 0x20)
475476
#define toupper(x) sane_case((unsigned char)(x), 0)
477+
#define is_pathspec_magic(x) sane_istest(x,GIT_PATHSPEC_MAGIC)
476478

477479
static inline int sane_case(int x, int high)
478480
{

revision.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1661,6 +1661,20 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s
16611661
}
16621662

16631663
if (prune_data.nr) {
1664+
/*
1665+
* If we need to introduce the magic "a lone ':' means no
1666+
* pathspec whatsoever", here is the place to do so.
1667+
*
1668+
* if (prune_data.nr == 1 && !strcmp(prune_data[0], ":")) {
1669+
* prune_data.nr = 0;
1670+
* prune_data.alloc = 0;
1671+
* free(prune_data.path);
1672+
* prune_data.path = NULL;
1673+
* } else {
1674+
* terminate prune_data.alloc with NULL and
1675+
* call init_pathspec() to set revs->prune_data here.
1676+
* }
1677+
*/
16641678
ALLOC_GROW(prune_data.path, prune_data.nr+1, prune_data.alloc);
16651679
prune_data.path[prune_data.nr++] = NULL;
16661680
init_pathspec(&revs->prune_data,

setup.c

Lines changed: 111 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,17 @@ static void NORETURN die_verify_filename(const char *prefix, const char *arg)
8585
{
8686
unsigned char sha1[20];
8787
unsigned mode;
88-
/* try a detailed diagnostic ... */
89-
get_sha1_with_mode_1(arg, sha1, &mode, 0, prefix);
88+
89+
/*
90+
* Saying "'(icase)foo' does not exist in the index" when the
91+
* user gave us ":(icase)foo" is just stupid. A magic pathspec
92+
* begins with a colon and is followed by a non-alnum; do not
93+
* let get_sha1_with_mode_1(only_to_die=1) to even trigger.
94+
*/
95+
if (!(arg[0] == ':' && !isalnum(arg[1])))
96+
/* try a detailed diagnostic ... */
97+
get_sha1_with_mode_1(arg, sha1, &mode, 1, prefix);
98+
9099
/* ... or fall back the most general message. */
91100
die("ambiguous argument '%s': unknown revision or path not in the working tree.\n"
92101
"Use '--' to separate paths from revisions", arg);
@@ -126,6 +135,105 @@ void verify_non_filename(const char *prefix, const char *arg)
126135
"Use '--' to separate filenames from revisions", arg);
127136
}
128137

138+
/*
139+
* Magic pathspec
140+
*
141+
* NEEDSWORK: These need to be moved to dir.h or even to a new
142+
* pathspec.h when we restructure get_pathspec() users to use the
143+
* "struct pathspec" interface.
144+
*
145+
* Possible future magic semantics include stuff like:
146+
*
147+
* { PATHSPEC_NOGLOB, '!', "noglob" },
148+
* { PATHSPEC_ICASE, '\0', "icase" },
149+
* { PATHSPEC_RECURSIVE, '*', "recursive" },
150+
* { PATHSPEC_REGEXP, '\0', "regexp" },
151+
*
152+
*/
153+
#define PATHSPEC_FROMTOP (1<<0)
154+
155+
static struct pathspec_magic {
156+
unsigned bit;
157+
char mnemonic; /* this cannot be ':'! */
158+
const char *name;
159+
} pathspec_magic[] = {
160+
{ PATHSPEC_FROMTOP, '/', "top" },
161+
};
162+
163+
/*
164+
* Take an element of a pathspec and check for magic signatures.
165+
* Append the result to the prefix.
166+
*
167+
* For now, we only parse the syntax and throw out anything other than
168+
* "top" magic.
169+
*
170+
* NEEDSWORK: This needs to be rewritten when we start migrating
171+
* get_pathspec() users to use the "struct pathspec" interface. For
172+
* example, a pathspec element may be marked as case-insensitive, but
173+
* the prefix part must always match literally, and a single stupid
174+
* string cannot express such a case.
175+
*/
176+
static const char *prefix_pathspec(const char *prefix, int prefixlen, const char *elt)
177+
{
178+
unsigned magic = 0;
179+
const char *copyfrom = elt;
180+
int i;
181+
182+
if (elt[0] != ':') {
183+
; /* nothing to do */
184+
} else if (elt[1] == '(') {
185+
/* longhand */
186+
const char *nextat;
187+
for (copyfrom = elt + 2;
188+
*copyfrom && *copyfrom != ')';
189+
copyfrom = nextat) {
190+
size_t len = strcspn(copyfrom, ",)");
191+
if (copyfrom[len] == ')')
192+
nextat = copyfrom + len;
193+
else
194+
nextat = copyfrom + len + 1;
195+
if (!len)
196+
continue;
197+
for (i = 0; i < ARRAY_SIZE(pathspec_magic); i++)
198+
if (strlen(pathspec_magic[i].name) == len &&
199+
!strncmp(pathspec_magic[i].name, copyfrom, len)) {
200+
magic |= pathspec_magic[i].bit;
201+
break;
202+
}
203+
if (ARRAY_SIZE(pathspec_magic) <= i)
204+
die("Invalid pathspec magic '%.*s' in '%s'",
205+
(int) len, copyfrom, elt);
206+
}
207+
if (*copyfrom == ')')
208+
copyfrom++;
209+
} else {
210+
/* shorthand */
211+
for (copyfrom = elt + 1;
212+
*copyfrom && *copyfrom != ':';
213+
copyfrom++) {
214+
char ch = *copyfrom;
215+
216+
if (!is_pathspec_magic(ch))
217+
break;
218+
for (i = 0; i < ARRAY_SIZE(pathspec_magic); i++)
219+
if (pathspec_magic[i].mnemonic == ch) {
220+
magic |= pathspec_magic[i].bit;
221+
break;
222+
}
223+
if (ARRAY_SIZE(pathspec_magic) <= i)
224+
die("Unimplemented pathspec magic '%c' in '%s'",
225+
ch, elt);
226+
}
227+
if (*copyfrom == ':')
228+
copyfrom++;
229+
}
230+
231+
if (magic & PATHSPEC_FROMTOP)
232+
return xstrdup(copyfrom);
233+
else
234+
return prefix_path(prefix, prefixlen, copyfrom);
235+
}
236+
129237
const char **get_pathspec(const char *prefix, const char **pathspec)
130238
{
131239
const char *entry = *pathspec;
@@ -147,8 +255,7 @@ const char **get_pathspec(const char *prefix, const char **pathspec)
147255
dst = pathspec;
148256
prefixlen = prefix ? strlen(prefix) : 0;
149257
while (*src) {
150-
const char *p = prefix_path(prefix, prefixlen, *src);
151-
*(dst++) = p;
258+
*(dst++) = prefix_pathspec(prefix, prefixlen, *src);
152259
src++;
153260
}
154261
*dst = NULL;

sha1_name.c

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,11 +1083,12 @@ static void diagnose_invalid_index_path(int stage,
10831083
}
10841084

10851085

1086-
int get_sha1_with_mode_1(const char *name, unsigned char *sha1, unsigned *mode, int gently, const char *prefix)
1086+
int get_sha1_with_mode_1(const char *name, unsigned char *sha1, unsigned *mode,
1087+
int only_to_die, const char *prefix)
10871088
{
10881089
struct object_context oc;
10891090
int ret;
1090-
ret = get_sha1_with_context_1(name, sha1, &oc, gently, prefix);
1091+
ret = get_sha1_with_context_1(name, sha1, &oc, only_to_die, prefix);
10911092
*mode = oc.mode;
10921093
return ret;
10931094
}
@@ -1111,7 +1112,7 @@ static char *resolve_relative_path(const char *rel)
11111112

11121113
int get_sha1_with_context_1(const char *name, unsigned char *sha1,
11131114
struct object_context *oc,
1114-
int gently, const char *prefix)
1115+
int only_to_die, const char *prefix)
11151116
{
11161117
int ret, bracket_depth;
11171118
int namelen = strlen(name);
@@ -1133,7 +1134,7 @@ int get_sha1_with_context_1(const char *name, unsigned char *sha1,
11331134
struct cache_entry *ce;
11341135
char *new_path = NULL;
11351136
int pos;
1136-
if (namelen > 2 && name[1] == '/') {
1137+
if (!only_to_die && namelen > 2 && name[1] == '/') {
11371138
struct commit_list *list = NULL;
11381139
for_each_ref(handle_one_ref, &list);
11391140
return get_sha1_oneline(name + 2, sha1, list);
@@ -1176,7 +1177,7 @@ int get_sha1_with_context_1(const char *name, unsigned char *sha1,
11761177
}
11771178
pos++;
11781179
}
1179-
if (!gently)
1180+
if (only_to_die && name[1] && name[1] != '/')
11801181
diagnose_invalid_index_path(stage, prefix, cp);
11811182
free(new_path);
11821183
return -1;
@@ -1192,7 +1193,7 @@ int get_sha1_with_context_1(const char *name, unsigned char *sha1,
11921193
if (*cp == ':') {
11931194
unsigned char tree_sha1[20];
11941195
char *object_name = NULL;
1195-
if (!gently) {
1196+
if (only_to_die) {
11961197
object_name = xmalloc(cp-name+1);
11971198
strncpy(object_name, name, cp-name);
11981199
object_name[cp-name] = '\0';
@@ -1205,7 +1206,7 @@ int get_sha1_with_context_1(const char *name, unsigned char *sha1,
12051206
if (new_filename)
12061207
filename = new_filename;
12071208
ret = get_tree_entry(tree_sha1, filename, sha1, &oc->mode);
1208-
if (!gently) {
1209+
if (only_to_die) {
12091210
diagnose_invalid_sha1_path(prefix, filename,
12101211
tree_sha1, object_name);
12111212
free(object_name);
@@ -1218,7 +1219,7 @@ int get_sha1_with_context_1(const char *name, unsigned char *sha1,
12181219
free(new_filename);
12191220
return ret;
12201221
} else {
1221-
if (!gently)
1222+
if (only_to_die)
12221223
die("Invalid object name '%s'.", object_name);
12231224
}
12241225
}

0 commit comments

Comments
 (0)