{"thread":{"id":"42403","subject":"[PATCHv9] pathspec: allow querying for attributes","startedAt":"2016-05-20T19:00:34Z","lastAt":"2016-05-20T19:38:46Z","messageCount":2,"participants":["Stefan Beller","Junio C Hamano"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"287126","messageId":"20160520190034.12681-1-sbeller@google.com","threadId":"42403","inReplyTo":null,"subject":"[PATCHv9] pathspec: allow querying for attributes","fromName":"Stefan Beller","fromEmail":"sbeller@google.com","sentAt":"2016-05-20T19:00:34Z","receivedAt":"2016-05-20T19:00:34Z","isPatch":false,"sender":{"key":"stefanbeller@gmail.com","avatar":"https://avatars.githubusercontent.com/u/455868?v=4"},"body":"The pathspec mechanism is extended via the new\n\":(attr:eol=input)pattern/to/match\" syntax to filter paths so that it\nrequires paths to not just match the given pattern but also have the\nspecified attrs attached for them to be chosen.\n\nSigned-off-by: Stefan Beller <sbeller@google.com>\n---\n\nJunio, please replace the last patch of sb/pathspec-label with this one.\n\ndiff to last round:\n#\tdiff --git a/Documentation/glossary-content.txt b/Documentation/glossary-content.txt\n#\tindex e06520b..181f52e 100644\n#\t--- a/Documentation/glossary-content.txt\n#\t+++ b/Documentation/glossary-content.txt\n#\t@@ -389,7 +389,7 @@ After `attr:` comes a space separated list of \"attribute\n#\t requirements\", all of which must be met in order for the\n#\t path to be considered a match; this is in addition to the\n#\t usual non-magic pathspec pattern matching.\n#\t-\n#\t++\n#\t Each of the attribute requirements for the path takes one of\n#\t these forms:\n#\t \n#\tdiff --git a/pathspec.c b/pathspec.c\n#\tindex 693a5e7..0a02255 100644\n#\t--- a/pathspec.c\n#\t+++ b/pathspec.c\n#\t@@ -115,19 +115,19 @@ static void parse_pathspec_attr_match(struct pathspec_item *item, const char *va\n#\t \t\tconst char *attr = si->string;\n#\t \t\tstruct attr_match *am = &item->attr_match[j];\n#\t \n#\t-\t\tattr_len = strcspn(attr, \"=\");\n#\t \t\tswitch (*attr) {\n#\t \t\tcase '!':\n#\t \t\t\tam->match_mode = MATCH_UNSPECIFIED;\n#\t \t\t\tattr++;\n#\t-\t\t\tattr_len--;\n#\t+\t\t\tattr_len = strlen(attr);\n#\t \t\t\tbreak;\n#\t \t\tcase '-':\n#\t \t\t\tam->match_mode = MATCH_UNSET;\n#\t \t\t\tattr++;\n#\t-\t\t\tattr_len--;\n#\t+\t\t\tattr_len = strlen(attr);\n#\t \t\t\tbreak;\n#\t \t\tdefault:\n#\t+\t\t\tattr_len = strcspn(attr, \"=\");\n#\t \t\t\tif (attr[attr_len] != '=')\n#\t \t\t\t\tam->match_mode = MATCH_SET;\n#\t \t\t\telse {\n#\tdiff --git a/t/t6134-pathspec-with-labels.sh b/t/t6134-pathspec-with-labels.sh\n#\tindex 5da1a63..a5c9632 100755\n#\t--- a/t/t6134-pathspec-with-labels.sh\n#\t+++ b/t/t6134-pathspec-with-labels.sh\n#\t@@ -40,8 +40,7 @@ test_expect_success 'setup a tree' '\n#\t '\n#\t \n#\t test_expect_success 'pathspec with no attr' '\n#\t-\ttest_must_fail git ls-files \":(attr:)\" 2>actual &&\n#\t-\ttest_i18ngrep fatal actual\n#\t+\ttest_must_fail git ls-files \":(attr:)\"\n#\t '\n#\t \n#\t test_expect_success 'pathspec with labels and non existent .gitattributes' '\n#\t@@ -156,8 +155,12 @@ test_expect_success 'check label excluding other labels' '\n#\t '\n#\t \n#\t test_expect_success 'abort on giving invalid label on the command line' '\n#\t-\ttest_must_fail git ls-files . \":(attr:☺)\" 2>actual &&\n#\t-\ttest_i18ngrep \"fatal\" actual\n#\t+\ttest_must_fail git ls-files . \":(attr:☺)\"\n#\t+'\n#\t+\n#\t+test_expect_success 'abort on asking for wrong magic' '\n#\t+\ttest_must_fail git ls-files . \":(attr:-label=foo)\" &&\n#\t+\ttest_must_fail git ls-files . \":(attr:!label=foo)\"\n#\t '\n#\t \n#\t test_done\n\n Documentation/glossary-content.txt |  20 +++++\n dir.c                              |  35 ++++++++\n pathspec.c                         | 101 +++++++++++++++++++++-\n pathspec.h                         |  16 ++++\n t/t6134-pathspec-with-labels.sh    | 166 +++++++++++++++++++++++++++++++++++++\n 5 files changed, 334 insertions(+), 4 deletions(-)\n create mode 100755 t/t6134-pathspec-with-labels.sh\n\ndiff --git a/Documentation/glossary-content.txt b/Documentation/glossary-content.txt\nindex cafc284..181f52e 100644\n--- a/Documentation/glossary-content.txt\n+++ b/Documentation/glossary-content.txt\n@@ -384,6 +384,26 @@ full pathname may have special meaning:\n +\n Glob magic is incompatible with literal magic.\n \n+attr;;\n+After `attr:` comes a space separated list of \"attribute\n+requirements\", all of which must be met in order for the\n+path to be considered a match; this is in addition to the\n+usual non-magic pathspec pattern matching.\n++\n+Each of the attribute requirements for the path takes one of\n+these forms:\n+\n+- \"`ATTR`\" requires that the attribute `ATTR` must be set.\n+\n+- \"`-ATTR`\" requires that the attribute `ATTR` must be unset.\n+\n+- \"`ATTR=VALUE`\" requires that the attribute `ATTR` must be\n+  set to the string `VALUE`.\n+\n+- \"`!ATTR`\" requires that the attribute `ATTR` must be\n+  unspecified.\n++\n+\n exclude;;\n \tAfter a path matches any non-exclude pathspec, it will be run\n \tthrough all exclude pathspec (magic signature: `!`). If it\ndiff --git a/dir.c b/dir.c\nindex 996653b..fc071af 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -9,6 +9,7 @@\n  */\n #include \"cache.h\"\n #include \"dir.h\"\n+#include \"attr.h\"\n #include \"refs.h\"\n #include \"wildmatch.h\"\n #include \"pathspec.h\"\n@@ -215,6 +216,37 @@ int within_depth(const char *name, int namelen,\n \treturn 1;\n }\n \n+static int match_attrs(const char *name, int namelen,\n+\t\t       const struct pathspec_item *item)\n+{\n+\tint i;\n+\n+\tgit_check_attr_counted(name, namelen, item->attr_check);\n+\tfor (i = 0; i < item->attr_match_nr; i++) {\n+\t\tconst char *value;\n+\t\tint matched;\n+\t\tenum attr_match_mode match_mode;\n+\n+\t\tvalue = item->attr_check->check[i].value;\n+\t\tmatch_mode = item->attr_match[i].match_mode;\n+\n+\t\tif (ATTR_TRUE(value))\n+\t\t\tmatched = (match_mode == MATCH_SET);\n+\t\telse if (ATTR_FALSE(value))\n+\t\t\tmatched = (match_mode == MATCH_UNSET);\n+\t\telse if (ATTR_UNSET(value))\n+\t\t\tmatched = (match_mode == MATCH_UNSPECIFIED);\n+\t\telse\n+\t\t\tmatched = (match_mode == MATCH_VALUE &&\n+\t\t\t\t   !strcmp(item->attr_match[i].value, value));\n+\n+\t\tif (!matched)\n+\t\t\treturn 0;\n+\t}\n+\n+\treturn 1;\n+}\n+\n #define DO_MATCH_EXCLUDE   1\n #define DO_MATCH_DIRECTORY 2\n \n@@ -270,6 +302,9 @@ static int match_pathspec_item(const struct pathspec_item *item, int prefix,\n \t    strncmp(item->match, name - prefix, item->prefix))\n \t\treturn 0;\n \n+\tif (item->attr_match_nr && !match_attrs(name, namelen, item))\n+\t\treturn 0;\n+\n \t/* If the match was just the prefix, we matched */\n \tif (!*match)\n \t\treturn MATCHED_RECURSIVELY;\ndiff --git a/pathspec.c b/pathspec.c\nindex 4dff252..0a02255 100644\n--- a/pathspec.c\n+++ b/pathspec.c\n@@ -1,6 +1,7 @@\n #include \"cache.h\"\n #include \"dir.h\"\n #include \"pathspec.h\"\n+#include \"attr.h\"\n \n /*\n  * Finds which of the given pathspecs match items in the index.\n@@ -88,12 +89,78 @@ static void prefix_short_magic(struct strbuf *sb, int prefixlen,\n \tstrbuf_addf(sb, \",prefix:%d)\", prefixlen);\n }\n \n+static void parse_pathspec_attr_match(struct pathspec_item *item, const char *value)\n+{\n+\tstruct string_list_item *si;\n+\tstruct string_list list = STRING_LIST_INIT_DUP;\n+\n+\n+\tif (!value || !strlen(value))\n+\t\tdie(_(\"attr spec must not be empty\"));\n+\n+\tstring_list_split(&list, value, ' ', -1);\n+\tstring_list_remove_empty_items(&list, 0);\n+\n+\tif (!item->attr_check)\n+\t\titem->attr_check = git_attr_check_alloc();\n+\telse\n+\t\tdie(_(\"Only one 'attr:' specification is allowed.\"));\n+\n+\tALLOC_GROW(item->attr_match, item->attr_match_nr + list.nr, item->attr_match_alloc);\n+\n+\tfor_each_string_list_item(si, &list) {\n+\t\tsize_t attr_len;\n+\n+\t\tint j = item->attr_match_nr++;\n+\t\tconst char *attr = si->string;\n+\t\tstruct attr_match *am = &item->attr_match[j];\n+\n+\t\tswitch (*attr) {\n+\t\tcase '!':\n+\t\t\tam->match_mode = MATCH_UNSPECIFIED;\n+\t\t\tattr++;\n+\t\t\tattr_len = strlen(attr);\n+\t\t\tbreak;\n+\t\tcase '-':\n+\t\t\tam->match_mode = MATCH_UNSET;\n+\t\t\tattr++;\n+\t\t\tattr_len = strlen(attr);\n+\t\t\tbreak;\n+\t\tdefault:\n+\t\t\tattr_len = strcspn(attr, \"=\");\n+\t\t\tif (attr[attr_len] != '=')\n+\t\t\t\tam->match_mode = MATCH_SET;\n+\t\t\telse {\n+\t\t\t\tam->match_mode = MATCH_VALUE;\n+\t\t\t\tam->value = xstrdup(&attr[attr_len + 1]);\n+\t\t\t\tif (strchr(am->value, '\\\\'))\n+\t\t\t\t\tdie(_(\"attr spec values must not contain backslashes\"));\n+\t\t\t}\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tam->attr = git_attr_counted(attr, attr_len);\n+\t\tif (!am->attr) {\n+\t\t\tstruct strbuf sb = STRBUF_INIT;\n+\t\t\tam->match_mode = INVALID_ATTR;\n+\t\t\tinvalid_attr_name_message(&sb, attr, attr_len);\n+\t\t\tdie(_(\"invalid attribute in '%s': '%s'\"), value, sb.buf);\n+\t\t}\n+\n+\t\tgit_attr_check_append(item->attr_check, am->attr);\n+\t}\n+\n+\tstring_list_clear(&list, 0);\n+\treturn;\n+}\n+\n static void eat_long_magic(struct pathspec_item *item, const char *elt,\n \t\tunsigned *magic, int *pathspec_prefix,\n \t\tconst char **copyfrom_, const char **long_magic_end)\n {\n \tint i;\n \tconst char *copyfrom = *copyfrom_;\n+\tconst char *body;\n \t/* longhand */\n \tconst char *nextat;\n \tfor (copyfrom = elt + 2;\n@@ -108,15 +175,21 @@ static void eat_long_magic(struct pathspec_item *item, const char *elt,\n \t\tif (!len)\n \t\t\tcontinue;\n \n-\t\tif (starts_with(copyfrom, \"prefix:\")) {\n+\t\tif (skip_prefix(copyfrom, \"prefix:\", &body)) {\n \t\t\tchar *endptr;\n-\t\t\t*pathspec_prefix = strtol(copyfrom + 7,\n-\t\t\t\t\t\t  &endptr, 10);\n+\t\t\t*pathspec_prefix = strtol(body, &endptr, 10);\n \t\t\tif (endptr - copyfrom != len)\n \t\t\t\tdie(_(\"invalid parameter for pathspec magic 'prefix'\"));\n \t\t\tcontinue;\n \t\t}\n \n+\t\tif (skip_prefix(copyfrom, \"attr:\", &body)) {\n+\t\t\tchar *attr_body = xmemdupz(body, len - strlen(\"attr:\"));\n+\t\t\tparse_pathspec_attr_match(item, attr_body);\n+\t\t\tfree(attr_body);\n+\t\t\tcontinue;\n+\t\t}\n+\n \t\tfor (i = 0; i < ARRAY_SIZE(pathspec_magic); i++) {\n \t\t\tif (strlen(pathspec_magic[i].name) == len &&\n \t\t\t    !strncmp(pathspec_magic[i].name, copyfrom, len)) {\n@@ -425,7 +498,10 @@ void parse_pathspec(struct pathspec *pathspec,\n \tfor (i = 0; i < n; i++) {\n \t\tunsigned short_magic;\n \t\tentry = argv[i];\n-\n+\t\titem[i].attr_check = NULL;\n+\t\titem[i].attr_match = NULL;\n+\t\titem[i].attr_match_nr = 0;\n+\t\titem[i].attr_match_alloc = 0;\n \t\titem[i].magic = prefix_pathspec(item + i, &short_magic,\n \t\t\t\t\t\targv + i, flags,\n \t\t\t\t\t\tprefix, prefixlen, entry);\n@@ -447,6 +523,13 @@ void parse_pathspec(struct pathspec *pathspec,\n \t\tif (item[i].nowildcard_len < item[i].len)\n \t\t\tpathspec->has_wildcard = 1;\n \t\tpathspec->magic |= item[i].magic;\n+\n+\t\tif (item[i].attr_match_nr) {\n+\t\t\tint j;\n+\t\t\tfor (j = 0; j < item[i].attr_match_nr; j++)\n+\t\t\t\tif (item[i].attr_match[j].match_mode == INVALID_ATTR)\n+\t\t\t\t\tdie(_(\"attribute spec in the wrong syntax are prohibited.\"));\n+\t\t}\n \t}\n \n \tif (nr_exclude == n)\n@@ -502,6 +585,16 @@ void copy_pathspec(struct pathspec *dst, const struct pathspec *src)\n \n void free_pathspec(struct pathspec *pathspec)\n {\n+\tint i, j;\n+\tfor (i = 0; i < pathspec->nr; i++) {\n+\t\tif (!pathspec->items[i].attr_match_nr)\n+\t\t\tcontinue;\n+\t\tfor (j = 0; j < pathspec->items[j].attr_match_nr; j++)\n+\t\t\tfree(pathspec->items[i].attr_match[j].value);\n+\t\tfree(pathspec->items[i].attr_match);\n+\t\tgit_attr_check_free(pathspec->items[i].attr_check);\n+\t}\n+\n \tfree(pathspec->items);\n \tpathspec->items = NULL;\n }\ndiff --git a/pathspec.h b/pathspec.h\nindex 0c11262..5308137 100644\n--- a/pathspec.h\n+++ b/pathspec.h\n@@ -32,6 +32,22 @@ struct pathspec {\n \t\tint len, prefix;\n \t\tint nowildcard_len;\n \t\tint flags;\n+\t\tint attr_match_nr;\n+\t\tint attr_match_alloc;\n+\t\tstruct attr_match {\n+\t\t\tstruct git_attr *attr;\n+\t\t\tchar *value;\n+\t\t\tenum attr_match_mode {\n+\t\t\t\tMATCH_SET,\n+\t\t\t\tMATCH_UNSET,\n+\t\t\t\tMATCH_VALUE,\n+\t\t\t\tMATCH_UNSPECIFIED,\n+\t\t\t\tMATCH_NOT_UNSPECIFIED,\n+\t\t\t\tMATCH_SET_OR_VALUE,\n+\t\t\t\tINVALID_ATTR\n+\t\t\t} match_mode;\n+\t\t} *attr_match;\n+\t\tstruct git_attr_check *attr_check;\n \t} *items;\n };\n \ndiff --git a/t/t6134-pathspec-with-labels.sh b/t/t6134-pathspec-with-labels.sh\nnew file mode 100755\nindex 0000000..a5c9632\n--- /dev/null\n+++ b/t/t6134-pathspec-with-labels.sh\n@@ -0,0 +1,166 @@\n+#!/bin/sh\n+\n+test_description='test labels in pathspecs'\n+. ./test-lib.sh\n+\n+test_expect_success 'setup a tree' '\n+\tcat <<-EOF >expect &&\n+\tfileA\n+\tfileAB\n+\tfileAC\n+\tfileB\n+\tfileBC\n+\tfileC\n+\tfileNoLabel\n+\tfileSetLabel\n+\tfileUnsetLabel\n+\tfileValue\n+\tfileWrongLabel\n+\tsub/fileA\n+\tsub/fileAB\n+\tsub/fileAC\n+\tsub/fileB\n+\tsub/fileBC\n+\tsub/fileC\n+\tsub/fileNoLabel\n+\tsub/fileSetLabel\n+\tsub/fileUnsetLabel\n+\tsub/fileValue\n+\tsub/fileWrongLabel\n+\tEOF\n+\tmkdir sub &&\n+\twhile read path\n+\tdo\n+\t\t: >$path &&\n+\t\tgit add $path || return 1\n+\tdone <expect &&\n+\tgit commit -m \"initial commit\" &&\n+\tgit ls-files >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'pathspec with no attr' '\n+\ttest_must_fail git ls-files \":(attr:)\"\n+'\n+\n+test_expect_success 'pathspec with labels and non existent .gitattributes' '\n+\tgit ls-files \":(attr:label)\" >actual &&\n+\ttest_must_be_empty actual\n+'\n+\n+test_expect_success 'setup .gitattributes' '\n+\tcat <<-EOF >.gitattributes &&\n+\tfileA labelA\n+\tfileB labelB\n+\tfileC labelC\n+\tfileAB labelA labelB\n+\tfileAC labelA labelC\n+\tfileBC labelB labelC\n+\tfileUnsetLabel -label\n+\tfileSetLabel label\n+\tfileValue label=foo\n+\tfileWrongLabel label☺\n+\tEOF\n+\tgit add .gitattributes &&\n+\tgit commit -m \"add attributes\"\n+'\n+\n+test_expect_success 'check specific set attr' '\n+\tcat <<-EOF >expect &&\n+\tfileSetLabel\n+\tsub/fileSetLabel\n+\tEOF\n+\tgit ls-files \":(attr:label)\" >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'check specific unset attr' '\n+\tcat <<-EOF >expect &&\n+\tfileUnsetLabel\n+\tsub/fileUnsetLabel\n+\tEOF\n+\tgit ls-files \":(attr:-label)\" >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'check specific value attr' '\n+\tcat <<-EOF >expect &&\n+\tfileValue\n+\tsub/fileValue\n+\tEOF\n+\tgit ls-files \":(attr:label=foo)\" >actual &&\n+\ttest_cmp expect actual &&\n+\tgit ls-files \":(attr:label=bar)\" >actual &&\n+\ttest_must_be_empty actual\n+'\n+\n+test_expect_success 'check unspecified attr' '\n+\tcat <<-EOF >expect &&\n+\t.gitattributes\n+\tfileA\n+\tfileAB\n+\tfileAC\n+\tfileB\n+\tfileBC\n+\tfileC\n+\tfileNoLabel\n+\tfileWrongLabel\n+\tsub/fileA\n+\tsub/fileAB\n+\tsub/fileAC\n+\tsub/fileB\n+\tsub/fileBC\n+\tsub/fileC\n+\tsub/fileNoLabel\n+\tsub/fileWrongLabel\n+\tEOF\n+\tgit ls-files \":(attr:!label)\" >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'check multiple unspecified attr' '\n+\tcat <<-EOF >expect &&\n+\t.gitattributes\n+\tfileC\n+\tfileNoLabel\n+\tfileWrongLabel\n+\tsub/fileC\n+\tsub/fileNoLabel\n+\tsub/fileWrongLabel\n+\tEOF\n+\tgit ls-files \":(attr:!labelB !labelA !label)\" >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'check label with more labels but excluded path' '\n+\tcat <<-EOF >expect &&\n+\tfileAB\n+\tfileB\n+\tfileBC\n+\tEOF\n+\tgit ls-files \":(attr:labelB)\" \":(exclude)sub/\" >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'check label excluding other labels' '\n+\tcat <<-EOF >expect &&\n+\tfileAB\n+\tfileB\n+\tfileBC\n+\tsub/fileAB\n+\tsub/fileB\n+\tEOF\n+\tgit ls-files \":(attr:labelB)\" \":(exclude,attr:labelC)sub/\" >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'abort on giving invalid label on the command line' '\n+\ttest_must_fail git ls-files . \":(attr:☺)\"\n+'\n+\n+test_expect_success 'abort on asking for wrong magic' '\n+\ttest_must_fail git ls-files . \":(attr:-label=foo)\" &&\n+\ttest_must_fail git ls-files . \":(attr:!label=foo)\"\n+'\n+\n+test_done\n-- \n2.8.2.123.gb4ad9b6.dirty\n"},{"id":"287127","messageId":"xmqqshxccyex.fsf@gitster.mtv.corp.google.com","threadId":"42403","inReplyTo":"20160520190034.12681-1-sbeller@google.com","subject":"Re: [PATCHv9] pathspec: allow querying for attributes","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2016-05-20T19:38:46Z","receivedAt":"2016-05-20T19:38:46Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Thanks.\n"}]}