The pathspec mechanism is extended via the new ":(attr:eol=input)pattern/to/match" syntax to filter paths so that it requires paths to not just match the given pattern but also have the specified attrs attached for them to be chosen. Signed-off-by: Stefan Beller <sbeller@xxxxxxxxxx> --- Documentation/glossary-content.txt | 19 ++++ attr.c | 2 +- attr.h | 2 + dir.c | 46 ++++++++++ pathspec.c | 103 ++++++++++++++++++++- pathspec.h | 16 ++++ t/t6134-pathspec-with-labels.sh | 177 +++++++++++++++++++++++++++++++++++++ 7 files changed, 360 insertions(+), 5 deletions(-) create mode 100755 t/t6134-pathspec-with-labels.sh diff --git a/Documentation/glossary-content.txt b/Documentation/glossary-content.txt index cafc284..866e8d8 100644 --- a/Documentation/glossary-content.txt +++ b/Documentation/glossary-content.txt @@ -384,6 +384,25 @@ full pathname may have special meaning: + Glob magic is incompatible with literal magic. +attr;; + Additionally to matching the pathspec, the path must have the + attribute as specified. The syntax for specifying the required + attributes is "`attr: [mode] <attribute name> [=value]`" ++ +Attributes can have 4 states (Set, Unset, Set to a value, unspecified) and +you can query each attribute for certain states. The "`[mode]`" is a special +character to indicate which attribute states are looked for. The following +modes are available: + + - "`+`" the attribute must be set + - "`-`" the attribute must be unset + - "`~`" the attribute must be unspecified + - "`?`" the attribute must not be unspecified, i.e. set, unset or value matches + - an empty "`[mode]`" matches if the attribute is set or has a value + - an empty "`[mode]`" combined with "`[=value]`" matches if the attribute has + the given value. ++ + exclude;; After a path matches any non-exclude pathspec, it will be run through all exclude pathspec (magic signature: `!`). If it diff --git a/attr.c b/attr.c index e0f7965..65cffd8 100644 --- a/attr.c +++ b/attr.c @@ -59,7 +59,7 @@ static unsigned hash_name(const char *name, int namelen) return val; } -static int invalid_attr_name(const char *name, int namelen) +int invalid_attr_name(const char *name, int namelen) { /* * Attribute name cannot begin with '-' and must consist of diff --git a/attr.h b/attr.h index 51ca36d..4bb4848 100644 --- a/attr.h +++ b/attr.h @@ -45,6 +45,8 @@ extern void git_attr_check_append(struct git_attr_check *, const struct git_attr extern void git_attr_check_clear(struct git_attr_check *); extern void git_attr_check_free(struct git_attr_check *); +extern int invalid_attr_name(const char *name, int namelen); + /* * Return the name of the attribute represented by the argument. The * return value is a pointer to a null-delimited string that is part diff --git a/dir.c b/dir.c index 996653b..3141a5a 100644 --- a/dir.c +++ b/dir.c @@ -9,6 +9,7 @@ */ #include "cache.h" #include "dir.h" +#include "attr.h" #include "refs.h" #include "wildmatch.h" #include "pathspec.h" @@ -215,6 +216,48 @@ int within_depth(const char *name, int namelen, return 1; } +static int match_attrs(const char *name, int namelen, + const struct pathspec_item *item) +{ + char *path; + int i; + + path = xmemdupz(name, namelen); + git_check_attr(path, 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 || + match_mode == MATCH_SET_OR_VALUE || + match_mode == MATCH_NOT_UNSPECIFIED; + } else if (ATTR_FALSE(value)) { + matched = match_mode == MATCH_UNSET || + match_mode == MATCH_NOT_UNSPECIFIED; + } else if (ATTR_UNSET(value)) { + matched = match_mode == MATCH_UNSPECIFIED; + } else { + matched = match_mode == MATCH_NOT_UNSPECIFIED || + match_mode == MATCH_SET_OR_VALUE || + (match_mode == MATCH_VALUE && + !strcmp(item->attr_match[i].value, value)); + } + if (!matched) + return 0; + } + + free(path); + + return 1; +} + #define DO_MATCH_EXCLUDE 1 #define DO_MATCH_DIRECTORY 2 @@ -270,6 +313,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 4dff252..32fb6a8 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,12 +89,82 @@ static void prefix_short_magic(struct strbuf *sb, int prefixlen, strbuf_addf(sb, ",prefix:%d)", prefixlen); } +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)) + goto err; + + string_list_split(&list, value, ' ', -1); + string_list_remove_empty_items(&list, 0); + + if (!item->attr_check) + item->attr_check = git_attr_check_alloc(); + + ALLOC_GROW(item->attr_match, item->attr_match_nr + list.nr, item->attr_match_alloc); + + for_each_string_list_item(si, &list) { + size_t val_len; + + int j = item->attr_match_nr++; + const char *val = si->string; + struct attr_match *am = &item->attr_match[j]; + + if (val[0] == '?') + am->match_mode = MATCH_NOT_UNSPECIFIED; + else if (val[0] == '~') + am->match_mode = MATCH_UNSPECIFIED; + else if (val[0] == '+') + am->match_mode = MATCH_SET; + else if (val[0] == '-') + am->match_mode = MATCH_UNSET; + else + am->match_mode = MATCH_SET_OR_VALUE; + + if (am->match_mode != MATCH_SET_OR_VALUE) + /* skip first character */ + val++; + + val_len = strcspn(val, "=,)"); + if (val[val_len] == '=') { + am->match_mode = MATCH_VALUE; + am->value = xstrdup(&val[val_len + 1]); + /* + * NEEDSWORK: + * Do we want to allow escaped commas to search + * for comma separated values? + */ + if (strchr(am->value, '\\')) + die(_("attr spec values must not contain backslashes")); + } else + am->value = NULL; + + if (invalid_attr_name(val, val_len)) { + am->match_mode = INVALID_ATTR; + goto err; + } + + am->attr = git_attr(xmemdupz(val, val_len)); + git_attr_check_append(item->attr_check, am->attr); + } + + string_list_clear(&list, 0); + return; +err: + die(_("attr spec '%s': attrs must not start with '-' and " + "be composed of [-A-Za-z0-9_.]."), value); +} + 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; @@ -108,15 +179,21 @@ static void eat_long_magic(struct pathspec_item *item, const char *elt, if (!len) continue; - if (starts_with(copyfrom, "prefix:")) { + if (skip_prefix(copyfrom, "prefix:", &body)) { char *endptr; - *pathspec_prefix = strtol(copyfrom + 7, - &endptr, 10); + *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 *pass = xmemdupz(body, len - strlen("attr:")); + parse_pathspec_attr_match(item, pass); + free(pass); + continue; + } + for (i = 0; i < ARRAY_SIZE(pathspec_magic); i++) { if (strlen(pathspec_magic[i].name) == len && !strncmp(pathspec_magic[i].name, copyfrom, len)) { @@ -425,7 +502,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); @@ -447,6 +527,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) @@ -502,6 +589,14 @@ void copy_pathspec(struct pathspec *dst, const struct pathspec *src) void free_pathspec(struct pathspec *pathspec) { + int i, j; + for (i = 0; i < pathspec->nr; i++) { + 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 0c11262..5308137 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; }; diff --git a/t/t6134-pathspec-with-labels.sh b/t/t6134-pathspec-with-labels.sh new file mode 100755 index 0000000..35b3ab2 --- /dev/null +++ b/t/t6134-pathspec-with-labels.sh @@ -0,0 +1,177 @@ +#!/bin/sh + +test_description='test labels in pathspecs' +. ./test-lib.sh + +test_expect_success 'setup a tree' ' + mkdir sub && + for p in fileA fileB fileC fileAB fileAC fileBC fileNoLabel fileUnsetLabel fileSetLabel fileValue fileWrongLabel; do + : >$p && + git add $p && + : >sub/$p + git add sub/$p + done && + git commit -m $p && + git ls-files >actual && + 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 + test_cmp expect actual +' + +test_expect_success 'pathspec with no attr' ' + test_must_fail git ls-files ":(attr:)" 2>actual && + test_i18ngrep fatal actual +' + +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" +' + +sq="'" + +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 set or value attr' ' + cat <<EOF >expect && +fileSetLabel +fileValue +sub/fileSetLabel +sub/fileValue +EOF + git ls-files ":(attr:label)" >actual && + test_cmp expect actual +' + +test_expect_success 'check unspecified attr' ' + cat <<EOF >expect && +.gitattributes +fileC +fileNoLabel +fileWrongLabel +sub/fileC +sub/fileNoLabel +sub/fileWrongLabel +EOF + git ls-files ":(attr:~label,attr:~labelA,attr:~labelB)" >actual && + test_cmp expect actual +' + +test_expect_success 'check not unspecified attr' ' + cat <<EOF >expect && +fileSetLabel +fileUnsetLabel +fileValue +sub/fileSetLabel +sub/fileUnsetLabel +sub/fileValue +EOF + git ls-files ":(attr:?label)" >actual && + test_cmp expect actual +' + +test_expect_success 'check label with 2 labels' ' + cat <<EOF >expect && +fileAB +sub/fileAB +EOF + git ls-files ":(attr:labelA labelB)" >actual && + test_cmp expect actual && + git ls-files ":(attr:labelA,attr:labelB)" >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:☺)" 2>actual && + test_i18ngrep "fatal" actual +' + +test_done -- 2.8.2.121.ga97fb08 -- To unsubscribe from this list: send the line "unsubscribe git" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html