Hi Stolee
On 10/03/2023 17:20, Derrick Stolee via GitGitGadget wrote:
From: Derrick Stolee <derrickstolee@xxxxxxxxxx>
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
requires a careful cast while freeing the individual strings,
conditioned on the --stdin option.
I think what you've got here is fine, but if you wanted you could
simplify it by using an strvec. Something like
struct strvec vec = STRVEC_INIT;
...
if (from_stdin) {
struct strbuf buf = STRBUF_INIT;
if (argv[0])
die(_("unknown arguments supplied with --stdin"));
while (strbuf_getline(&line, stdin) != EOF)
strvec_push(&vec, buf.buf);
filter.name_patters = vec.v;
} else {
filter.name_patterns = argv;
}
...
strvec_clear(&vec);
gets rid of the manual memory management with ALLOC_GROW() and the need
to cast filter.name_patterns when free()ing. It is not immediately
obvious from the name but struct strvec keeps the array NULL terminated.
Best Wishes
Phillip
Signed-off-by: Derrick Stolee <derrickstolee@xxxxxxxxxx>
---
Documentation/git-for-each-ref.txt | 7 +++++-
builtin/for-each-ref.c | 29 ++++++++++++++++++++++-
t/t6300-for-each-ref.sh | 37 ++++++++++++++++++++++++++++++
3 files changed, 71 insertions(+), 2 deletions(-)
diff --git a/Documentation/git-for-each-ref.txt b/Documentation/git-for-each-ref.txt
index 6da899c6296..ccdc2911bb9 100644
--- a/Documentation/git-for-each-ref.txt
+++ b/Documentation/git-for-each-ref.txt
@@ -9,7 +9,8 @@ SYNOPSIS
--------
[verse]
'git for-each-ref' [--count=<count>] [--shell|--perl|--python|--tcl]
- [(--sort=<key>)...] [--format=<format>] [<pattern>...]
+ [(--sort=<key>)...] [--format=<format>]
+ [ --stdin | <pattern>... ]
[--points-at=<object>]
[--merged[=<object>]] [--no-merged[=<object>]]
[--contains[=<object>]] [--no-contains[=<object>]]
@@ -32,6 +33,10 @@ OPTIONS
literally, in the latter case matching completely or from the
beginning up to a slash.
+--stdin::
+ If `--stdin` is supplied, then the list of patterns is read from
+ standard input instead of from the argument list.
+
--count=<count>::
By default the command shows all refs that match
`<pattern>`. This option makes it stop after showing
diff --git a/builtin/for-each-ref.c b/builtin/for-each-ref.c
index 6f62f40d126..e005a7ef3ce 100644
--- a/builtin/for-each-ref.c
+++ b/builtin/for-each-ref.c
@@ -25,6 +25,7 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
struct ref_format format = REF_FORMAT_INIT;
struct strbuf output = STRBUF_INIT;
struct strbuf err = STRBUF_INIT;
+ int from_stdin = 0;
struct option opts[] = {
OPT_BIT('s', "shell", &format.quote_style,
@@ -49,6 +50,7 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
OPT_CONTAINS(&filter.with_commit, N_("print only refs which contain the commit")),
OPT_NO_CONTAINS(&filter.no_commit, N_("print only refs which don't contain the commit")),
OPT_BOOL(0, "ignore-case", &icase, N_("sorting and filtering are case insensitive")),
+ OPT_BOOL(0, "stdin", &from_stdin, N_("read reference patterns from stdin")),
OPT_END(),
};
@@ -75,7 +77,27 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
ref_sorting_set_sort_flags_all(sorting, REF_SORTING_ICASE, icase);
filter.ignore_case = icase;
- filter.name_patterns = argv;
+ if (from_stdin) {
+ struct strbuf line = STRBUF_INIT;
+ size_t nr = 0, alloc = 16;
+
+ if (argv[0])
+ die(_("unknown arguments supplied with --stdin"));
+
+ CALLOC_ARRAY(filter.name_patterns, alloc);
+
+ while (strbuf_getline(&line, stdin) != EOF) {
+ ALLOC_GROW(filter.name_patterns, nr + 1, alloc);
+ filter.name_patterns[nr++] = strbuf_detach(&line, NULL);
+ }
+
+ /* Add a terminating NULL string. */
+ ALLOC_GROW(filter.name_patterns, nr + 1, alloc);
+ filter.name_patterns[nr + 1] = NULL;
+ } else {
+ filter.name_patterns = argv;
+ }
+
filter.match_as_path = 1;
filter_refs(&array, &filter, FILTER_REFS_ALL);
ref_array_sort(sorting, &array);
@@ -97,5 +119,10 @@ int cmd_for_each_ref(int argc, const char **argv, const char *prefix)
free_commit_list(filter.with_commit);
free_commit_list(filter.no_commit);
ref_sorting_release(sorting);
+ if (from_stdin) {
+ for (size_t i = 0; filter.name_patterns[i]; i++)
+ free((char *)filter.name_patterns[i]);
+ free(filter.name_patterns);
+ }
return 0;
}
diff --git a/t/t6300-for-each-ref.sh b/t/t6300-for-each-ref.sh
index c466fd989f1..a58053a54c5 100755
--- a/t/t6300-for-each-ref.sh
+++ b/t/t6300-for-each-ref.sh
@@ -1464,4 +1464,41 @@ sig_crlf="$(printf "%s" "$sig" | append_cr; echo dummy)"
sig_crlf=${sig_crlf%dummy}
test_atom refs/tags/fake-sig-crlf contents:signature "$sig_crlf"
+test_expect_success 'git for-each-ref --stdin: empty' '
+ >in &&
+ git for-each-ref --format="%(refname)" --stdin <in >actual &&
+ git for-each-ref --format="%(refname)" >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success 'git for-each-ref --stdin: fails if extra args' '
+ >in &&
+ test_must_fail git for-each-ref --format="%(refname)" \
+ --stdin refs/heads/extra <in 2>err &&
+ grep "unknown arguments supplied with --stdin" err
+'
+
+test_expect_success 'git for-each-ref --stdin: matches' '
+ cat >in <<-EOF &&
+ refs/tags/multi*
+ refs/heads/amb*
+ EOF
+
+ cat >expect <<-EOF &&
+ refs/heads/ambiguous
+ refs/tags/multi-ref1-100000-user1
+ refs/tags/multi-ref1-100000-user2
+ refs/tags/multi-ref1-200000-user1
+ refs/tags/multi-ref1-200000-user2
+ refs/tags/multi-ref2-100000-user1
+ refs/tags/multi-ref2-100000-user2
+ refs/tags/multi-ref2-200000-user1
+ refs/tags/multi-ref2-200000-user2
+ refs/tags/multiline
+ EOF
+
+ git for-each-ref --format="%(refname)" --stdin <in >actual &&
+ test_cmp expect actual
+'
+
test_done