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>
---
 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 majord...@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html

Reply via email to