diff options
author | Junio C Hamano <gitster@pobox.com> | 2016-06-20 14:10:39 -0700 |
---|---|---|
committer | Junio C Hamano <gitster@pobox.com> | 2016-06-20 14:10:39 -0700 |
commit | 0840c49c2843d732e1e2fb2756595389c21ad5cb (patch) | |
tree | 5a1d82aa27dce2671bd6236024d508593b576e76 | |
parent | f7e7f4dc169032a78b3520363429a152fa0d1832 (diff) | |
parent | d8e47e7d5c62e46c517cd56737f8fcb55ccf4c8f (diff) | |
download | git-0840c49c2843d732e1e2fb2756595389c21ad5cb.tar.gz |
Merge branch 'sb/pathspec-label' into sb/submodule-default-paths
* sb/pathspec-label:
pathspec: disable preload-index when attribute pathspec magic is in use
pathspec: allow escaped query values
pathspec: allow querying for attributes
pathspec: move prefix check out of the inner loop
pathspec: move long magic parsing out of prefix_pathspec
Documentation: fix a typo
-rw-r--r-- | Documentation/gitattributes.txt | 2 | ||||
-rw-r--r-- | Documentation/glossary-content.txt | 20 | ||||
-rw-r--r-- | dir.c | 35 | ||||
-rw-r--r-- | pathspec.c | 235 | ||||
-rw-r--r-- | pathspec.h | 18 | ||||
-rw-r--r-- | preload-index.c | 4 | ||||
-rwxr-xr-x | t/t6134-pathspec-with-labels.sh | 176 |
7 files changed, 451 insertions, 39 deletions
diff --git a/Documentation/gitattributes.txt b/Documentation/gitattributes.txt index a18804c657..0705cf2e4e 100644 --- a/Documentation/gitattributes.txt +++ b/Documentation/gitattributes.txt @@ -88,7 +88,7 @@ is either not set or empty, $HOME/.config/git/attributes is used instead. Attributes for all users on a system should be placed in the `$(prefix)/etc/gitattributes` file. -Sometimes you would need to override an setting of an attribute +Sometimes you would need to override a setting of an attribute for a path to `Unspecified` state. This can be done by listing the name of the attribute prefixed with an exclamation point `!`. diff --git a/Documentation/glossary-content.txt b/Documentation/glossary-content.txt index 8ad29e61a9..f90bd45395 100644 --- a/Documentation/glossary-content.txt +++ b/Documentation/glossary-content.txt @@ -384,6 +384,26 @@ full pathname may have special meaning: + Glob magic is incompatible with literal magic. +attr;; +After `attr:` comes a space separated list of "attribute +requirements", all of which must be met in order for the +path to be considered a match; this is in addition to the +usual non-magic pathspec pattern matching. ++ +Each of the attribute requirements for the path takes one of +these forms: + +- "`ATTR`" requires that the attribute `ATTR` must be set. + +- "`-ATTR`" requires that the attribute `ATTR` must be unset. + +- "`ATTR=VALUE`" requires that the attribute `ATTR` must be + set to the string `VALUE`. + +- "`!ATTR`" requires that the attribute `ATTR` must be + unspecified. ++ + exclude;; After a path matches any non-exclude pathspec, it will be run through all exclude pathspec (magic signature: `!`). If it @@ -9,6 +9,7 @@ */ #include "cache.h" #include "dir.h" +#include "attr.h" #include "refs.h" #include "wildmatch.h" #include "pathspec.h" @@ -207,6 +208,37 @@ int within_depth(const char *name, int namelen, return 1; } +static int match_attrs(const char *name, int namelen, + const struct pathspec_item *item) +{ + int i; + + git_check_attr_counted(name, namelen, item->attr_check); + for (i = 0; i < item->attr_match_nr; i++) { + const char *value; + int matched; + enum attr_match_mode match_mode; + + value = item->attr_check->check[i].value; + match_mode = item->attr_match[i].match_mode; + + if (ATTR_TRUE(value)) + matched = (match_mode == MATCH_SET); + else if (ATTR_FALSE(value)) + matched = (match_mode == MATCH_UNSET); + else if (ATTR_UNSET(value)) + matched = (match_mode == MATCH_UNSPECIFIED); + else + matched = (match_mode == MATCH_VALUE && + !strcmp(item->attr_match[i].value, value)); + + if (!matched) + return 0; + } + + return 1; +} + #define DO_MATCH_EXCLUDE 1 #define DO_MATCH_DIRECTORY 2 @@ -262,6 +294,9 @@ static int match_pathspec_item(const struct pathspec_item *item, int prefix, strncmp(item->match, name - prefix, item->prefix)) return 0; + if (item->attr_match_nr && !match_attrs(name, namelen, item)) + return 0; + /* If the match was just the prefix, we matched */ if (!*match) return MATCHED_RECURSIVELY; diff --git a/pathspec.c b/pathspec.c index 24e0dd5232..c94327fb44 100644 --- a/pathspec.c +++ b/pathspec.c @@ -1,6 +1,7 @@ #include "cache.h" #include "dir.h" #include "pathspec.h" +#include "attr.h" /* * Finds which of the given pathspecs match items in the index. @@ -88,6 +89,180 @@ static void prefix_short_magic(struct strbuf *sb, int prefixlen, strbuf_addf(sb, ",prefix:%d)", prefixlen); } +static size_t strcspn_escaped(const char *s, const char *stop) +{ + const char *i; + + for (i = s; *i; i++) { + /* skip the escaped character */ + if (i[0] == '\\' && i[1]) { + i++; + continue; + } + + if (strchr(stop, *i)) + break; + } + return i - s; +} + +static inline int invalid_value_char(const char ch) +{ + if (isalnum(ch) || strchr(",-_", ch)) + return 0; + return -1; +} + +static char *attr_value_unescape(const char *value) +{ + const char *src; + char *dst, *ret; + + ret = xmallocz(strlen(value)); + for (src = value, dst = ret; *src; src++, dst++) { + if (*src == '\\') { + if (!src[1]) + die(_("Escape character '\\' not allowed as " + "last character in attr value")); + src++; + } + if (invalid_value_char(*src)) + die("cannot use '%c' for value matching", *src); + *dst = *src; + } + *dst = '\0'; + return ret; +} + +static void parse_pathspec_attr_match(struct pathspec_item *item, const char *value) +{ + struct string_list_item *si; + struct string_list list = STRING_LIST_INIT_DUP; + + if (!value || !strlen(value)) + die(_("attr spec must not be empty")); + + string_list_split(&list, value, ' ', -1); + string_list_remove_empty_items(&list, 0); + + if (!item->attr_check) + item->attr_check = git_attr_check_alloc(); + else + die(_("Only one 'attr:' specification is allowed.")); + + ALLOC_GROW(item->attr_match, item->attr_match_nr + list.nr, item->attr_match_alloc); + + for_each_string_list_item(si, &list) { + size_t attr_len; + + int j = item->attr_match_nr++; + const char *attr = si->string; + struct attr_match *am = &item->attr_match[j]; + + switch (*attr) { + case '!': + am->match_mode = MATCH_UNSPECIFIED; + attr++; + attr_len = strlen(attr); + break; + case '-': + am->match_mode = MATCH_UNSET; + attr++; + attr_len = strlen(attr); + break; + default: + attr_len = strcspn(attr, "="); + if (attr[attr_len] != '=') + am->match_mode = MATCH_SET; + else { + const char *v = &attr[attr_len + 1]; + am->match_mode = MATCH_VALUE; + am->value = attr_value_unescape(v); + } + break; + } + + am->attr = git_attr_counted(attr, attr_len); + if (!am->attr) { + struct strbuf sb = STRBUF_INIT; + am->match_mode = INVALID_ATTR; + invalid_attr_name_message(&sb, attr, attr_len); + die(_("invalid attribute in '%s': '%s'"), value, sb.buf); + } + + git_attr_check_append(item->attr_check, am->attr); + } + + string_list_clear(&list, 0); + return; +} + +static void eat_long_magic(struct pathspec_item *item, const char *elt, + unsigned *magic, int *pathspec_prefix, + const char **copyfrom_, const char **long_magic_end) +{ + int i; + const char *copyfrom = *copyfrom_; + const char *body; + /* longhand */ + const char *nextat; + for (copyfrom = elt + 2; + *copyfrom && *copyfrom != ')'; + copyfrom = nextat) { + size_t len = strcspn_escaped(copyfrom, ",)"); + if (copyfrom[len] == ',') + nextat = copyfrom + len + 1; + else + /* handle ')' and '\0' */ + nextat = copyfrom + len; + if (!len) + continue; + + if (skip_prefix(copyfrom, "prefix:", &body)) { + char *endptr; + *pathspec_prefix = strtol(body, &endptr, 10); + if (endptr - copyfrom != len) + die(_("invalid parameter for pathspec magic 'prefix'")); + continue; + } + + if (skip_prefix(copyfrom, "attr:", &body)) { + char *attr_body = xmemdupz(body, len - strlen("attr:")); + parse_pathspec_attr_match(item, attr_body); + free(attr_body); + 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 != ')') + die(_("Missing ')' at the end of pathspec magic in '%s'"), elt); + *long_magic_end = copyfrom; + copyfrom++; + *copyfrom_ = copyfrom; +} + +int pathspec_is_non_threadable(const struct pathspec *pathspec) +{ + int i; + + for (i = 0; i < pathspec->nr; i++) { + const struct pathspec_item *item = &pathspec->items[i]; + if (item->attr_check) + return 1; + } + return 0; +} + /* * Take an element of a pathspec and check for magic signatures. * Append the result to the prefix. Return the magic bitmap. @@ -150,43 +325,7 @@ static unsigned prefix_pathspec(struct pathspec_item *item, (flags & PATHSPEC_LITERAL_PATH)) { ; /* 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 + 1; - else - /* handle ')' and '\0' */ - nextat = copyfrom + len; - 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 (starts_with(copyfrom, "prefix:")) { - char *endptr; - pathspec_prefix = strtol(copyfrom + 7, - &endptr, 10); - if (endptr - copyfrom != len) - die(_("invalid parameter for pathspec magic 'prefix'")); - /* "i" would be wrong, but it does not matter */ - break; - } - } - if (ARRAY_SIZE(pathspec_magic) <= i) - die(_("Invalid pathspec magic '%.*s' in '%s'"), - (int) len, copyfrom, elt); - } - if (*copyfrom != ')') - die(_("Missing ')' at the end of pathspec magic in '%s'"), elt); - long_magic_end = copyfrom; - copyfrom++; + eat_long_magic(item, elt, &magic, &pathspec_prefix, ©from, &long_magic_end); } else { /* shorthand */ for (copyfrom = elt + 1; @@ -414,7 +553,10 @@ void parse_pathspec(struct pathspec *pathspec, for (i = 0; i < n; i++) { unsigned short_magic; entry = argv[i]; - + item[i].attr_check = NULL; + item[i].attr_match = NULL; + item[i].attr_match_nr = 0; + item[i].attr_match_alloc = 0; item[i].magic = prefix_pathspec(item + i, &short_magic, argv + i, flags, prefix, prefixlen, entry); @@ -436,6 +578,13 @@ void parse_pathspec(struct pathspec *pathspec, if (item[i].nowildcard_len < item[i].len) pathspec->has_wildcard = 1; pathspec->magic |= item[i].magic; + + if (item[i].attr_match_nr) { + int j; + for (j = 0; j < item[i].attr_match_nr; j++) + if (item[i].attr_match[j].match_mode == INVALID_ATTR) + die(_("attribute spec in the wrong syntax are prohibited.")); + } } if (nr_exclude == n) @@ -491,6 +640,16 @@ void copy_pathspec(struct pathspec *dst, const struct pathspec *src) void clear_pathspec(struct pathspec *pathspec) { + int i, j; + for (i = 0; i < pathspec->nr; i++) { + if (!pathspec->items[i].attr_match_nr) + continue; + for (j = 0; j < pathspec->items[j].attr_match_nr; j++) + free(pathspec->items[i].attr_match[j].value); + free(pathspec->items[i].attr_match); + git_attr_check_free(pathspec->items[i].attr_check); + } + free(pathspec->items); pathspec->items = NULL; } diff --git a/pathspec.h b/pathspec.h index 4a80f6fc96..b25181a0d0 100644 --- a/pathspec.h +++ b/pathspec.h @@ -32,6 +32,22 @@ struct pathspec { int len, prefix; int nowildcard_len; int flags; + int attr_match_nr; + int attr_match_alloc; + struct attr_match { + struct git_attr *attr; + char *value; + enum attr_match_mode { + MATCH_SET, + MATCH_UNSET, + MATCH_VALUE, + MATCH_UNSPECIFIED, + MATCH_NOT_UNSPECIFIED, + MATCH_SET_OR_VALUE, + INVALID_ATTR + } match_mode; + } *attr_match; + struct git_attr_check *attr_check; } *items; }; @@ -99,4 +115,6 @@ extern void add_pathspec_matches_against_index(const struct pathspec *pathspec, extern const char *check_path_for_gitlink(const char *path); extern void die_if_path_beyond_symlink(const char *path, const char *prefix); +extern int pathspec_is_non_threadable(const struct pathspec *); + #endif /* PATHSPEC_H */ diff --git a/preload-index.c b/preload-index.c index c1fe3a3ef9..af468789f5 100644 --- a/preload-index.c +++ b/preload-index.c @@ -76,6 +76,10 @@ static void preload_index(struct index_state *index, if (!core_preload_index) return; + /* Do not preload when pathspec uses non-threadable subsystems */ + if (pathspec && pathspec_is_non_threadable(pathspec)) + return; /* for now ... */ + threads = index->cache_nr / THREAD_COST; if (threads < 2) return; diff --git a/t/t6134-pathspec-with-labels.sh b/t/t6134-pathspec-with-labels.sh new file mode 100755 index 0000000000..f1e355f657 --- /dev/null +++ b/t/t6134-pathspec-with-labels.sh @@ -0,0 +1,176 @@ +#!/bin/sh + +test_description='test labels in pathspecs' +. ./test-lib.sh + +test_expect_success 'setup a tree' ' + cat <<-EOF >expect && + fileA + fileAB + fileAC + fileB + fileBC + fileC + fileNoLabel + fileSetLabel + fileUnsetLabel + fileValue + fileWrongLabel + sub/fileA + sub/fileAB + sub/fileAC + sub/fileB + sub/fileBC + sub/fileC + sub/fileNoLabel + sub/fileSetLabel + sub/fileUnsetLabel + sub/fileValue + sub/fileWrongLabel + EOF + mkdir sub && + while read path + do + : >$path && + git add $path || return 1 + done <expect && + git commit -m "initial commit" && + git ls-files >actual && + test_cmp expect actual +' + +test_expect_success 'pathspec with no attr' ' + test_must_fail git ls-files ":(attr:)" +' + +test_expect_success 'pathspec with labels and non existent .gitattributes' ' + git ls-files ":(attr:label)" >actual && + test_must_be_empty actual +' + +test_expect_success 'setup .gitattributes' ' + cat <<-EOF >.gitattributes && + fileA labelA + fileB labelB + fileC labelC + fileAB labelA labelB + fileAC labelA labelC + fileBC labelB labelC + fileUnsetLabel -label + fileSetLabel label + fileValue label=foo + fileWrongLabel label☺ + EOF + git add .gitattributes && + git commit -m "add attributes" +' + +test_expect_success 'check specific set attr' ' + cat <<-EOF >expect && + fileSetLabel + sub/fileSetLabel + EOF + git ls-files ":(attr:label)" >actual && + test_cmp expect actual +' + +test_expect_success 'check specific unset attr' ' + cat <<-EOF >expect && + fileUnsetLabel + sub/fileUnsetLabel + EOF + git ls-files ":(attr:-label)" >actual && + test_cmp expect actual +' + +test_expect_success 'check specific value attr' ' + cat <<-EOF >expect && + fileValue + sub/fileValue + EOF + git ls-files ":(attr:label=foo)" >actual && + test_cmp expect actual && + git ls-files ":(attr:label=bar)" >actual && + test_must_be_empty actual +' + +test_expect_success 'check unspecified attr' ' + cat <<-EOF >expect && + .gitattributes + fileA + fileAB + fileAC + fileB + fileBC + fileC + fileNoLabel + fileWrongLabel + sub/fileA + sub/fileAB + sub/fileAC + sub/fileB + sub/fileBC + sub/fileC + sub/fileNoLabel + sub/fileWrongLabel + EOF + git ls-files ":(attr:!label)" >actual && + test_cmp expect actual +' + +test_expect_success 'check multiple unspecified attr' ' + cat <<-EOF >expect && + .gitattributes + fileC + fileNoLabel + fileWrongLabel + sub/fileC + sub/fileNoLabel + sub/fileWrongLabel + EOF + git ls-files ":(attr:!labelB !labelA !label)" >actual && + test_cmp expect actual +' + +test_expect_success 'check label with more labels but excluded path' ' + cat <<-EOF >expect && + fileAB + fileB + fileBC + EOF + git ls-files ":(attr:labelB)" ":(exclude)sub/" >actual && + test_cmp expect actual +' + +test_expect_success 'check label excluding other labels' ' + cat <<-EOF >expect && + fileAB + fileB + fileBC + sub/fileAB + sub/fileB + EOF + git ls-files ":(attr:labelB)" ":(exclude,attr:labelC)sub/" >actual && + test_cmp expect actual +' + +test_expect_success 'abort on giving invalid label on the command line' ' + test_must_fail git ls-files . ":(attr:☺)" +' + +test_expect_success 'abort on asking for wrong magic' ' + test_must_fail git ls-files . ":(attr:-label=foo)" && + test_must_fail git ls-files . ":(attr:!label=foo)" +' + +test_expect_success 'check attribute list' ' + cat <<-EOF >>.gitattributes && + * whitespace=indent,trail,space + EOF + cat .gitattributes && + git ls-files ":(attr:whitespace=indent\,trail\,space)" >actual && + git ls-files >expect && + test_cmp expect actual +' + +test_done |