for-each-ref: add --stdin option

When a user wishes to input a large list of patterns to 'git for-each-ref' (likely a long list of exact refs) there are frequently system limits on the number of command-line arguments. Add a new --stdin option to instead read the patterns from standard input. Add tests that check that any unrecognized arguments are considered an error when --stdin is provided. Also, an empty pattern list is interpreted as the complete ref set. When reading from stdin, we populate the filter.name_patterns array dynamically as opposed to pointing to the 'argv' array directly. This is simple when using a strvec, as it is NULL-terminated in the same way. We then free the memory directly from the strvec. Helped-by: Phillip Wood <phillip.wood123@gmail.com> Signed-off-by: Derrick Stolee <derrickstolee@github.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Derrick Stolee committed Mar 20, 2023 at 11:26 UTC b73dec55309d9b4f7e0db95dba729af0fcafb67f
3 files changed +65 -2
Documentation/git-for-each-ref.txt
+6 -1
@@ -9,7 +9,8 @@ SYNOPSIS
9 --------
10 [verse]
11 'git for-each-ref' [--count=<count>] [--shell|--perl|--python|--tcl]
12 - [(--sort=<key>)...] [--format=<format>] [<pattern>...]
12 + [(--sort=<key>)...] [--format=<format>]
13 + [ --stdin | <pattern>... ]
14 [--points-at=<object>]
15 [--merged[=<object>]] [--no-merged[=<object>]]
16 [--contains[=<object>]] [--no-contains[=<object>]]
@@ -32,6 +33,10 @@ OPTIONS
33 literally, in the latter case matching completely or from the
34 beginning up to a slash.
35
36 +--stdin::
37 + If `--stdin` is supplied, then the list of patterns is read from
38 + standard input instead of from the argument list.
39 +
40 --count=<count>::
41 By default the command shows all refs that match
42 `<pattern>`. This option makes it stop after showing
builtin/for-each-ref.c
+22 -1
@@ -5,6 +5,7 @@
5 #include "object.h"
6 #include "parse-options.h"
7 #include "ref-filter.h"
8 +#include "strvec.h"
9
10 static char const * const for_each_ref_usage[] = {
11 N_("git for-each-ref [<options>] [<pattern>]"),
@@ -25,6 +26,8 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
26 struct ref_format format = REF_FORMAT_INIT;
27 struct strbuf output = STRBUF_INIT;
28 struct strbuf err = STRBUF_INIT;
29 + int from_stdin = 0;
30 + struct strvec vec = STRVEC_INIT;
31
32 struct option opts[] = {
33 OPT_BIT('s', "shell", &format.quote_style,
@@ -49,6 +52,7 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
52 OPT_CONTAINS(&filter.with_commit, N_("print only refs which contain the commit")),
53 OPT_NO_CONTAINS(&filter.no_commit, N_("print only refs which don't contain the commit")),
54 OPT_BOOL(0, "ignore-case", &icase, N_("sorting and filtering are case insensitive")),
55 + OPT_BOOL(0, "stdin", &from_stdin, N_("read reference patterns from stdin")),
56 OPT_END(),
57 };
58
@@ -75,7 +79,23 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
79 ref_sorting_set_sort_flags_all(sorting, REF_SORTING_ICASE, icase);
80 filter.ignore_case = icase;
81
78 - filter.name_patterns = argv;
82 + if (from_stdin) {
83 + struct strbuf line = STRBUF_INIT;
84 +
85 + if (argv[0])
86 + die(_("unknown arguments supplied with --stdin"));
87 +
88 + while (strbuf_getline(&line, stdin) != EOF)
89 + strvec_push(&vec, line.buf);
90 +
91 + strbuf_release(&line);
92 +
93 + /* vec.v is NULL-terminated, just like 'argv'. */
94 + filter.name_patterns = vec.v;
95 + } else {
96 + filter.name_patterns = argv;
97 + }
98 +
99 filter.match_as_path = 1;
100 filter_refs(&array, &filter, FILTER_REFS_ALL);
101 ref_array_sort(sorting, &array);
@@ -97,5 +117,6 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
117 free_commit_list(filter.with_commit);
118 free_commit_list(filter.no_commit);
119 ref_sorting_release(sorting);
120 + strvec_clear(&vec);
121 return 0;
122 }
t/t6300-for-each-ref.sh
+37
@@ -1464,4 +1464,41 @@ sig_crlf="$(printf "%s" "$sig" | append_cr; echo dummy)"
1464 sig_crlf=${sig_crlf%dummy}
1465 test_atom refs/tags/fake-sig-crlf contents:signature "$sig_crlf"
1466
1467 +test_expect_success 'git for-each-ref --stdin: empty' '
1468 + >in &&
1469 + git for-each-ref --format="%(refname)" --stdin <in >actual &&
1470 + git for-each-ref --format="%(refname)" >expect &&
1471 + test_cmp expect actual
1472 +'
1473 +
1474 +test_expect_success 'git for-each-ref --stdin: fails if extra args' '
1475 + >in &&
1476 + test_must_fail git for-each-ref --format="%(refname)" \
1477 + --stdin refs/heads/extra <in 2>err &&
1478 + grep "unknown arguments supplied with --stdin" err
1479 +'
1480 +
1481 +test_expect_success 'git for-each-ref --stdin: matches' '
1482 + cat >in <<-EOF &&
1483 + refs/tags/multi*
1484 + refs/heads/amb*
1485 + EOF
1486 +
1487 + cat >expect <<-EOF &&
1488 + refs/heads/ambiguous
1489 + refs/tags/multi-ref1-100000-user1
1490 + refs/tags/multi-ref1-100000-user2
1491 + refs/tags/multi-ref1-200000-user1
1492 + refs/tags/multi-ref1-200000-user2
1493 + refs/tags/multi-ref2-100000-user1
1494 + refs/tags/multi-ref2-100000-user2
1495 + refs/tags/multi-ref2-200000-user1
1496 + refs/tags/multi-ref2-200000-user2
1497 + refs/tags/multiline
1498 + EOF
1499 +
1500 + git for-each-ref --format="%(refname)" --stdin <in >actual &&
1501 + test_cmp expect actual
1502 +'
1503 +
1504 test_done