attr: enable attr pathspec magic for git-add and git-stash

Allow users to limit or exclude files based on file attributes during git-add and git-stash. For example, the chromium project would like to use $ git add . ':(exclude,attr:submodule)' as submodules are managed by an external tool, forbidding end users to record changes with "git add". Allowing "git add" to often records changes that users do not want in their commits. This commit does not change any attr magic implementation. It is only adding attr as an allowed pathspec in git-add and git-stash, which was previously blocked by GUARD_PATHSPEC and a pathspec mask in parse_pathspec()). However, we fix a bug in prefix_magic() where attr values were unintentionally removed. This was triggerable when parse_pathspec() is called with PATHSPEC_PREFIX_ORIGIN as a flag, which was the case for git-stash (Bug originally filed here [*]) Furthermore, while other commands hit this code path it did not result in unexpected behavior because this bug only impacts the pathspec->items->original field which is NOT used to filter paths. However, git-stash does use pathspec->items->original when building args used to call other git commands. (See add_pathspecs() usage and implementation in stash.c) It is possible that when the attr pathspec feature was first added in b0db704652 (pathspec: allow querying for attributes, 2017-03-13), "PATHSPEC_ATTR" was just unintentionally left out of a few GUARD_PATHSPEC() invocations. Later, to get a more user-friendly error message when attr was used with git-add, PATHSPEC_ATTR was added as a mask to git-add's invocation of parse_pathspec() 84d938b732 (add: do not accept pathspec magic 'attr', 2018-09-18). However, this user-friendly error message was never added for git-stash. [Reference] * https://lore.kernel.org/git/CAMmZTi-0QKtj7Q=sbC5qhipGsQxJFOY-Qkk1jfkRYwfF5FcUVg@mail.gmail.com/) Signed-off-by: Joanna Wang <jojwang@google.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Joanna Wang committed Nov 3, 2023 at 16:34 UTC 1164c7232e2773917c0fcf9a98b3e7a417a7644f
4 files changed +139 -18
builtin/add.c
+4 -3
@@ -424,7 +424,7 @@ int cmd_add(int argc, const char **argv, const char *prefix)
424 * Check the "pathspec '%s' did not match any files" block
425 * below before enabling new magic.
426 */
427 - parse_pathspec(&pathspec, PATHSPEC_ATTR,
427 + parse_pathspec(&pathspec, 0,
428 PATHSPEC_PREFER_FULL |
429 PATHSPEC_SYMLINK_LEADING_PATH,
430 prefix, argv);
@@ -433,7 +433,7 @@ int cmd_add(int argc, const char **argv, const char *prefix)
433 if (pathspec.nr)
434 die(_("'%s' and pathspec arguments cannot be used together"), "--pathspec-from-file");
435
436 - parse_pathspec_file(&pathspec, PATHSPEC_ATTR,
436 + parse_pathspec_file(&pathspec, 0,
437 PATHSPEC_PREFER_FULL |
438 PATHSPEC_SYMLINK_LEADING_PATH,
439 prefix, pathspec_from_file, pathspec_file_nul);
@@ -504,7 +504,8 @@ int cmd_add(int argc, const char **argv, const char *prefix)
504 PATHSPEC_LITERAL |
505 PATHSPEC_GLOB |
506 PATHSPEC_ICASE |
507 - PATHSPEC_EXCLUDE);
507 + PATHSPEC_EXCLUDE |
508 + PATHSPEC_ATTR);
509
510 for (i = 0; i < pathspec.nr; i++) {
511 const char *path = pathspec.items[i].match;
dir.c
+2 -1
@@ -2179,7 +2179,8 @@ static int exclude_matches_pathspec(const char *path, int pathlen,
2179 PATHSPEC_LITERAL |
2180 PATHSPEC_GLOB |
2181 PATHSPEC_ICASE |
2182 - PATHSPEC_EXCLUDE);
2182 + PATHSPEC_EXCLUDE |
2183 + PATHSPEC_ATTR);
2184
2185 for (i = 0; i < pathspec->nr; i++) {
2186 const struct pathspec_item *item = &pathspec->items[i];
pathspec.c
+30 -9
@@ -109,16 +109,37 @@ static struct pathspec_magic {
109 { PATHSPEC_ATTR, '\0', "attr" },
110 };
111
112 -static void prefix_magic(struct strbuf *sb, int prefixlen, unsigned magic)
112 +static void prefix_magic(struct strbuf *sb, int prefixlen,
113 + unsigned magic, const char *element)
114 {
114 - int i;
115 - strbuf_addstr(sb, ":(");
116 - for (i = 0; i < ARRAY_SIZE(pathspec_magic); i++)
117 - if (magic & pathspec_magic[i].bit) {
118 - if (sb->buf[sb->len - 1] != '(')
119 - strbuf_addch(sb, ',');
120 - strbuf_addstr(sb, pathspec_magic[i].name);
115 + /* No magic was found in element, just add prefix magic */
116 + if (!magic) {
117 + strbuf_addf(sb, ":(prefix:%d)", prefixlen);
118 + return;
119 + }
120 +
121 + /*
122 + * At this point, we know that parse_element_magic() was able
123 + * to extract some pathspec magic from element. So we know
124 + * element is correctly formatted in either shorthand or
125 + * longhand form
126 + */
127 + if (element[1] != '(') {
128 + /* Process an element in shorthand form (e.g. ":!/<match>") */
129 + strbuf_addstr(sb, ":(");
130 + for (int i = 0; i < ARRAY_SIZE(pathspec_magic); i++) {
131 + if ((magic & pathspec_magic[i].bit) &&
132 + pathspec_magic[i].mnemonic) {
133 + if (sb->buf[sb->len - 1] != '(')
134 + strbuf_addch(sb, ',');
135 + strbuf_addstr(sb, pathspec_magic[i].name);
136 + }
137 }
138 + } else {
139 + /* For the longhand form, we copy everything up to the final ')' */
140 + size_t len = strchr(element, ')') - element;
141 + strbuf_add(sb, element, len);
142 + }
143 strbuf_addf(sb, ",prefix:%d)", prefixlen);
144 }
145
@@ -493,7 +514,7 @@ static void init_pathspec_item(struct pathspec_item *item, unsigned flags,
514 struct strbuf sb = STRBUF_INIT;
515
516 /* Preserve the actual prefix length of each pattern */
496 - prefix_magic(&sb, prefixlen, element_magic);
517 + prefix_magic(&sb, prefixlen, element_magic, elt);
518
519 strbuf_addstr(&sb, match);
520 item->original = strbuf_detach(&sb, NULL);
t/t6135-pathspec-with-attrs.sh
+103 -5
@@ -64,12 +64,24 @@ test_expect_success 'setup .gitattributes' '
64 fileSetLabel label
65 fileValue label=foo
66 fileWrongLabel label☺
67 + newFileA* labelA
68 + newFileB* labelB
69 EOF
70 echo fileSetLabel label1 >sub/.gitattributes &&
71 git add .gitattributes sub/.gitattributes &&
72 git commit -m "add attributes"
73 '
74
75 +test_expect_success 'setup .gitignore' '
76 + cat <<-\EOF >.gitignore &&
77 + actual
78 + expect
79 + pathspec_file
80 + EOF
81 + git add .gitignore &&
82 + git commit -m "add gitignore"
83 +'
84 +
85 test_expect_success 'check specific set attr' '
86 cat <<-\EOF >expect &&
87 fileSetLabel
@@ -150,6 +162,7 @@ test_expect_success 'check specific value attr (2)' '
162 test_expect_success 'check unspecified attr' '
163 cat <<-\EOF >expect &&
164 .gitattributes
165 + .gitignore
166 fileA
167 fileAB
168 fileAC
@@ -175,6 +188,7 @@ test_expect_success 'check unspecified attr' '
188 test_expect_success 'check unspecified attr (2)' '
189 cat <<-\EOF >expect &&
190 HEAD:.gitattributes
191 + HEAD:.gitignore
192 HEAD:fileA
193 HEAD:fileAB
194 HEAD:fileAC
@@ -200,6 +214,7 @@ test_expect_success 'check unspecified attr (2)' '
214 test_expect_success 'check multiple unspecified attr' '
215 cat <<-\EOF >expect &&
216 .gitattributes
217 + .gitignore
218 fileC
219 fileNoLabel
220 fileWrongLabel
@@ -239,16 +254,99 @@ test_expect_success 'fail on multiple attr specifiers in one pathspec item' '
254 test_i18ngrep "Only one" actual
255 '
256
242 -test_expect_success 'fail if attr magic is used places not implemented' '
257 +test_expect_success 'fail if attr magic is used in places not implemented' '
258 # The main purpose of this test is to check that we actually fail
259 # when you attempt to use attr magic in commands that do not implement
245 - # attr magic. This test does not advocate git-add to stay that way,
246 - # though, but git-add is convenient as it has its own internal pathspec
247 - # parsing.
248 - test_must_fail git add ":(attr:labelB)" 2>actual &&
260 + # attr magic. This test does not advocate check-ignore to stay that way.
261 + # When you teach the command to grok the pathspec, you need to find
262 + # another command to replace it for the test.
263 + test_must_fail git check-ignore ":(attr:labelB)" 2>actual &&
264 test_i18ngrep "magic not supported" actual
265 '
266
267 +test_expect_success 'check that attr magic works for git stash push' '
268 + cat <<-\EOF >expect &&
269 + A sub/newFileA-foo
270 + EOF
271 + >sub/newFileA-foo &&
272 + >sub/newFileB-foo &&
273 + git stash push --include-untracked -- ":(exclude,attr:labelB)" &&
274 + git stash show --include-untracked --name-status >actual &&
275 + test_cmp expect actual
276 +'
277 +
278 +test_expect_success 'check that attr magic works for git add --all' '
279 + cat <<-\EOF >expect &&
280 + sub/newFileA-foo
281 + EOF
282 + >sub/newFileA-foo &&
283 + >sub/newFileB-foo &&
284 + git add --all ":(exclude,attr:labelB)" &&
285 + git diff --name-only --cached >actual &&
286 + git restore -W -S . &&
287 + test_cmp expect actual
288 +'
289 +
290 +test_expect_success 'check that attr magic works for git add -u' '
291 + cat <<-\EOF >expect &&
292 + sub/fileA
293 + EOF
294 + >sub/newFileA-foo &&
295 + >sub/newFileB-foo &&
296 + >sub/fileA &&
297 + >sub/fileB &&
298 + git add -u ":(exclude,attr:labelB)" &&
299 + git diff --name-only --cached >actual &&
300 + git restore -S -W . && rm sub/new* &&
301 + test_cmp expect actual
302 +'
303 +
304 +test_expect_success 'check that attr magic works for git add <path>' '
305 + cat <<-\EOF >expect &&
306 + fileA
307 + fileB
308 + sub/fileA
309 + EOF
310 + >fileA &&
311 + >fileB &&
312 + >sub/fileA &&
313 + >sub/fileB &&
314 + git add ":(exclude,attr:labelB)sub/*" &&
315 + git diff --name-only --cached >actual &&
316 + git restore -S -W . &&
317 + test_cmp expect actual
318 +'
319 +
320 +test_expect_success 'check that attr magic works for git -add .' '
321 + cat <<-\EOF >expect &&
322 + sub/fileA
323 + EOF
324 + >fileA &&
325 + >fileB &&
326 + >sub/fileA &&
327 + >sub/fileB &&
328 + cd sub &&
329 + git add . ":(exclude,attr:labelB)" &&
330 + cd .. &&
331 + git diff --name-only --cached >actual &&
332 + git restore -S -W . &&
333 + test_cmp expect actual
334 +'
335 +
336 +test_expect_success 'check that attr magic works for git add --pathspec-from-file' '
337 + cat <<-\EOF >pathspec_file &&
338 + :(exclude,attr:labelB)
339 + EOF
340 + cat <<-\EOF >expect &&
341 + sub/newFileA-foo
342 + EOF
343 + >sub/newFileA-foo &&
344 + >sub/newFileB-foo &&
345 + git add --all --pathspec-from-file=pathspec_file &&
346 + git diff --name-only --cached >actual &&
347 + test_cmp expect actual
348 +'
349 +
350 test_expect_success 'abort on giving invalid label on the command line' '
351 test_must_fail git ls-files . ":(attr:☺)"
352 '