From 8a42c9850177cc91e9f38779e8aca89682a02975 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Wed, 6 Apr 2011 16:11:56 -0700 Subject: [PATCH 1/3] magic pathspec: add tentative ":/path/from/top/level" pathspec support Support ":/" magic string that can be prefixed to a pathspec element to say "this names the path from the top-level of the working tree", when you are in the subdirectory. For example, you should be able to say: $ edit Makefile ;# top-level $ cd Documentation $ edit git.txt ;# in the subdirectory and then do one of three things, still inside the subdirectory: $ git add -u . ;# add only Documentation/git.txt $ git add -u :/ ;# add everything, including paths outside Documentation $ git add -u ;# whatever the default setting is. To truly support magic pathspec, the API needs to be restructured so that get_pathspec() and init_pathspec() are unified into one call. Currently, the former just prefixes the user supplied pathspec with the current subdirectory path, and the latter takes the output from the former and pre-parses them into a bit richer structure for easier handling. They should become a single API function that takes the current subdirectory path and the remainder of argv[] (after parsing --options and revision arguments from the command line) and returns an array of parsed pathspec elements, and "magic" should become attributes of struct pathspec_item. This patch implements only "top" magic because it can be hacked into the system without such a refactoring. The syntax for magic pathspec prefix is designed to be extensible yet simple to type to invoke a simple magic like "from the top". The parser for the magic prefix is hooked into get_pathspec() function in this patch, and it needs to be moved when we refactor the API. But we have to start from somewhere. Signed-off-by: Junio C Hamano --- Documentation/glossary-content.txt | 31 +++++++++- setup.c | 98 +++++++++++++++++++++++++++++- 2 files changed, 126 insertions(+), 3 deletions(-) diff --git a/Documentation/glossary-content.txt b/Documentation/glossary-content.txt index 33716a31d0..e51d7e60eb 100644 --- a/Documentation/glossary-content.txt +++ b/Documentation/glossary-content.txt @@ -277,7 +277,8 @@ This commit is referred to as a "merge commit", or sometimes just a Pattern used to specify paths. + Pathspecs are used on the command line of "git ls-files", "git -ls-tree", "git grep", "git checkout", and many other commands to +ls-tree", "git add", "git grep", "git diff", "git checkout", +and many other commands to limit the scope of operations to some subset of the tree or worktree. See the documentation of each command for whether paths are relative to the current directory or toplevel. The @@ -296,6 +297,34 @@ For example, Documentation/*.jpg will match all .jpg files in the Documentation subtree, including Documentation/chapter_1/figure_1.jpg. ++ +A pathspec that begins with a colon `:` has special meaning. In the +short form, the leading colon `:` is followed by zero or more "magic +signature" letters (which optionally is terminated by another colon `:`), +and the remainder is the pattern to match against the path. The optional +colon that terminates the "magic signature" can be omitted if the pattern +begins with a character that cannot be a "magic signature" and is not a +colon. ++ +In the long form, the leading colon `:` is followed by a open +parenthesis `(`, a comma-separated list of zero or more "magic words", +and a close parentheses `)`, and the remainder is the pattern to match +against the path. ++ +The "magic signature" consists of an ASCII symbol that is not +alphanumeric. ++ +-- +top `/`;; + The magic word `top` (mnemonic: `/`) makes the pattern match + from the root of the working tree, even when you are running + the command from inside a subdirectory. +-- ++ +Currently only the slash `/` is recognized as the "magic signature", +but it is envisioned that we will support more types of magic in later +versions of git. + [[def_parent]]parent:: A <> contains a (possibly empty) list of the logical predecessor(s) in the line of development, i.e. its diff --git a/setup.c b/setup.c index 03cd84f2fc..820ed05b27 100644 --- a/setup.c +++ b/setup.c @@ -126,6 +126,101 @@ void verify_non_filename(const char *prefix, const char *arg) "Use '--' to separate filenames from revisions", arg); } +/* + * Magic pathspec + * + * NEEDSWORK: These need to be moved to dir.h or even to a new + * pathspec.h when we restructure get_pathspec() users to use the + * "struct pathspec" interface. + * + * Possible future magic semantics include stuff like: + * + * { PATHSPEC_NOGLOB, '!', "noglob" }, + * { PATHSPEC_ICASE, '\0', "icase" }, + * { PATHSPEC_RECURSIVE, '*', "recursive" }, + * { PATHSPEC_REGEXP, '\0', "regexp" }, + * + */ +#define PATHSPEC_FROMTOP (1<<0) + +struct pathspec_magic { + unsigned bit; + char mnemonic; /* this cannot be ':'! */ + const char *name; +} pathspec_magic[] = { + { PATHSPEC_FROMTOP, '/', "top" }, +}; + +/* + * Take an element of a pathspec and check for magic signatures. + * Append the result to the prefix. + * + * For now, we only parse the syntax and throw out anything other than + * "top" magic. + * + * NEEDSWORK: This needs to be rewritten when we start migrating + * get_pathspec() users to use the "struct pathspec" interface. For + * example, a pathspec element may be marked as case-insensitive, but + * the prefix part must always match literally, and a single stupid + * string cannot express such a case. + */ +const char *prefix_pathspec(const char *prefix, int prefixlen, const char *elt) +{ + unsigned magic = 0; + const char *copyfrom = elt; + int i; + + if (elt[0] != ':') { + ; /* nothing to do */ + } else if (elt[1] == '(') { + /* longhand */ + const char *nextat; + for (copyfrom = elt + 2; + *copyfrom && *copyfrom != ')'; + copyfrom = nextat) { + size_t len = strcspn(copyfrom, ",)"); + if (copyfrom[len] == ')') + nextat = copyfrom + len; + else + nextat = copyfrom + len + 1; + if (!len) + continue; + for (i = 0; i < ARRAY_SIZE(pathspec_magic); i++) + if (strlen(pathspec_magic[i].name) == len && + !strncmp(pathspec_magic[i].name, copyfrom, len)) { + magic |= pathspec_magic[i].bit; + break; + } + if (ARRAY_SIZE(pathspec_magic) <= i) + die("Invalid pathspec magic '%.*s' in '%s'", + (int) len, copyfrom, elt); + } + if (*copyfrom == ')') + copyfrom++; + } else { + /* shorthand */ + for (copyfrom = elt + 1; + *copyfrom && *copyfrom != ':'; + copyfrom++) { + char ch = *copyfrom; + for (i = 0; i < ARRAY_SIZE(pathspec_magic); i++) + if (pathspec_magic[i].mnemonic == ch) { + magic |= pathspec_magic[i].bit; + break; + } + if (ARRAY_SIZE(pathspec_magic) <= i) + break; + } + if (*copyfrom == ':') + copyfrom++; + } + + if (magic & PATHSPEC_FROMTOP) + return xstrdup(copyfrom); + else + return prefix_path(prefix, prefixlen, copyfrom); +} + const char **get_pathspec(const char *prefix, const char **pathspec) { const char *entry = *pathspec; @@ -147,8 +242,7 @@ const char **get_pathspec(const char *prefix, const char **pathspec) dst = pathspec; prefixlen = prefix ? strlen(prefix) : 0; while (*src) { - const char *p = prefix_path(prefix, prefixlen, *src); - *(dst++) = p; + *(dst++) = prefix_pathspec(prefix, prefixlen, *src); src++; } *dst = NULL; From 2f6c9760debfb4705f6efb5862e2b3a23b2b951c Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Fri, 8 Apr 2011 16:18:46 -0700 Subject: [PATCH 2/3] magic pathspec: futureproof shorthand form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The earlier design was to take whatever non-alnum that the short format parser happens to support, leaving the rest as part of the pattern, so a version of git that knows '*' magic and a version that does not would have behaved differently when given ":*Makefile". The former would have applied the '*' magic to the pattern "Makefile", while the latter would used no magic to the pattern "*Makefile". Instead, just reserve all non-alnum ASCII letters that are neither glob nor regexp special as potential magic signature, and when we see a magic that is not supported, die with an error message, just like the longhand codepath does. With this, ":%#!*Makefile" will always mean "%#!" magic applied to the pattern "*Makefile", no matter what version of git is used (it is a different matter if the version of git supports all of these three magic matching rules). Also make ':' without anything else to mean "there is no pathspec". This would allow differences between "git log" and "git log ." run from the top level of the working tree (the latter simplifies no-op commits away from the history) to be expressed from a subdirectory by saying "git log :". Helped-by: Nguyễn Thái Ngọc Duy Signed-off-by: Junio C Hamano --- ctype.c | 15 ++++++++------- git-compat-util.h | 2 ++ setup.c | 9 ++++++++- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/ctype.c b/ctype.c index de600279ee..b5d856fd26 100644 --- a/ctype.c +++ b/ctype.c @@ -10,17 +10,18 @@ enum { A = GIT_ALPHA, D = GIT_DIGIT, G = GIT_GLOB_SPECIAL, /* *, ?, [, \\ */ - R = GIT_REGEX_SPECIAL /* $, (, ), +, ., ^, {, | */ + R = GIT_REGEX_SPECIAL, /* $, (, ), +, ., ^, {, | */ + P = GIT_PATHSPEC_MAGIC /* other non-alnum, except for ] and } */ }; unsigned char sane_ctype[256] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, S, S, 0, 0, S, 0, 0, /* 0.. 15 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 16.. 31 */ - S, 0, 0, 0, R, 0, 0, 0, R, R, G, R, 0, 0, R, 0, /* 32.. 47 */ - D, D, D, D, D, D, D, D, D, D, 0, 0, 0, 0, 0, G, /* 48.. 63 */ - 0, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, /* 64.. 79 */ - A, A, A, A, A, A, A, A, A, A, A, G, G, 0, R, 0, /* 80.. 95 */ - 0, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, /* 96..111 */ - A, A, A, A, A, A, A, A, A, A, A, R, R, 0, 0, 0, /* 112..127 */ + S, P, P, P, R, P, P, P, R, R, G, R, P, P, R, P, /* 32.. 47 */ + D, D, D, D, D, D, D, D, D, D, P, P, P, P, P, G, /* 48.. 63 */ + P, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, /* 64.. 79 */ + A, A, A, A, A, A, A, A, A, A, A, G, G, 0, R, P, /* 80.. 95 */ + P, A, A, A, A, A, A, A, A, A, A, A, A, A, A, A, /* 96..111 */ + A, A, A, A, A, A, A, A, A, A, A, R, R, 0, P, 0, /* 112..127 */ /* Nothing in the 128.. range */ }; diff --git a/git-compat-util.h b/git-compat-util.h index 49b50eec86..d88cf8afb6 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -462,6 +462,7 @@ extern unsigned char sane_ctype[256]; #define GIT_ALPHA 0x04 #define GIT_GLOB_SPECIAL 0x08 #define GIT_REGEX_SPECIAL 0x10 +#define GIT_PATHSPEC_MAGIC 0x20 #define sane_istest(x,mask) ((sane_ctype[(unsigned char)(x)] & (mask)) != 0) #define isascii(x) (((x) & ~0x7f) == 0) #define isspace(x) sane_istest(x,GIT_SPACE) @@ -472,6 +473,7 @@ extern unsigned char sane_ctype[256]; #define is_regex_special(x) sane_istest(x,GIT_GLOB_SPECIAL | GIT_REGEX_SPECIAL) #define tolower(x) sane_case((unsigned char)(x), 0x20) #define toupper(x) sane_case((unsigned char)(x), 0) +#define is_pathspec_magic(x) sane_istest(x,GIT_PATHSPEC_MAGIC) static inline int sane_case(int x, int high) { diff --git a/setup.c b/setup.c index 820ed05b27..5048252d78 100644 --- a/setup.c +++ b/setup.c @@ -197,19 +197,26 @@ const char *prefix_pathspec(const char *prefix, int prefixlen, const char *elt) } if (*copyfrom == ')') copyfrom++; + } else if (!elt[1]) { + /* Just ':' -- no element! */ + return NULL; } else { /* shorthand */ for (copyfrom = elt + 1; *copyfrom && *copyfrom != ':'; copyfrom++) { char ch = *copyfrom; + + if (!is_pathspec_magic(ch)) + break; for (i = 0; i < ARRAY_SIZE(pathspec_magic); i++) if (pathspec_magic[i].mnemonic == ch) { magic |= pathspec_magic[i].bit; break; } if (ARRAY_SIZE(pathspec_magic) <= i) - break; + die("Unimplemented pathspec magic '%c' in '%s'", + ch, elt); } if (*copyfrom == ':') copyfrom++; From d0546e2d488b1ba185c430b638619ab1d91af509 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Wed, 6 Apr 2011 20:56:19 -0700 Subject: [PATCH 3/3] magic pathspec: add ":(icase)path" to match case insensitively Signed-off-by: Junio C Hamano --- Documentation/glossary-content.txt | 7 +++++-- setup.c | 31 ++++++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/Documentation/glossary-content.txt b/Documentation/glossary-content.txt index e51d7e60eb..0ca029b738 100644 --- a/Documentation/glossary-content.txt +++ b/Documentation/glossary-content.txt @@ -319,10 +319,13 @@ top `/`;; The magic word `top` (mnemonic: `/`) makes the pattern match from the root of the working tree, even when you are running the command from inside a subdirectory. +icase;; + The magic word `icase` (there is no mnemonic for it) makes the + pattern match case insensitively. E.g. `:(icase)makefile` matches + both `Makefile` and `makefile`. -- + -Currently only the slash `/` is recognized as the "magic signature", -but it is envisioned that we will support more types of magic in later +It is envisioned that we will support more types of magic in later versions of git. [[def_parent]]parent:: diff --git a/setup.c b/setup.c index 5048252d78..51e354ca07 100644 --- a/setup.c +++ b/setup.c @@ -136,12 +136,12 @@ void verify_non_filename(const char *prefix, const char *arg) * Possible future magic semantics include stuff like: * * { PATHSPEC_NOGLOB, '!', "noglob" }, - * { PATHSPEC_ICASE, '\0', "icase" }, * { PATHSPEC_RECURSIVE, '*', "recursive" }, * { PATHSPEC_REGEXP, '\0', "regexp" }, * */ #define PATHSPEC_FROMTOP (1<<0) +#define PATHSPEC_ICASE (1<<1) struct pathspec_magic { unsigned bit; @@ -149,6 +149,7 @@ struct pathspec_magic { const char *name; } pathspec_magic[] = { { PATHSPEC_FROMTOP, '/', "top" }, + { PATHSPEC_ICASE, '\0', "icase" }, }; /* @@ -168,7 +169,8 @@ const char *prefix_pathspec(const char *prefix, int prefixlen, const char *elt) { unsigned magic = 0; const char *copyfrom = elt; - int i; + const char *retval; + int i, free_source = 0; if (elt[0] != ':') { ; /* nothing to do */ @@ -222,10 +224,31 @@ const char *prefix_pathspec(const char *prefix, int prefixlen, const char *elt) copyfrom++; } + if (magic & PATHSPEC_ICASE) { + struct strbuf sb = STRBUF_INIT; + for (i = 0; copyfrom[i]; i++) { + int ch = copyfrom[i]; + if (('a' <= ch && ch <= 'z') || + ('A' <= ch && ch <= 'Z')) { + strbuf_addf(&sb, "[%c%c]", + tolower(ch), toupper(ch)); + } else { + strbuf_addch(&sb, ch); + } + } + if (sb.len) { + free_source = 1; + copyfrom = strbuf_detach(&sb, NULL); + } + } + if (magic & PATHSPEC_FROMTOP) - return xstrdup(copyfrom); + retval = xstrdup(copyfrom); else - return prefix_path(prefix, prefixlen, copyfrom); + retval = prefix_path(prefix, prefixlen, copyfrom); + if (free_source) + free((char *)copyfrom); + return retval; } const char **get_pathspec(const char *prefix, const char **pathspec)