pathspec: allow querying for attributes

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. Based on a patch by Stefan Beller <sbeller@google.com> Signed-off-by: Brandon Williams <bmwill@google.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Brandon Williams committed Mar 13, 2017 at 11:23 UTC b0db70465246bb8309d3d12c9bc34ac3f0c1e203
7 files changed +382 -9
Documentation/glossary-content.txt
+21
@@ -384,6 +384,27 @@ full pathname may have special meaning:
384 +
385 Glob magic is incompatible with literal magic.
386
387 +attr;;
388 +After `attr:` comes a space separated list of "attribute
389 +requirements", all of which must be met in order for the
390 +path to be considered a match; this is in addition to the
391 +usual non-magic pathspec pattern matching.
392 +See linkgit:gitattributes[5].
393 ++
394 +Each of the attribute requirements for the path takes one of
395 +these forms:
396 +
397 +- "`ATTR`" requires that the attribute `ATTR` be set.
398 +
399 +- "`-ATTR`" requires that the attribute `ATTR` be unset.
400 +
401 +- "`ATTR=VALUE`" requires that the attribute `ATTR` be
402 + set to the string `VALUE`.
403 +
404 +- "`!ATTR`" requires that the attribute `ATTR` be
405 + unspecified.
406 ++
407 +
408 exclude;;
409 After a path matches any non-exclude pathspec, it will be run
410 through all exclude pathspec (magic signature: `!` or its
attr.c
+17
@@ -603,6 +603,23 @@ struct attr_check *attr_check_initl(const char *one, ...)
603 return check;
604 }
605
606 +struct attr_check *attr_check_dup(const struct attr_check *check)
607 +{
608 + struct attr_check *ret;
609 +
610 + if (!check)
611 + return NULL;
612 +
613 + ret = attr_check_alloc();
614 +
615 + ret->nr = check->nr;
616 + ret->alloc = check->alloc;
617 + ALLOC_ARRAY(ret->items, ret->nr);
618 + COPY_ARRAY(ret->items, check->items, ret->nr);
619 +
620 + return ret;
621 +}
622 +
623 struct attr_check_item *attr_check_append(struct attr_check *check,
624 const struct git_attr *attr)
625 {
attr.h
+1
@@ -44,6 +44,7 @@ struct attr_check {
44
45 extern struct attr_check *attr_check_alloc(void);
46 extern struct attr_check *attr_check_initl(const char *, ...);
47 +extern struct attr_check *attr_check_dup(const struct attr_check *check);
48
49 extern struct attr_check_item *attr_check_append(struct attr_check *check,
50 const struct git_attr *attr);
dir.c
+40 -3
@@ -9,6 +9,7 @@
9 */
10 #include "cache.h"
11 #include "dir.h"
12 +#include "attr.h"
13 #include "refs.h"
14 #include "wildmatch.h"
15 #include "pathspec.h"
@@ -134,7 +135,8 @@ static size_t common_prefix_len(const struct pathspec *pathspec)
135 PATHSPEC_LITERAL |
136 PATHSPEC_GLOB |
137 PATHSPEC_ICASE |
137 - PATHSPEC_EXCLUDE);
138 + PATHSPEC_EXCLUDE |
139 + PATHSPEC_ATTR);
140
141 for (n = 0; n < pathspec->nr; n++) {
142 size_t i = 0, len = 0, item_len;
@@ -209,6 +211,36 @@ int within_depth(const char *name, int namelen,
211 #define DO_MATCH_DIRECTORY (1<<1)
212 #define DO_MATCH_SUBMODULE (1<<2)
213
214 +static int match_attrs(const char *name, int namelen,
215 + const struct pathspec_item *item)
216 +{
217 + int i;
218 +
219 + git_check_attr(name, item->attr_check);
220 + for (i = 0; i < item->attr_match_nr; i++) {
221 + const char *value;
222 + int matched;
223 + enum attr_match_mode match_mode;
224 +
225 + value = item->attr_check->items[i].value;
226 + match_mode = item->attr_match[i].match_mode;
227 +
228 + if (ATTR_TRUE(value))
229 + matched = (match_mode == MATCH_SET);
230 + else if (ATTR_FALSE(value))
231 + matched = (match_mode == MATCH_UNSET);
232 + else if (ATTR_UNSET(value))
233 + matched = (match_mode == MATCH_UNSPECIFIED);
234 + else
235 + matched = (match_mode == MATCH_VALUE &&
236 + !strcmp(item->attr_match[i].value, value));
237 + if (!matched)
238 + return 0;
239 + }
240 +
241 + return 1;
242 +}
243 +
244 /*
245 * Does 'match' match the given name?
246 * A match is found if
@@ -261,6 +293,9 @@ static int match_pathspec_item(const struct pathspec_item *item, int prefix,
293 strncmp(item->match, name - prefix, item->prefix))
294 return 0;
295
296 + if (item->attr_match_nr && !match_attrs(name, namelen, item))
297 + return 0;
298 +
299 /* If the match was just the prefix, we matched */
300 if (!*match)
301 return MATCHED_RECURSIVELY;
@@ -339,7 +374,8 @@ static int do_match_pathspec(const struct pathspec *ps,
374 PATHSPEC_LITERAL |
375 PATHSPEC_GLOB |
376 PATHSPEC_ICASE |
342 - PATHSPEC_EXCLUDE);
377 + PATHSPEC_EXCLUDE |
378 + PATHSPEC_ATTR);
379
380 if (!ps->nr) {
381 if (!ps->recursive ||
@@ -1361,7 +1397,8 @@ static int simplify_away(const char *path, int pathlen,
1397 PATHSPEC_LITERAL |
1398 PATHSPEC_GLOB |
1399 PATHSPEC_ICASE |
1364 - PATHSPEC_EXCLUDE);
1400 + PATHSPEC_EXCLUDE |
1401 + PATHSPEC_ATTR);
1402
1403 for (i = 0; i < pathspec->nr; i++) {
1404 const struct pathspec_item *item = &pathspec->items[i];
pathspec.c
+108 -5
@@ -1,6 +1,7 @@
1 #include "cache.h"
2 #include "dir.h"
3 #include "pathspec.h"
4 +#include "attr.h"
5
6 /*
7 * Finds which of the given pathspecs match items in the index.
@@ -72,6 +73,7 @@ static struct pathspec_magic {
73 { PATHSPEC_GLOB, '\0', "glob" },
74 { PATHSPEC_ICASE, '\0', "icase" },
75 { PATHSPEC_EXCLUDE, '!', "exclude" },
76 + { PATHSPEC_ATTR, '\0', "attr" },
77 };
78
79 static void prefix_magic(struct strbuf *sb, int prefixlen, unsigned magic)
@@ -87,6 +89,72 @@ static void prefix_magic(struct strbuf *sb, int prefixlen, unsigned magic)
89 strbuf_addf(sb, ",prefix:%d)", prefixlen);
90 }
91
92 +static void parse_pathspec_attr_match(struct pathspec_item *item, const char *value)
93 +{
94 + struct string_list_item *si;
95 + struct string_list list = STRING_LIST_INIT_DUP;
96 +
97 + if (item->attr_check || item->attr_match)
98 + die(_("Only one 'attr:' specification is allowed."));
99 +
100 + if (!value || !*value)
101 + die(_("attr spec must not be empty"));
102 +
103 + string_list_split(&list, value, ' ', -1);
104 + string_list_remove_empty_items(&list, 0);
105 +
106 + item->attr_check = attr_check_alloc();
107 + item->attr_match = xcalloc(list.nr, sizeof(struct attr_match));
108 +
109 + for_each_string_list_item(si, &list) {
110 + size_t attr_len;
111 + char *attr_name;
112 + const struct git_attr *a;
113 +
114 + int j = item->attr_match_nr++;
115 + const char *attr = si->string;
116 + struct attr_match *am = &item->attr_match[j];
117 +
118 + switch (*attr) {
119 + case '!':
120 + am->match_mode = MATCH_UNSPECIFIED;
121 + attr++;
122 + attr_len = strlen(attr);
123 + break;
124 + case '-':
125 + am->match_mode = MATCH_UNSET;
126 + attr++;
127 + attr_len = strlen(attr);
128 + break;
129 + default:
130 + attr_len = strcspn(attr, "=");
131 + if (attr[attr_len] != '=')
132 + am->match_mode = MATCH_SET;
133 + else {
134 + am->match_mode = MATCH_VALUE;
135 + am->value = xstrdup(&attr[attr_len + 1]);
136 + if (strchr(am->value, '\\'))
137 + die(_("attr spec values must not contain backslashes"));
138 + }
139 + break;
140 + }
141 +
142 + attr_name = xmemdupz(attr, attr_len);
143 + a = git_attr(attr_name);
144 + if (!a)
145 + die(_("invalid attribute name %s"), attr_name);
146 +
147 + attr_check_append(item->attr_check, a);
148 +
149 + free(attr_name);
150 + }
151 +
152 + if (item->attr_check->nr != item->attr_match_nr)
153 + die("BUG: should have same number of entries");
154 +
155 + string_list_clear(&list, 0);
156 +}
157 +
158 static inline int get_literal_global(void)
159 {
160 static int literal = -1;
@@ -164,6 +232,7 @@ static int get_global_magic(int element_magic)
232 * returns the position in 'elem' after all magic has been parsed
233 */
234 static const char *parse_long_magic(unsigned *magic, int *prefix_len,
235 + struct pathspec_item *item,
236 const char *elem)
237 {
238 const char *pos;
@@ -189,6 +258,14 @@ static const char *parse_long_magic(unsigned *magic, int *prefix_len,
258 continue;
259 }
260
261 + if (starts_with(pos, "attr:")) {
262 + char *attr_body = xmemdupz(pos + 5, len - 5);
263 + parse_pathspec_attr_match(item, attr_body);
264 + *magic |= PATHSPEC_ATTR;
265 + free(attr_body);
266 + continue;
267 + }
268 +
269 for (i = 0; i < ARRAY_SIZE(pathspec_magic); i++) {
270 if (strlen(pathspec_magic[i].name) == len &&
271 !strncmp(pathspec_magic[i].name, pos, len)) {
@@ -252,13 +329,14 @@ static const char *parse_short_magic(unsigned *magic, const char *elem)
329 }
330
331 static const char *parse_element_magic(unsigned *magic, int *prefix_len,
332 + struct pathspec_item *item,
333 const char *elem)
334 {
335 if (elem[0] != ':' || get_literal_global())
336 return elem; /* nothing to do */
337 else if (elem[1] == '(')
338 /* longhand */
261 - return parse_long_magic(magic, prefix_len, elem);
339 + return parse_long_magic(magic, prefix_len, item, elem);
340 else
341 /* shorthand */
342 return parse_short_magic(magic, elem);
@@ -335,12 +413,17 @@ static void init_pathspec_item(struct pathspec_item *item, unsigned flags,
413 char *match;
414 int pathspec_prefix = -1;
415
416 + item->attr_check = NULL;
417 + item->attr_match = NULL;
418 + item->attr_match_nr = 0;
419 +
420 /* PATHSPEC_LITERAL_PATH ignores magic */
421 if (flags & PATHSPEC_LITERAL_PATH) {
422 magic = PATHSPEC_LITERAL;
423 } else {
424 copyfrom = parse_element_magic(&element_magic,
425 &pathspec_prefix,
426 + item,
427 elt);
428 magic |= element_magic;
429 magic |= get_global_magic(element_magic);
@@ -565,26 +648,46 @@ void parse_pathspec(struct pathspec *pathspec,
648
649 void copy_pathspec(struct pathspec *dst, const struct pathspec *src)
650 {
568 - int i;
651 + int i, j;
652
653 *dst = *src;
654 ALLOC_ARRAY(dst->items, dst->nr);
655 COPY_ARRAY(dst->items, src->items, dst->nr);
656
657 for (i = 0; i < dst->nr; i++) {
575 - dst->items[i].match = xstrdup(src->items[i].match);
576 - dst->items[i].original = xstrdup(src->items[i].original);
658 + struct pathspec_item *d = &dst->items[i];
659 + struct pathspec_item *s = &src->items[i];
660 +
661 + d->match = xstrdup(s->match);
662 + d->original = xstrdup(s->original);
663 +
664 + ALLOC_ARRAY(d->attr_match, d->attr_match_nr);
665 + COPY_ARRAY(d->attr_match, s->attr_match, d->attr_match_nr);
666 + for (j = 0; j < d->attr_match_nr; j++) {
667 + const char *value = s->attr_match[j].value;
668 + d->attr_match[j].value = xstrdup_or_null(value);
669 + }
670 +
671 + d->attr_check = attr_check_dup(s->attr_check);
672 }
673 }
674
675 void clear_pathspec(struct pathspec *pathspec)
676 {
582 - int i;
677 + int i, j;
678
679 for (i = 0; i < pathspec->nr; i++) {
680 free(pathspec->items[i].match);
681 free(pathspec->items[i].original);
682 +
683 + for (j = 0; j < pathspec->items[j].attr_match_nr; j++)
684 + free(pathspec->items[i].attr_match[j].value);
685 + free(pathspec->items[i].attr_match);
686 +
687 + if (pathspec->items[i].attr_check)
688 + attr_check_free(pathspec->items[i].attr_check);
689 }
690 +
691 free(pathspec->items);
692 pathspec->items = NULL;
693 pathspec->nr = 0;
pathspec.h
+14 -1
@@ -8,13 +8,15 @@
8 #define PATHSPEC_GLOB (1<<3)
9 #define PATHSPEC_ICASE (1<<4)
10 #define PATHSPEC_EXCLUDE (1<<5)
11 +#define PATHSPEC_ATTR (1<<6)
12 #define PATHSPEC_ALL_MAGIC \
13 (PATHSPEC_FROMTOP | \
14 PATHSPEC_MAXDEPTH | \
15 PATHSPEC_LITERAL | \
16 PATHSPEC_GLOB | \
17 PATHSPEC_ICASE | \
17 - PATHSPEC_EXCLUDE)
18 + PATHSPEC_EXCLUDE | \
19 + PATHSPEC_ATTR)
20
21 #define PATHSPEC_ONESTAR 1 /* the pathspec pattern satisfies GFNM_ONESTAR */
22
@@ -31,6 +33,17 @@ struct pathspec {
33 int len, prefix;
34 int nowildcard_len;
35 int flags;
36 + int attr_match_nr;
37 + struct attr_match {
38 + char *value;
39 + enum attr_match_mode {
40 + MATCH_SET,
41 + MATCH_UNSET,
42 + MATCH_VALUE,
43 + MATCH_UNSPECIFIED
44 + } match_mode;
45 + } *attr_match;
46 + struct attr_check *attr_check;
47 } *items;
48 };
49
t/t6135-pathspec-with-attrs.sh new
+181
@@ -0,0 +1,181 @@
1 +#!/bin/sh
2 +
3 +test_description='test labels in pathspecs'
4 +. ./test-lib.sh
5 +
6 +test_expect_success 'setup a tree' '
7 + cat <<-\EOF >expect &&
8 + fileA
9 + fileAB
10 + fileAC
11 + fileB
12 + fileBC
13 + fileC
14 + fileNoLabel
15 + fileSetLabel
16 + fileUnsetLabel
17 + fileValue
18 + fileWrongLabel
19 + sub/fileA
20 + sub/fileAB
21 + sub/fileAC
22 + sub/fileB
23 + sub/fileBC
24 + sub/fileC
25 + sub/fileNoLabel
26 + sub/fileSetLabel
27 + sub/fileUnsetLabel
28 + sub/fileValue
29 + sub/fileWrongLabel
30 + EOF
31 + mkdir sub &&
32 + while read path
33 + do
34 + : >$path &&
35 + git add $path || return 1
36 + done <expect &&
37 + git commit -m "initial commit" &&
38 + git ls-files >actual &&
39 + test_cmp expect actual
40 +'
41 +
42 +test_expect_success 'pathspec with no attr' '
43 + test_must_fail git ls-files ":(attr:)"
44 +'
45 +
46 +test_expect_success 'pathspec with labels and non existent .gitattributes' '
47 + git ls-files ":(attr:label)" >actual &&
48 + test_must_be_empty actual
49 +'
50 +
51 +test_expect_success 'setup .gitattributes' '
52 + cat <<-\EOF >.gitattributes &&
53 + fileA labelA
54 + fileB labelB
55 + fileC labelC
56 + fileAB labelA labelB
57 + fileAC labelA labelC
58 + fileBC labelB labelC
59 + fileUnsetLabel -label
60 + fileSetLabel label
61 + fileValue label=foo
62 + fileWrongLabel label☺
63 + EOF
64 + git add .gitattributes &&
65 + git commit -m "add attributes"
66 +'
67 +
68 +test_expect_success 'check specific set attr' '
69 + cat <<-\EOF >expect &&
70 + fileSetLabel
71 + sub/fileSetLabel
72 + EOF
73 + git ls-files ":(attr:label)" >actual &&
74 + test_cmp expect actual
75 +'
76 +
77 +test_expect_success 'check specific unset attr' '
78 + cat <<-\EOF >expect &&
79 + fileUnsetLabel
80 + sub/fileUnsetLabel
81 + EOF
82 + git ls-files ":(attr:-label)" >actual &&
83 + test_cmp expect actual
84 +'
85 +
86 +test_expect_success 'check specific value attr' '
87 + cat <<-\EOF >expect &&
88 + fileValue
89 + sub/fileValue
90 + EOF
91 + git ls-files ":(attr:label=foo)" >actual &&
92 + test_cmp expect actual &&
93 + git ls-files ":(attr:label=bar)" >actual &&
94 + test_must_be_empty actual
95 +'
96 +
97 +test_expect_success 'check unspecified attr' '
98 + cat <<-\EOF >expect &&
99 + .gitattributes
100 + fileA
101 + fileAB
102 + fileAC
103 + fileB
104 + fileBC
105 + fileC
106 + fileNoLabel
107 + fileWrongLabel
108 + sub/fileA
109 + sub/fileAB
110 + sub/fileAC
111 + sub/fileB
112 + sub/fileBC
113 + sub/fileC
114 + sub/fileNoLabel
115 + sub/fileWrongLabel
116 + EOF
117 + git ls-files ":(attr:!label)" >actual &&
118 + test_cmp expect actual
119 +'
120 +
121 +test_expect_success 'check multiple unspecified attr' '
122 + cat <<-\EOF >expect &&
123 + .gitattributes
124 + fileC
125 + fileNoLabel
126 + fileWrongLabel
127 + sub/fileC
128 + sub/fileNoLabel
129 + sub/fileWrongLabel
130 + EOF
131 + git ls-files ":(attr:!labelB !labelA !label)" >actual &&
132 + test_cmp expect actual
133 +'
134 +
135 +test_expect_success 'check label with more labels but excluded path' '
136 + cat <<-\EOF >expect &&
137 + fileAB
138 + fileB
139 + fileBC
140 + EOF
141 + git ls-files ":(attr:labelB)" ":(exclude)sub/" >actual &&
142 + test_cmp expect actual
143 +'
144 +
145 +test_expect_success 'check label excluding other labels' '
146 + cat <<-\EOF >expect &&
147 + fileAB
148 + fileB
149 + fileBC
150 + sub/fileAB
151 + sub/fileB
152 + EOF
153 + git ls-files ":(attr:labelB)" ":(exclude,attr:labelC)sub/" >actual &&
154 + test_cmp expect actual
155 +'
156 +
157 +test_expect_success 'fail on multiple attr specifiers in one pathspec item' '
158 + test_must_fail git ls-files . ":(attr:labelB,attr:labelC)" 2>actual &&
159 + test_i18ngrep "Only one" actual
160 +'
161 +
162 +test_expect_success 'fail if attr magic is used places not implemented' '
163 + # The main purpose of this test is to check that we actually fail
164 + # when you attempt to use attr magic in commands that do not implement
165 + # attr magic. This test does not advocate git-add to stay that way,
166 + # though, but git-add is convenient as it has its own internal pathspec
167 + # parsing.
168 + test_must_fail git add ":(attr:labelB)" 2>actual &&
169 + test_i18ngrep "unsupported magic" actual
170 +'
171 +
172 +test_expect_success 'abort on giving invalid label on the command line' '
173 + test_must_fail git ls-files . ":(attr:☺)"
174 +'
175 +
176 +test_expect_success 'abort on asking for wrong magic' '
177 + test_must_fail git ls-files . ":(attr:-label=foo)" &&
178 + test_must_fail git ls-files . ":(attr:!label=foo)"
179 +'
180 +
181 +test_done