{"thread":{"id":"42345","subject":"[PATCHv4 1/5] Documentation: fix a typo","startedAt":"2016-05-16T18:05:34Z","lastAt":"2016-05-16T18:05:39Z","messageCount":6,"participants":["Stefan Beller"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"286691","messageId":"20160516180539.4114-1-sbeller@google.com","threadId":"42345","inReplyTo":null,"subject":"[PATCHv4 0/5] pathspec labels","fromName":"Stefan Beller","fromEmail":"sbeller@google.com","sentAt":"2016-05-16T18:05:34Z","receivedAt":"2016-05-16T18:05:34Z","isPatch":false,"sender":{"key":"stefanbeller@gmail.com","avatar":"https://avatars.githubusercontent.com/u/455868?v=4"},"body":"Thanks Junio for the review!\nThanks Duy for suggestions to think about in the not-submodule case :)\n\n* when invalid labels are found\n  -> in the .gitattributes \"warn and ignore\"\n  -> in command line args die(..)\n* treat labels set to false as unset.\n* fixes in documentation/ reworded the commit message\n\nThanks,\nStefan\n\ndiff to current origin/sb/pathspec-label (v2 of the series, so a lot):\n\ndiff --git a/Documentation/gitattributes.txt b/Documentation/gitattributes.txt\nindex af2c682..b846848 100644\n--- a/Documentation/gitattributes.txt\n+++ b/Documentation/gitattributes.txt\n@@ -906,6 +906,18 @@ If this attribute is not set or has an invalid value, the value of the\n (See linkgit:git-config[1]).\n \n \n+Attaching labels to path\n+~~~~~~~~~~~~~~~~~~~~~~~~\n+\n+`label`\n+^^^^^^^\n+By the value of this attribute you can specify a list of arbitrary strings\n+to be attached to a path as labels. These labels can be used for\n+easier paths matching using pathspecs (linkgit:gitglossary[1]).\n+It is required to use only alphanumeric characters, dashes and\n+underscores for the labels.\n+\n+\n USING MACRO ATTRIBUTES\n ----------------------\n \ndiff --git a/Documentation/glossary-content.txt b/Documentation/glossary-content.txt\nindex a1fc9e0..e264a58 100644\n--- a/Documentation/glossary-content.txt\n+++ b/Documentation/glossary-content.txt\n@@ -362,11 +362,6 @@ glob;;\n \tFor example, \"Documentation/{asterisk}.html\" matches\n \t\"Documentation/git.html\" but not \"Documentation/ppc/ppc.html\"\n \tor \"tools/perf/Documentation/perf.html\".\n-\n-label:<white space separated list>;;\n-\tLabels can be assigned to pathspecs in the .gitattributes file.\n-\tBy specifying a list of labels the pattern will match only\n-\tfiles which have all of the listed labels.\n +\n Two consecutive asterisks (\"`**`\") in patterns matched against\n full pathname may have special meaning:\n@@ -389,6 +384,10 @@ full pathname may have special meaning:\n +\n Glob magic is incompatible with literal magic.\n \n+label=<white space separated list>;;\n+\tAdditionally to matching the pathspec, the path must have a 'label'\n+\tattribute having set all labels listed here.\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/attr.h b/attr.h\nindex f6fc7c3..8b08d33 100644\n--- a/attr.h\n+++ b/attr.h\n@@ -18,7 +18,6 @@ extern const char git_attr__false[];\n #define ATTR_TRUE(v) ((v) == git_attr__true)\n #define ATTR_FALSE(v) ((v) == git_attr__false)\n #define ATTR_UNSET(v) ((v) == NULL)\n-#define ATTR_CUSTOM(v) (!(ATTR_UNSET(v) || ATTR_FALSE(v) || ATTR_TRUE(v)))\n \n /*\n  * Send one or more git_attr_check to git_check_attr(), and\ndiff --git a/dir.c b/dir.c\nindex 51d5965..fd39f92 100644\n--- a/dir.c\n+++ b/dir.c\n@@ -209,25 +209,56 @@ int within_depth(const char *name, int namelen,\n \treturn 1;\n }\n \n-void load_labels(const char *name, int namelen, struct string_list *list)\n+static int has_all_labels(const char *name, int namelen,\n+\t\t\t   const struct string_list *required_labels)\n {\n \tstatic struct git_attr *attr;\n+\n \tstruct git_attr_check check;\n-\tchar *path = xmemdupz(name, namelen);\n+\tchar *path;\n+\tint ret;\n \n \tif (!attr)\n \t\tattr = git_attr(\"label\");\n \tcheck.attr = attr;\n \n+\tpath = xmemdupz(name, namelen);\n \tif (git_check_attr(path, 1, &check))\n-\t\tdie(\"git_check_attr died\");\n-\n-\tif (ATTR_CUSTOM(check.value)) {\n-\t\tstring_list_split(list, check.value, ',', -1);\n-\t\tstring_list_sort(list);\n+\t\tdie(\"internal bug: git_check_attr died.\");\n+\n+\tif (ATTR_TRUE(check.value))\n+\t\tret = 1; /* has all the labels */\n+\telse if (ATTR_FALSE(check.value)) {\n+\t\twarning(_(\"Path '%s': Label must not be false. Treat as if no label was set.\"), path);\n+\t\tret = 0;\n+\t} else if (ATTR_UNSET(check.value))\n+\t\tret = 0; /* has no labels */\n+\telse {\n+\t\tstruct string_list_item *si;\n+\t\tstruct string_list attr_labels = STRING_LIST_INIT_DUP;\n+\t\tstring_list_split(&attr_labels, check.value, ',', -1);\n+\t\tfor_each_string_list_item(si, &attr_labels) {\n+\t\t\tif (validate_label_name(si->string)) {\n+\t\t\t\twarning(_(\"Ignoring label '%s'\"), si->string);\n+\t\t\t\tfree(si->string);\n+\t\t\t\tsi->string = \"\";\n+\t\t\t}\n+\t\t}\n+\t\tstring_list_remove_empty_items(&attr_labels, 0);\n+\t\tstring_list_sort(&attr_labels);\n+\t\tret = 1;\n+\t\tfor_each_string_list_item(si, required_labels) {\n+\t\t\tif (!string_list_has_string(&attr_labels, si->string)) {\n+\t\t\t\tret = 0;\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t}\n+\t\tstring_list_clear(&attr_labels, 0);\n \t}\n \n \tfree(path);\n+\n+\treturn ret;\n }\n \n #define DO_MATCH_EXCLUDE   1\n@@ -285,14 +316,8 @@ 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->group) {\n-\t\tstruct string_list has_labels = STRING_LIST_INIT_DUP;\n-\t\tstruct string_list_item *si;\n-\t\tload_labels(name, namelen, &has_labels);\n-\t\tfor_each_string_list_item(si, item->group)\n-\t\t\tif (!string_list_has_string(&has_labels, si->string))\n-\t\t\t\treturn 0;\n-\t}\n+\tif (item->labels && !has_all_labels(name, namelen, item->labels))\n+\t\treturn 0;\n \n \t/* If the match was just the prefix, we matched */\n \tif (!*match)\ndiff --git a/pathspec.c b/pathspec.c\nindex c227c25..2f053fb 100644\n--- a/pathspec.c\n+++ b/pathspec.c\n@@ -88,13 +88,42 @@ static void prefix_short_magic(struct strbuf *sb, int prefixlen,\n \tstrbuf_addf(sb, \",prefix:%d)\", prefixlen);\n }\n \n+static int check_valid_label_name(const char *label)\n+{\n+\tif (!label || !strlen(label))\n+\t\treturn -1;\n+\n+\tif (!isalpha(*label))\n+\t\treturn -1;\n+\n+\twhile (*label) {\n+\t\tif (!(isalnum(*label) ||\n+\t\t\t*label == '-' ||\n+\t\t\t*label == '_'))\n+\t\t\treturn -1;\n+\t\tlabel++;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+int validate_label_name(const char *label)\n+{\n+\tint ret = check_valid_label_name(label);\n+\tif (ret)\n+\t\twarning(_(\"Label '%s': Label names must start with an \"\n+\t\t\t  \"alphabetic character and use only alphanumeric \"\n+\t\t\t  \"characters, dashes and underscores.\"), label);\n+\treturn ret;\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 *out;\n+\tconst char *body;\n \t/* longhand */\n \tconst char *nextat;\n \tfor (copyfrom = elt + 2;\n@@ -109,25 +138,24 @@ 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, \"label:\", &out)) {\n+\t\tif (skip_prefix(copyfrom, \"label=\", &body)) {\n \t\t\tstruct strbuf sb = STRBUF_INIT;\n-\t\t\tsize_t l = nextat - out;\n-\t\t\tstrbuf_add(&sb, out, l);\n-\t\t\tif (!item->group) {\n-\t\t\t\titem->group = xmalloc(sizeof(*item->group));\n-\t\t\t\tstring_list_init(item->group, 1);\n-\t\t\t}\n-\t\t\tstring_list_split(item->group, sb.buf, ' ', -1);\n-\t\t\tstring_list_remove_empty_items(item->group, 0);\n+\t\t\tstrbuf_add(&sb, body, nextat - body);\n+\t\t\tif (!item->labels) {\n+\t\t\t\titem->labels = xmalloc(sizeof(*item->labels));\n+\t\t\t\tstring_list_init(item->labels, 1);\n+\t\t\t} else\n+\t\t\t\tdie(_(\"multiple labels not supported in pathspec magic\"));\n+\t\t\tstring_list_split(item->labels, sb.buf, ' ', -1);\n+\t\t\tstring_list_remove_empty_items(item->labels, 0);\n \t\t\tstrbuf_release(&sb);\n \t\t\tcontinue;\n \t\t}\n@@ -440,7 +468,7 @@ void parse_pathspec(struct pathspec *pathspec,\n \tfor (i = 0; i < n; i++) {\n \t\tunsigned short_magic;\n \t\tentry = argv[i];\n-\t\titem[i].group = NULL;\n+\t\titem[i].labels = NULL;\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@@ -462,6 +490,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].labels) {\n+\t\t\tstruct string_list_item *si;\n+\t\t\tfor_each_string_list_item(si, item[i].labels)\n+\t\t\t\tif (validate_label_name(si->string))\n+\t\t\t\t\tdie(_(\"Labels in the wrong syntax are prohibited.\"));\n+\t\t}\n \t}\n \n \tif (nr_exclude == n)\n@@ -519,9 +554,9 @@ void free_pathspec(struct pathspec *pathspec)\n {\n \tint i;\n \tfor (i = 0; i < pathspec->nr; i++) {\n-\t\tif (pathspec->items[i].group)\n-\t\t\tstring_list_clear(pathspec->items[i].group, 1);\n-\t\tfree(pathspec->items[i].group);\n+\t\tif (pathspec->items[i].labels)\n+\t\t\tstring_list_clear(pathspec->items[i].labels, 1);\n+\t\tfree(pathspec->items[i].labels);\n \t}\n \n \tfree(pathspec->items);\ndiff --git a/pathspec.h b/pathspec.h\nindex e3f7ebf..bf02931 100644\n--- a/pathspec.h\n+++ b/pathspec.h\n@@ -32,7 +32,7 @@ struct pathspec {\n \t\tint len, prefix;\n \t\tint nowildcard_len;\n \t\tint flags;\n-\t\tstruct string_list *group;\n+\t\tstruct string_list *labels;\n \t} *items;\n };\n \n@@ -99,5 +99,6 @@ extern char *find_pathspecs_matching_against_index(const struct pathspec *pathsp\n extern void add_pathspec_matches_against_index(const struct pathspec *pathspec, char *seen);\n extern const char *check_path_for_gitlink(const char *path);\n extern void die_if_path_beyond_symlink(const char *path, const char *prefix);\n+extern int validate_label_name(const char *label);\n \n #endif /* PATHSPEC_H */\ndiff --git a/t/t6134-pathspec-with-labels.sh b/t/t6134-pathspec-with-labels.sh\nindex 0c061ce..e2e4753 100755\n--- a/t/t6134-pathspec-with-labels.sh\n+++ b/t/t6134-pathspec-with-labels.sh\n@@ -4,88 +4,155 @@ test_description='test labels in pathspecs'\n . ./test-lib.sh\n \n test_expect_success 'setup a tree' '\n-\tfor p in file sub/file sub/sub/file sub/file2 sub/sub/sub/file sub2/file; do\n-\t\tif echo $p | grep /; then\n-\t\t\tmkdir -p $(dirname $p)\n-\t\tfi &&\n+\tmkdir sub &&\n+\tfor p in fileA fileB fileC fileAB fileAC fileBC fileNoLabel fileUnsetLabel fileSetLabel; do\n \t\t: >$p &&\n \t\tgit add $p &&\n-\t\tgit commit -m $p\n+\t\t: >sub/$p\n+\t\tgit add sub/$p\n \tdone &&\n-\tgit log --oneline --format=%s >actual &&\n+\tgit commit -m $p &&\n+\tgit ls-files >actual &&\n \tcat <<EOF >expect &&\n-sub2/file\n-sub/sub/sub/file\n-sub/file2\n-sub/sub/file\n-sub/file\n-file\n+fileA\n+fileAB\n+fileAC\n+fileB\n+fileBC\n+fileC\n+fileNoLabel\n+fileSetLabel\n+fileUnsetLabel\n+sub/fileA\n+sub/fileAB\n+sub/fileAC\n+sub/fileB\n+sub/fileBC\n+sub/fileC\n+sub/fileNoLabel\n+sub/fileSetLabel\n+sub/fileUnsetLabel\n EOF\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'pathspec with labels and no .gitattributes exists' '\n-\tgit ls-files \":(label:a)\" >actual &&\n+test_expect_success 'pathspec with no label and non existent .gitattributes' '\n+\tgit ls-files \":(label=)\" >actual &&\n+\ttest_must_be_empty actual\n+'\n+\n+test_expect_success 'pathspec with labels and non existent .gitattributes' '\n+\tgit ls-files \":(label=a)\" >actual &&\n \ttest_must_be_empty actual\n '\n \n test_expect_success 'setup .gitattributes' '\n-\tcat <<-EOF >.gitattributes &&\n-\t/file label=b\n-\tsub/file label=a\n-\tsub/sub/* label=b,c\n-\tEOF\n+\tcat <<EOF >.gitattributes &&\n+fileA label=labelA\n+fileB label=labelB\n+fileC label=labelC\n+fileAB label=labelA,labelB\n+fileAC label=labelA,labelC\n+fileBC label=labelB,labelC\n+fileUnsetLabel -label\n+fileSetLabel label\n+EOF\n \tgit add .gitattributes &&\n \tgit commit -m \"add attributes\"\n '\n \n-test_expect_success 'check label' '\n-\tcat <<-EOF >expect &&\n-\tsub/file\n-\tEOF\n-\tgit ls-files \":(label:a)\" >actual &&\n+sq=\"'\"\n+\n+test_expect_success 'check for any label' '\n+\tcat <<EOF >expect &&\n+fileA\n+fileAB\n+fileAC\n+fileB\n+fileBC\n+fileC\n+fileSetLabel\n+sub/fileA\n+sub/fileAB\n+sub/fileAC\n+sub/fileB\n+sub/fileBC\n+sub/fileC\n+sub/fileSetLabel\n+EOF\n+\tcat <<EOF >expect2 &&\n+warning: Path ${sq}fileUnsetLabel${sq}: Label must not be unset\n+warning: Path ${sq}sub/fileUnsetLabel${sq}: Label must not be unset\n+EOF\n+\tgit ls-files \":(label=)\" >actual 2>actual2 &&\n+\ttest_cmp expect actual &&\n+\ttest_cmp expect2 actual2\n+\n+'\n+\n+test_expect_success 'check specific label' '\n+\tcat <<EOF >expect &&\n+fileA\n+fileAB\n+fileAC\n+fileSetLabel\n+sub/fileA\n+sub/fileAB\n+sub/fileAC\n+sub/fileSetLabel\n+EOF\n+\tgit ls-files \":(label=labelA)\" >actual &&\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'check label from label list' '\n-\tcat <<-EOF >expect &&\n-\tsub/sub/file\n-\tEOF\n-\tgit ls-files \":(label:c)\" >actual &&\n+test_expect_success 'check label with 2 labels' '\n+\tcat <<EOF >expect &&\n+fileAB\n+fileSetLabel\n+sub/fileAB\n+sub/fileSetLabel\n+EOF\n+\tgit ls-files \":(label=labelA labelB)\" >actual &&\n \ttest_cmp expect actual\n '\n \n test_expect_success 'check label with more labels' '\n-\tcat <<-EOF >expect &&\n-\tfile\n-\tsub/sub/file\n-\tEOF\n-\tgit ls-files \":(label:b)\" >actual &&\n-\ttest_cmp expect actual\n+\ttest_must_fail git ls-files \":(label=labelA,label=labelB)\" 2>actual &&\n+\ttest_i18ngrep \"not supported\" actual\n '\n \n test_expect_success 'check label with more labels but excluded path' '\n-\tcat <<-EOF >expect &&\n-\tsub/sub/file\n-\tEOF\n-\tgit ls-files \":(label:b)\" \":(exclude)./file\" >actual &&\n+\tcat <<EOF >expect &&\n+fileAB\n+fileB\n+fileBC\n+fileSetLabel\n+EOF\n+\tgit ls-files \":(label=labelB)\" \":(exclude)sub/\" >actual &&\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'check label specifying more labels' '\n-\tcat <<-EOF >expect &&\n-\tsub/sub/file\n-\tEOF\n-\tgit ls-files \":(label:b c)\" >actual &&\n+test_expect_success 'check label excluding other labels' '\n+\tcat <<EOF >expect &&\n+fileAB\n+fileB\n+fileBC\n+fileSetLabel\n+sub/fileAB\n+sub/fileB\n+EOF\n+\tgit ls-files \":(label=labelB)\" \":(exclude,label=labelC)sub/\" >actual &&\n \ttest_cmp expect actual\n '\n \n-test_expect_success 'check label specifying more labels' '\n-\tcat <<-EOF >expect &&\n-\tsub/file\n-\tsub/sub/file\n-\tEOF\n-\tgit ls-files \":(label:b c)\" \":(label:a)\" >actual &&\n+test_expect_success 'check for paths with no label' '\n+\tcat <<EOF >expect &&\n+.gitattributes\n+fileNoLabel\n+sub/fileNoLabel\n+EOF\n+\tgit ls-files . \":(exclude,label=)\" >actual &&\n \ttest_cmp expect actual\n '\n+\n test_done\n\n\n\nStefan Beller (5):\n  Documentation: fix a typo\n  Documentation: correct typo in example for querying attributes\n  pathspec: move long magic parsing out of prefix_pathspec\n  pathspec: move prefix check out of the inner loop\n  pathspec: record labels\n\n Documentation/gitattributes.txt               |  14 ++-\n Documentation/glossary-content.txt            |   4 +\n Documentation/technical/api-gitattributes.txt |   2 +-\n dir.c                                         |  56 +++++++++\n pathspec.c                                    | 144 ++++++++++++++++-------\n pathspec.h                                    |   2 +\n t/t6134-pathspec-with-labels.sh               | 158 ++++++++++++++++++++++++++\n 7 files changed, 340 insertions(+), 40 deletions(-)\n create mode 100755 t/t6134-pathspec-with-labels.sh\n\n-- \n2.8.2.401.gb49ffe6.dirty\n"},{"id":"286690","messageId":"20160516180539.4114-2-sbeller@google.com","threadId":"42345","inReplyTo":"20160516180539.4114-1-sbeller@google.com","subject":"[PATCHv4 1/5] Documentation: fix a typo","fromName":"Stefan Beller","fromEmail":"sbeller@google.com","sentAt":"2016-05-16T18:05:35Z","receivedAt":"2016-05-16T18:05:35Z","isPatch":false,"sender":{"key":"stefanbeller@gmail.com","avatar":"https://avatars.githubusercontent.com/u/455868?v=4"},"body":"Signed-off-by: Stefan Beller <sbeller@google.com>\n---\n Documentation/gitattributes.txt | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/Documentation/gitattributes.txt b/Documentation/gitattributes.txt\nindex e3b1de8..af2c682 100644\n--- a/Documentation/gitattributes.txt\n+++ b/Documentation/gitattributes.txt\n@@ -86,7 +86,7 @@ is either not set or empty, $HOME/.config/git/attributes is used instead.\n Attributes for all users on a system should be placed in the\n `$(prefix)/etc/gitattributes` file.\n \n-Sometimes you would need to override an setting of an attribute\n+Sometimes you would need to override a setting of an attribute\n for a path to `Unspecified` state.  This can be done by listing\n the name of the attribute prefixed with an exclamation point `!`.\n \n-- \n2.8.2.401.gb49ffe6.dirty\n"},{"id":"286694","messageId":"20160516180539.4114-3-sbeller@google.com","threadId":"42345","inReplyTo":"20160516180539.4114-1-sbeller@google.com","subject":"[PATCHv4 2/5] Documentation: correct typo in example for querying attributes","fromName":"Stefan Beller","fromEmail":"sbeller@google.com","sentAt":"2016-05-16T18:05:36Z","receivedAt":"2016-05-16T18:05:36Z","isPatch":false,"sender":{"key":"stefanbeller@gmail.com","avatar":"https://avatars.githubusercontent.com/u/455868?v=4"},"body":"The introductory text of the example has a typo in the\nspecification which makes it harder to follow that example.\n\nSigned-off-by: Stefan Beller <sbeller@google.com>\n---\n Documentation/technical/api-gitattributes.txt | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/Documentation/technical/api-gitattributes.txt b/Documentation/technical/api-gitattributes.txt\nindex 2602668..36fc9af 100644\n--- a/Documentation/technical/api-gitattributes.txt\n+++ b/Documentation/technical/api-gitattributes.txt\n@@ -61,7 +61,7 @@ Querying Specific Attributes\n Example\n -------\n \n-To see how attributes \"crlf\" and \"indent\" are set for different paths.\n+To see how attributes \"crlf\" and \"ident\" are set for different paths.\n \n . Prepare an array of `struct git_attr_check` with two elements (because\n   we are checking two attributes).  Initialize their `attr` member with\n-- \n2.8.2.401.gb49ffe6.dirty\n"},{"id":"286693","messageId":"20160516180539.4114-4-sbeller@google.com","threadId":"42345","inReplyTo":"20160516180539.4114-1-sbeller@google.com","subject":"[PATCHv4 3/5] pathspec: move long magic parsing out of prefix_pathspec","fromName":"Stefan Beller","fromEmail":"sbeller@google.com","sentAt":"2016-05-16T18:05:37Z","receivedAt":"2016-05-16T18:05:37Z","isPatch":false,"sender":{"key":"stefanbeller@gmail.com","avatar":"https://avatars.githubusercontent.com/u/455868?v=4"},"body":"`prefix_pathspec` is quite a lengthy function and we plan on adding more.\nSplit it up for better readability. As we want to add code into the\ninner loop of the long magic parsing, we also benefit from lower\nindentation.\n\nSigned-off-by: Stefan Beller <sbeller@google.com>\n---\n pathspec.c | 84 +++++++++++++++++++++++++++++++++++---------------------------\n 1 file changed, 47 insertions(+), 37 deletions(-)\n\ndiff --git a/pathspec.c b/pathspec.c\nindex c9e9b6c..eba37c2 100644\n--- a/pathspec.c\n+++ b/pathspec.c\n@@ -88,6 +88,52 @@ static void prefix_short_magic(struct strbuf *sb, int prefixlen,\n \tstrbuf_addf(sb, \",prefix:%d)\", prefixlen);\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+\t/* longhand */\n+\tconst char *nextat;\n+\tfor (copyfrom = elt + 2;\n+\t     *copyfrom && *copyfrom != ')';\n+\t     copyfrom = nextat) {\n+\t\tsize_t len = strcspn(copyfrom, \",)\");\n+\t\tif (copyfrom[len] == ',')\n+\t\t\tnextat = copyfrom + len + 1;\n+\t\telse\n+\t\t\t/* handle ')' and '\\0' */\n+\t\t\tnextat = copyfrom + len;\n+\t\tif (!len)\n+\t\t\tcontinue;\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+\t\t\t\t*magic |= pathspec_magic[i].bit;\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t\tif (starts_with(copyfrom, \"prefix:\")) {\n+\t\t\t\tchar *endptr;\n+\t\t\t\t*pathspec_prefix = strtol(copyfrom + 7,\n+\t\t\t\t\t\t\t  &endptr, 10);\n+\t\t\t\tif (endptr - copyfrom != len)\n+\t\t\t\t\tdie(_(\"invalid parameter for pathspec magic 'prefix'\"));\n+\t\t\t\t/* \"i\" would be wrong, but it does not matter */\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t}\n+\t\tif (ARRAY_SIZE(pathspec_magic) <= i)\n+\t\t\tdie(_(\"Invalid pathspec magic '%.*s' in '%s'\"),\n+\t\t\t    (int) len, copyfrom, elt);\n+\t}\n+\tif (*copyfrom != ')')\n+\t\tdie(_(\"Missing ')' at the end of pathspec magic in '%s'\"), elt);\n+\t*long_magic_end = copyfrom;\n+\tcopyfrom++;\n+\t*copyfrom_ = copyfrom;\n+}\n+\n /*\n  * Take an element of a pathspec and check for magic signatures.\n  * Append the result to the prefix. Return the magic bitmap.\n@@ -150,43 +196,7 @@ static unsigned prefix_pathspec(struct pathspec_item *item,\n \t    (flags & PATHSPEC_LITERAL_PATH)) {\n \t\t; /* nothing to do */\n \t} else if (elt[1] == '(') {\n-\t\t/* longhand */\n-\t\tconst char *nextat;\n-\t\tfor (copyfrom = elt + 2;\n-\t\t     *copyfrom && *copyfrom != ')';\n-\t\t     copyfrom = nextat) {\n-\t\t\tsize_t len = strcspn(copyfrom, \",)\");\n-\t\t\tif (copyfrom[len] == ',')\n-\t\t\t\tnextat = copyfrom + len + 1;\n-\t\t\telse\n-\t\t\t\t/* handle ')' and '\\0' */\n-\t\t\t\tnextat = copyfrom + len;\n-\t\t\tif (!len)\n-\t\t\t\tcontinue;\n-\t\t\tfor (i = 0; i < ARRAY_SIZE(pathspec_magic); i++) {\n-\t\t\t\tif (strlen(pathspec_magic[i].name) == len &&\n-\t\t\t\t    !strncmp(pathspec_magic[i].name, copyfrom, len)) {\n-\t\t\t\t\tmagic |= pathspec_magic[i].bit;\n-\t\t\t\t\tbreak;\n-\t\t\t\t}\n-\t\t\t\tif (starts_with(copyfrom, \"prefix:\")) {\n-\t\t\t\t\tchar *endptr;\n-\t\t\t\t\tpathspec_prefix = strtol(copyfrom + 7,\n-\t\t\t\t\t\t\t\t &endptr, 10);\n-\t\t\t\t\tif (endptr - copyfrom != len)\n-\t\t\t\t\t\tdie(_(\"invalid parameter for pathspec magic 'prefix'\"));\n-\t\t\t\t\t/* \"i\" would be wrong, but it does not matter */\n-\t\t\t\t\tbreak;\n-\t\t\t\t}\n-\t\t\t}\n-\t\t\tif (ARRAY_SIZE(pathspec_magic) <= i)\n-\t\t\t\tdie(_(\"Invalid pathspec magic '%.*s' in '%s'\"),\n-\t\t\t\t    (int) len, copyfrom, elt);\n-\t\t}\n-\t\tif (*copyfrom != ')')\n-\t\t\tdie(_(\"Missing ')' at the end of pathspec magic in '%s'\"), elt);\n-\t\tlong_magic_end = copyfrom;\n-\t\tcopyfrom++;\n+\t\teat_long_magic(item, elt, &magic, &pathspec_prefix, &copyfrom, &long_magic_end);\n \t} else {\n \t\t/* shorthand */\n \t\tfor (copyfrom = elt + 1;\n-- \n2.8.2.401.gb49ffe6.dirty\n"},{"id":"286692","messageId":"20160516180539.4114-5-sbeller@google.com","threadId":"42345","inReplyTo":"20160516180539.4114-1-sbeller@google.com","subject":"[PATCHv4 4/5] pathspec: move prefix check out of the inner loop","fromName":"Stefan Beller","fromEmail":"sbeller@google.com","sentAt":"2016-05-16T18:05:38Z","receivedAt":"2016-05-16T18:05:38Z","isPatch":false,"sender":{"key":"stefanbeller@gmail.com","avatar":"https://avatars.githubusercontent.com/u/455868?v=4"},"body":"The prefix check is not related the check of pathspec magic; also there\nis no code that is relevant after we'd break the loop on a match for\n\"prefix:\". So move the check before the loop and shortcircuit the outer\nloop.\n\nSigned-off-by: Stefan Beller <sbeller@google.com>\n---\n pathspec.c | 19 ++++++++++---------\n 1 file changed, 10 insertions(+), 9 deletions(-)\n\ndiff --git a/pathspec.c b/pathspec.c\nindex eba37c2..4dff252 100644\n--- a/pathspec.c\n+++ b/pathspec.c\n@@ -107,21 +107,22 @@ static void eat_long_magic(struct pathspec_item *item, const char *elt,\n \t\t\tnextat = copyfrom + len;\n \t\tif (!len)\n \t\t\tcontinue;\n+\n+\t\tif (starts_with(copyfrom, \"prefix:\")) {\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\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\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 \t\t\t\t*magic |= pathspec_magic[i].bit;\n \t\t\t\tbreak;\n \t\t\t}\n-\t\t\tif (starts_with(copyfrom, \"prefix:\")) {\n-\t\t\t\tchar *endptr;\n-\t\t\t\t*pathspec_prefix = strtol(copyfrom + 7,\n-\t\t\t\t\t\t\t  &endptr, 10);\n-\t\t\t\tif (endptr - copyfrom != len)\n-\t\t\t\t\tdie(_(\"invalid parameter for pathspec magic 'prefix'\"));\n-\t\t\t\t/* \"i\" would be wrong, but it does not matter */\n-\t\t\t\tbreak;\n-\t\t\t}\n \t\t}\n \t\tif (ARRAY_SIZE(pathspec_magic) <= i)\n \t\t\tdie(_(\"Invalid pathspec magic '%.*s' in '%s'\"),\n-- \n2.8.2.401.gb49ffe6.dirty\n"},{"id":"286695","messageId":"20160516180539.4114-6-sbeller@google.com","threadId":"42345","inReplyTo":"20160516180539.4114-1-sbeller@google.com","subject":"[PATCHv4 5/5] pathspec: record labels","fromName":"Stefan Beller","fromEmail":"sbeller@google.com","sentAt":"2016-05-16T18:05:39Z","receivedAt":"2016-05-16T18:05:39Z","isPatch":false,"sender":{"key":"stefanbeller@gmail.com","avatar":"https://avatars.githubusercontent.com/u/455868?v=4"},"body":"The \"label\" attribute can be attached to paths, and the pathspec\nmechanism is extended via the new \":(label=X)pattern/to/match\"\nsyntax to filter paths so that it requires paths to not just\nmatch the given pattern but also have the specified labels\nattached for them to be chosen.\n\nLabels are meant to describe arbitrary set of files, which\nis not described via the tree layout.\n\nSigned-off-by: Stefan Beller <sbeller@google.com>\n---\n Documentation/gitattributes.txt    |  12 +++\n Documentation/glossary-content.txt |   4 +\n dir.c                              |  56 +++++++++++++\n pathspec.c                         |  65 ++++++++++++++-\n pathspec.h                         |   2 +\n t/t6134-pathspec-with-labels.sh    | 158 +++++++++++++++++++++++++++++++++++++\n 6 files changed, 293 insertions(+), 4 deletions(-)\n create mode 100755 t/t6134-pathspec-with-labels.sh\n\ndiff --git a/Documentation/gitattributes.txt b/Documentation/gitattributes.txt\nindex af2c682..b846848 100644\n--- a/Documentation/gitattributes.txt\n+++ b/Documentation/gitattributes.txt\n@@ -906,6 +906,18 @@ If this attribute is not set or has an invalid value, the value of the\n (See linkgit:git-config[1]).\n \n \n+Attaching labels to path\n+~~~~~~~~~~~~~~~~~~~~~~~~\n+\n+`label`\n+^^^^^^^\n+By the value of this attribute you can specify a list of arbitrary strings\n+to be attached to a path as labels. These labels can be used for\n+easier paths matching using pathspecs (linkgit:gitglossary[1]).\n+It is required to use only alphanumeric characters, dashes and\n+underscores for the labels.\n+\n+\n USING MACRO ATTRIBUTES\n ----------------------\n \ndiff --git a/Documentation/glossary-content.txt b/Documentation/glossary-content.txt\nindex 8ad29e6..e264a58 100644\n--- a/Documentation/glossary-content.txt\n+++ b/Documentation/glossary-content.txt\n@@ -384,6 +384,10 @@ full pathname may have special meaning:\n +\n Glob magic is incompatible with literal magic.\n \n+label=<white space separated list>;;\n+\tAdditionally to matching the pathspec, the path must have a 'label'\n+\tattribute having set all labels listed here.\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 656f272..fd39f92 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@@ -208,6 +209,58 @@ int within_depth(const char *name, int namelen,\n \treturn 1;\n }\n \n+static int has_all_labels(const char *name, int namelen,\n+\t\t\t   const struct string_list *required_labels)\n+{\n+\tstatic struct git_attr *attr;\n+\n+\tstruct git_attr_check check;\n+\tchar *path;\n+\tint ret;\n+\n+\tif (!attr)\n+\t\tattr = git_attr(\"label\");\n+\tcheck.attr = attr;\n+\n+\tpath = xmemdupz(name, namelen);\n+\tif (git_check_attr(path, 1, &check))\n+\t\tdie(\"internal bug: git_check_attr died.\");\n+\n+\tif (ATTR_TRUE(check.value))\n+\t\tret = 1; /* has all the labels */\n+\telse if (ATTR_FALSE(check.value)) {\n+\t\twarning(_(\"Path '%s': Label must not be false. Treat as if no label was set.\"), path);\n+\t\tret = 0;\n+\t} else if (ATTR_UNSET(check.value))\n+\t\tret = 0; /* has no labels */\n+\telse {\n+\t\tstruct string_list_item *si;\n+\t\tstruct string_list attr_labels = STRING_LIST_INIT_DUP;\n+\t\tstring_list_split(&attr_labels, check.value, ',', -1);\n+\t\tfor_each_string_list_item(si, &attr_labels) {\n+\t\t\tif (validate_label_name(si->string)) {\n+\t\t\t\twarning(_(\"Ignoring label '%s'\"), si->string);\n+\t\t\t\tfree(si->string);\n+\t\t\t\tsi->string = \"\";\n+\t\t\t}\n+\t\t}\n+\t\tstring_list_remove_empty_items(&attr_labels, 0);\n+\t\tstring_list_sort(&attr_labels);\n+\t\tret = 1;\n+\t\tfor_each_string_list_item(si, required_labels) {\n+\t\t\tif (!string_list_has_string(&attr_labels, si->string)) {\n+\t\t\t\tret = 0;\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t}\n+\t\tstring_list_clear(&attr_labels, 0);\n+\t}\n+\n+\tfree(path);\n+\n+\treturn ret;\n+}\n+\n #define DO_MATCH_EXCLUDE   1\n #define DO_MATCH_DIRECTORY 2\n \n@@ -263,6 +316,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->labels && !has_all_labels(name, namelen, item->labels))\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..2f053fb 100644\n--- a/pathspec.c\n+++ b/pathspec.c\n@@ -88,12 +88,42 @@ static void prefix_short_magic(struct strbuf *sb, int prefixlen,\n \tstrbuf_addf(sb, \",prefix:%d)\", prefixlen);\n }\n \n+static int check_valid_label_name(const char *label)\n+{\n+\tif (!label || !strlen(label))\n+\t\treturn -1;\n+\n+\tif (!isalpha(*label))\n+\t\treturn -1;\n+\n+\twhile (*label) {\n+\t\tif (!(isalnum(*label) ||\n+\t\t\t*label == '-' ||\n+\t\t\t*label == '_'))\n+\t\t\treturn -1;\n+\t\tlabel++;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+int validate_label_name(const char *label)\n+{\n+\tint ret = check_valid_label_name(label);\n+\tif (ret)\n+\t\twarning(_(\"Label '%s': Label names must start with an \"\n+\t\t\t  \"alphabetic character and use only alphanumeric \"\n+\t\t\t  \"characters, dashes and underscores.\"), label);\n+\treturn ret;\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 +138,28 @@ 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, \"label=\", &body)) {\n+\t\t\tstruct strbuf sb = STRBUF_INIT;\n+\t\t\tstrbuf_add(&sb, body, nextat - body);\n+\t\t\tif (!item->labels) {\n+\t\t\t\titem->labels = xmalloc(sizeof(*item->labels));\n+\t\t\t\tstring_list_init(item->labels, 1);\n+\t\t\t} else\n+\t\t\t\tdie(_(\"multiple labels not supported in pathspec magic\"));\n+\t\t\tstring_list_split(item->labels, sb.buf, ' ', -1);\n+\t\t\tstring_list_remove_empty_items(item->labels, 0);\n+\t\t\tstrbuf_release(&sb);\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 +468,7 @@ 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].labels = NULL;\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 +490,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].labels) {\n+\t\t\tstruct string_list_item *si;\n+\t\t\tfor_each_string_list_item(si, item[i].labels)\n+\t\t\t\tif (validate_label_name(si->string))\n+\t\t\t\t\tdie(_(\"Labels in the wrong syntax are prohibited.\"));\n+\t\t}\n \t}\n \n \tif (nr_exclude == n)\n@@ -502,6 +552,13 @@ void copy_pathspec(struct pathspec *dst, const struct pathspec *src)\n \n void free_pathspec(struct pathspec *pathspec)\n {\n+\tint i;\n+\tfor (i = 0; i < pathspec->nr; i++) {\n+\t\tif (pathspec->items[i].labels)\n+\t\t\tstring_list_clear(pathspec->items[i].labels, 1);\n+\t\tfree(pathspec->items[i].labels);\n+\t}\n+\n \tfree(pathspec->items);\n \tpathspec->items = NULL;\n }\ndiff --git a/pathspec.h b/pathspec.h\nindex 0c11262..bf02931 100644\n--- a/pathspec.h\n+++ b/pathspec.h\n@@ -32,6 +32,7 @@ struct pathspec {\n \t\tint len, prefix;\n \t\tint nowildcard_len;\n \t\tint flags;\n+\t\tstruct string_list *labels;\n \t} *items;\n };\n \n@@ -98,5 +99,6 @@ extern char *find_pathspecs_matching_against_index(const struct pathspec *pathsp\n extern void add_pathspec_matches_against_index(const struct pathspec *pathspec, char *seen);\n extern const char *check_path_for_gitlink(const char *path);\n extern void die_if_path_beyond_symlink(const char *path, const char *prefix);\n+extern int validate_label_name(const char *label);\n \n #endif /* PATHSPEC_H */\ndiff --git a/t/t6134-pathspec-with-labels.sh b/t/t6134-pathspec-with-labels.sh\nnew file mode 100755\nindex 0000000..e2e4753\n--- /dev/null\n+++ b/t/t6134-pathspec-with-labels.sh\n@@ -0,0 +1,158 @@\n+#!/bin/sh\n+\n+test_description='test labels in pathspecs'\n+. ./test-lib.sh\n+\n+test_expect_success 'setup a tree' '\n+\tmkdir sub &&\n+\tfor p in fileA fileB fileC fileAB fileAC fileBC fileNoLabel fileUnsetLabel fileSetLabel; do\n+\t\t: >$p &&\n+\t\tgit add $p &&\n+\t\t: >sub/$p\n+\t\tgit add sub/$p\n+\tdone &&\n+\tgit commit -m $p &&\n+\tgit ls-files >actual &&\n+\tcat <<EOF >expect &&\n+fileA\n+fileAB\n+fileAC\n+fileB\n+fileBC\n+fileC\n+fileNoLabel\n+fileSetLabel\n+fileUnsetLabel\n+sub/fileA\n+sub/fileAB\n+sub/fileAC\n+sub/fileB\n+sub/fileBC\n+sub/fileC\n+sub/fileNoLabel\n+sub/fileSetLabel\n+sub/fileUnsetLabel\n+EOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'pathspec with no label and non existent .gitattributes' '\n+\tgit ls-files \":(label=)\" >actual &&\n+\ttest_must_be_empty actual\n+'\n+\n+test_expect_success 'pathspec with labels and non existent .gitattributes' '\n+\tgit ls-files \":(label=a)\" >actual &&\n+\ttest_must_be_empty actual\n+'\n+\n+test_expect_success 'setup .gitattributes' '\n+\tcat <<EOF >.gitattributes &&\n+fileA label=labelA\n+fileB label=labelB\n+fileC label=labelC\n+fileAB label=labelA,labelB\n+fileAC label=labelA,labelC\n+fileBC label=labelB,labelC\n+fileUnsetLabel -label\n+fileSetLabel label\n+EOF\n+\tgit add .gitattributes &&\n+\tgit commit -m \"add attributes\"\n+'\n+\n+sq=\"'\"\n+\n+test_expect_success 'check for any label' '\n+\tcat <<EOF >expect &&\n+fileA\n+fileAB\n+fileAC\n+fileB\n+fileBC\n+fileC\n+fileSetLabel\n+sub/fileA\n+sub/fileAB\n+sub/fileAC\n+sub/fileB\n+sub/fileBC\n+sub/fileC\n+sub/fileSetLabel\n+EOF\n+\tcat <<EOF >expect2 &&\n+warning: Path ${sq}fileUnsetLabel${sq}: Label must not be unset\n+warning: Path ${sq}sub/fileUnsetLabel${sq}: Label must not be unset\n+EOF\n+\tgit ls-files \":(label=)\" >actual 2>actual2 &&\n+\ttest_cmp expect actual &&\n+\ttest_cmp expect2 actual2\n+\n+'\n+\n+test_expect_success 'check specific label' '\n+\tcat <<EOF >expect &&\n+fileA\n+fileAB\n+fileAC\n+fileSetLabel\n+sub/fileA\n+sub/fileAB\n+sub/fileAC\n+sub/fileSetLabel\n+EOF\n+\tgit ls-files \":(label=labelA)\" >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'check label with 2 labels' '\n+\tcat <<EOF >expect &&\n+fileAB\n+fileSetLabel\n+sub/fileAB\n+sub/fileSetLabel\n+EOF\n+\tgit ls-files \":(label=labelA labelB)\" >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'check label with more labels' '\n+\ttest_must_fail git ls-files \":(label=labelA,label=labelB)\" 2>actual &&\n+\ttest_i18ngrep \"not supported\" actual\n+'\n+\n+test_expect_success 'check label with more labels but excluded path' '\n+\tcat <<EOF >expect &&\n+fileAB\n+fileB\n+fileBC\n+fileSetLabel\n+EOF\n+\tgit ls-files \":(label=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+fileAB\n+fileB\n+fileBC\n+fileSetLabel\n+sub/fileAB\n+sub/fileB\n+EOF\n+\tgit ls-files \":(label=labelB)\" \":(exclude,label=labelC)sub/\" >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'check for paths with no label' '\n+\tcat <<EOF >expect &&\n+.gitattributes\n+fileNoLabel\n+sub/fileNoLabel\n+EOF\n+\tgit ls-files . \":(exclude,label=)\" >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_done\n-- \n2.8.2.401.gb49ffe6.dirty\n"}]}