revision: handle pseudo-opts in `--stdin` mode

While both git-rev-list(1) and git-log(1) support `--stdin`, it only accepts commits and files. Most notably, it is impossible to pass any of the pseudo-opts like `--all`, `--glob=` or others via stdin. This makes it hard to use this function in certain scripted scenarios, like when one wants to support queries against specific revisions, but also against reference patterns. While this is theoretically possible by using arguments, this may run into issues once we hit platform limits with sufficiently large queries. And because `--stdin` cannot handle pseudo-opts, the only alternative would be to use a mixture of arguments and standard input, which is cumbersome. Implement support for handling pseudo-opts in both commands to support this usecase better. One notable restriction here is that `--stdin` only supports "stuck" arguments in the form of `--glob=foo`. This is because "unstuck" arguments would also require us to read the next line, which would add quite some complexity to the code. This restriction should be fine for scripted usage though. Signed-off-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrick Steinhardt committed Jun 15, 2023 at 16:39 UTC c40f0b78771ecc8696014e2e0aa37e78417b4723
3 files changed +72 -9
Documentation/rev-list-options.txt
+5 -4
@@ -236,10 +236,11 @@ ifndef::git-rev-list[]
236 endif::git-rev-list[]
237
238 --stdin::
239 - In addition to the '<commit>' listed on the command
240 - line, read them from the standard input. If a `--` separator is
241 - seen, stop reading commits and start reading paths to limit the
242 - result.
239 + In addition to getting arguments from the command line, read
240 + them for standard input as well. This accepts commits and
241 + pseudo-options like `--all` and `--glob=`. When a `--` separator
242 + is seen, the following input is treated as paths and used to
243 + limit the result.
244
245 ifdef::git-rev-list[]
246 --quiet::
revision.c
+17 -4
@@ -2784,10 +2784,12 @@ static int handle_revision_pseudo_opt(struct rev_info *revs,
2784 }
2785
2786 static void read_revisions_from_stdin(struct rev_info *revs,
2787 - struct strvec *prune)
2787 + struct strvec *prune,
2788 + int *flags)
2789 {
2790 struct strbuf sb;
2791 int seen_dashdash = 0;
2792 + int seen_end_of_options = 0;
2793 int save_warning;
2794
2795 save_warning = warn_on_object_refname_ambiguity;
@@ -2803,8 +2805,19 @@ static void read_revisions_from_stdin(struct rev_info *revs,
2805 break;
2806 }
2807
2806 - if (sb.buf[0] == '-')
2807 - die("options not supported in --stdin mode");
2808 + if (!seen_end_of_options && sb.buf[0] == '-') {
2809 + const char *argv[] = { sb.buf, NULL };
2810 +
2811 + if (!strcmp(sb.buf, "--end-of-options")) {
2812 + seen_end_of_options = 1;
2813 + continue;
2814 + }
2815 +
2816 + if (handle_revision_pseudo_opt(revs, argv, flags) > 0)
2817 + continue;
2818 +
2819 + die(_("invalid option '%s' in --stdin mode"), sb.buf);
2820 + }
2821
2822 if (handle_revision_arg(sb.buf, revs, 0,
2823 REVARG_CANNOT_BE_FILENAME))
@@ -2889,7 +2902,7 @@ int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct s
2902 }
2903 if (revs->read_from_stdin++)
2904 die("--stdin given twice?");
2892 - read_revisions_from_stdin(revs, &prune_data);
2905 + read_revisions_from_stdin(revs, &prune_data, &flags);
2906 continue;
2907 }
2908
t/t6017-rev-list-stdin.sh
+50 -1
@@ -48,7 +48,9 @@ test_expect_success setup '
48 git add file-$i &&
49 test_tick &&
50 git commit -m side-$i || exit
51 - done
51 + done &&
52 +
53 + git update-ref refs/heads/-dashed-branch HEAD
54 )
55 '
56
@@ -60,6 +62,12 @@ check side-1 ^side-7 -- file-2
62 check side-3 ^side-4 -- file-3
63 check side-3 ^side-2
64 check side-3 ^side-2 -- file-1
65 +check --all
66 +check --all --not --branches
67 +check --glob=refs/heads
68 +check --glob=refs/heads --
69 +check --glob=refs/heads -- file-1
70 +check --end-of-options -dashed-branch
71
72 test_expect_success 'not only --stdin' '
73 cat >expect <<-EOF &&
@@ -78,4 +86,45 @@ test_expect_success 'not only --stdin' '
86 test_cmp expect actual
87 '
88
89 +test_expect_success 'pseudo-opt with missing value' '
90 + cat >input <<-EOF &&
91 + --glob
92 + refs/heads
93 + EOF
94 +
95 + cat >expect <<-EOF &&
96 + fatal: Option ${SQ}--glob${SQ} requires a value
97 + EOF
98 +
99 + test_must_fail git rev-list --stdin <input 2>error &&
100 + test_cmp expect error
101 +'
102 +
103 +test_expect_success 'pseudo-opt with invalid value' '
104 + cat >input <<-EOF &&
105 + --no-walk=garbage
106 + EOF
107 +
108 + cat >expect <<-EOF &&
109 + error: invalid argument to --no-walk
110 + fatal: invalid option ${SQ}--no-walk=garbage${SQ} in --stdin mode
111 + EOF
112 +
113 + test_must_fail git rev-list --stdin <input 2>error &&
114 + test_cmp expect error
115 +'
116 +
117 +test_expect_success 'unknown option without --end-of-options' '
118 + cat >input <<-EOF &&
119 + -dashed-branch
120 + EOF
121 +
122 + cat >expect <<-EOF &&
123 + fatal: invalid option ${SQ}-dashed-branch${SQ} in --stdin mode
124 + EOF
125 +
126 + test_must_fail git rev-list --stdin <input 2>error &&
127 + test_cmp expect error
128 +'
129 +
130 test_done