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