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 <sbel...@google.com>
---

Junio, please replace the last patch of sb/pathspec-label with this one.

diff to last round:
#       diff --git a/Documentation/glossary-content.txt 
b/Documentation/glossary-content.txt
#       index e06520b..181f52e 100644
#       --- a/Documentation/glossary-content.txt
#       +++ b/Documentation/glossary-content.txt
#       @@ -389,7 +389,7 @@ 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:
#        
#       diff --git a/pathspec.c b/pathspec.c
#       index 693a5e7..0a02255 100644
#       --- a/pathspec.c
#       +++ b/pathspec.c
#       @@ -115,19 +115,19 @@ static void parse_pathspec_attr_match(struct 
pathspec_item *item, const char *va
#                       const char *attr = si->string;
#                       struct attr_match *am = &item->attr_match[j];
#        
#       -               attr_len = strcspn(attr, "=");
#                       switch (*attr) {
#                       case '!':
#                               am->match_mode = MATCH_UNSPECIFIED;
#                               attr++;
#       -                       attr_len--;
#       +                       attr_len = strlen(attr);
#                               break;
#                       case '-':
#                               am->match_mode = MATCH_UNSET;
#                               attr++;
#       -                       attr_len--;
#       +                       attr_len = strlen(attr);
#                               break;
#                       default:
#       +                       attr_len = strcspn(attr, "=");
#                               if (attr[attr_len] != '=')
#                                       am->match_mode = MATCH_SET;
#                               else {
#       diff --git a/t/t6134-pathspec-with-labels.sh 
b/t/t6134-pathspec-with-labels.sh
#       index 5da1a63..a5c9632 100755
#       --- a/t/t6134-pathspec-with-labels.sh
#       +++ b/t/t6134-pathspec-with-labels.sh
#       @@ -40,8 +40,7 @@ test_expect_success 'setup a tree' '
#        '
#        
#        test_expect_success 'pathspec with no attr' '
#       -       test_must_fail git ls-files ":(attr:)" 2>actual &&
#       -       test_i18ngrep fatal actual
#       +       test_must_fail git ls-files ":(attr:)"
#        '
#        
#        test_expect_success 'pathspec with labels and non existent 
.gitattributes' '
#       @@ -156,8 +155,12 @@ test_expect_success 'check label excluding other 
labels' '
#        '
#        
#        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_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_done

 Documentation/glossary-content.txt |  20 +++++
 dir.c                              |  35 ++++++++
 pathspec.c                         | 101 +++++++++++++++++++++-
 pathspec.h                         |  16 ++++
 t/t6134-pathspec-with-labels.sh    | 166 +++++++++++++++++++++++++++++++++++++
 5 files changed, 334 insertions(+), 4 deletions(-)
 create mode 100755 t/t6134-pathspec-with-labels.sh

diff --git a/Documentation/glossary-content.txt 
b/Documentation/glossary-content.txt
index cafc284..181f52e 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
diff --git a/dir.c b/dir.c
index 996653b..fc071af 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,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
 
@@ -270,6 +302,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..0a02255 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,78 @@ 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))
+               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 {
+                               am->match_mode = MATCH_VALUE;
+                               am->value = xstrdup(&attr[attr_len + 1]);
+                               if (strchr(am->value, '\\'))
+                                       die(_("attr spec values must not 
contain backslashes"));
+                       }
+                       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;
@@ -108,15 +175,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 *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)) {
@@ -425,7 +498,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 +523,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 +585,16 @@ 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++) {
+               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 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..a5c9632
--- /dev/null
+++ b/t/t6134-pathspec-with-labels.sh
@@ -0,0 +1,166 @@
+#!/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_done
-- 
2.8.2.123.gb4ad9b6.dirty

--
To unsubscribe from this list: send the line "unsubscribe git" in
the body of a message to majord...@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html

Reply via email to