run-command: allow capturing of collated output

Some callers, for example server-side hooks which wish to relay hook output to clients across a transport, want to capture what would normally print to stderr and do something else with it. Allow that via a callback. By calling the callback regardless of whether there's output available, we allow clients to send e.g. a keepalive if necessary. Because we expose a strbuf, not a fd or FILE*, there's no need to create a temporary pipe or similar - we can just skip the print to stderr and instead hand it to the caller. Signed-off-by: Emily Shaffer <emilyshaffer@google.com> Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Emily Shaffer committed Dec 26, 2025 at 14:23 UTC 5ab5872a53296b009cca43d412efd1a74ea4f149
4 files changed +61 -8
run-command.c
+22 -8
@@ -1595,7 +1595,10 @@ static void pp_cleanup(struct parallel_processes *pp,
1595 * When get_next_task added messages to the buffer in its last
1596 * iteration, the buffered output is non empty.
1597 */
1598 - strbuf_write(&pp->buffered_output, stderr);
1598 + if (opts->consume_output)
1599 + opts->consume_output(&pp->buffered_output, opts->data);
1600 + else
1601 + strbuf_write(&pp->buffered_output, stderr);
1602 strbuf_release(&pp->buffered_output);
1603
1604 sigchain_pop_common();
@@ -1734,13 +1737,17 @@ static void pp_buffer_stderr(struct parallel_processes *pp,
1737 }
1738 }
1739
1737 -static void pp_output(const struct parallel_processes *pp)
1740 +static void pp_output(const struct parallel_processes *pp,
1741 + const struct run_process_parallel_opts *opts)
1742 {
1743 size_t i = pp->output_owner;
1744
1745 if (child_is_working(&pp->children[i]) &&
1746 pp->children[i].err.len) {
1743 - strbuf_write(&pp->children[i].err, stderr);
1747 + if (opts->consume_output)
1748 + opts->consume_output(&pp->children[i].err, opts->data);
1749 + else
1750 + strbuf_write(&pp->children[i].err, stderr);
1751 strbuf_reset(&pp->children[i].err);
1752 }
1753 }
@@ -1788,11 +1795,15 @@ static int pp_collect_finished(struct parallel_processes *pp,
1795 } else {
1796 const size_t n = opts->processes;
1797
1791 - strbuf_write(&pp->children[i].err, stderr);
1798 + /* Output errors, then all other finished child processes */
1799 + if (opts->consume_output) {
1800 + opts->consume_output(&pp->children[i].err, opts->data);
1801 + opts->consume_output(&pp->buffered_output, opts->data);
1802 + } else {
1803 + strbuf_write(&pp->children[i].err, stderr);
1804 + strbuf_write(&pp->buffered_output, stderr);
1805 + }
1806 strbuf_reset(&pp->children[i].err);
1793 -
1794 - /* Output all other finished child processes */
1795 - strbuf_write(&pp->buffered_output, stderr);
1807 strbuf_reset(&pp->buffered_output);
1808
1809 /*
@@ -1829,7 +1840,7 @@ static void pp_handle_child_IO(struct parallel_processes *pp,
1840 pp->children[i].state = GIT_CP_WAIT_CLEANUP;
1841 } else {
1842 pp_buffer_stderr(pp, opts, output_timeout);
1832 - pp_output(pp);
1843 + pp_output(pp, opts);
1844 }
1845 }
1846
@@ -1852,6 +1863,9 @@ void run_processes_parallel(const struct run_process_parallel_opts *opts)
1863 "max:%"PRIuMAX,
1864 (uintmax_t)opts->processes);
1865
1866 + if (opts->ungroup && opts->consume_output)
1867 + BUG("ungroup and reading output are mutualy exclusive");
1868 +
1869 /*
1870 * Child tasks might receive input via stdin, terminating early (or not), so
1871 * ignore the default SIGPIPE which gets handled by each feed_pipe_fn which
run-command.h
+17
@@ -435,6 +435,17 @@ typedef int (*feed_pipe_fn)(int child_in,
435 void *pp_cb,
436 void *pp_task_cb);
437
438 +/**
439 + * If this callback is provided, output is collated into a new pipe instead
440 + * of the process stderr. Then `consume_output_fn` will be called repeatedly
441 + * with output contained in the `output` arg. It will also be called with an
442 + * empty `output` to allow for keepalives or similar operations if necessary.
443 + *
444 + * pp_cb is the callback cookie as passed into run_processes_parallel.
445 + * No task cookie is provided because the callback receives collated output.
446 + */
447 +typedef void (*consume_output_fn)(struct strbuf *output, void *pp_cb);
448 +
449 /**
450 * This callback is called on every child process that finished processing.
451 *
@@ -494,6 +505,12 @@ struct run_process_parallel_opts
505 */
506 feed_pipe_fn feed_pipe;
507
508 + /*
509 + * consume_output: see consume_output_fn() above. This can be NULL
510 + * to omit any special handling.
511 + */
512 + consume_output_fn consume_output;
513 +
514 /**
515 * task_finished: See task_finished_fn() above. This can be
516 * NULL to omit any special handling.
t/helper/test-run-command.c
+15
@@ -58,6 +58,16 @@ static int no_job(struct child_process *cp UNUSED,
58 return 0;
59 }
60
61 +static void test_divert_output(struct strbuf *output, void *cb UNUSED)
62 +{
63 + FILE *output_file;
64 +
65 + output_file = fopen("./output_file", "a");
66 +
67 + strbuf_write(output, output_file);
68 + fclose(output_file);
69 +}
70 +
71 static int task_finished(int result UNUSED,
72 struct strbuf *err,
73 void *pp_cb UNUSED,
@@ -198,6 +208,7 @@ static int testsuite(int argc, const char **argv)
208 .get_next_task = next_test,
209 .start_failure = test_failed,
210 .feed_pipe = test_stdin_pipe_feed,
211 + .consume_output = test_divert_output,
212 .task_finished = test_finished,
213 .data = &suite,
214 };
@@ -514,6 +525,10 @@ int cmd__run_command(int argc, const char **argv)
525 opts.get_next_task = parallel_next;
526 opts.task_finished = task_finished_quiet;
527 opts.feed_pipe = test_stdin_pipe_feed;
528 + } else if (!strcmp(argv[1], "run-command-divert-output")) {
529 + opts.get_next_task = parallel_next;
530 + opts.consume_output = test_divert_output;
531 + opts.task_finished = task_finished_quiet;
532 } else {
533 ret = 1;
534 fprintf(stderr, "check usage\n");
t/t0061-run-command.sh
+7
@@ -164,6 +164,13 @@ test_expect_success 'run_command runs ungrouped in parallel with more tasks than
164 test_line_count = 4 err
165 '
166
167 +test_expect_success 'run_command can divert output' '
168 + test_when_finished rm output_file &&
169 + test-tool run-command run-command-divert-output 3 sh -c "printf \"%s\n%s\n\" Hello World" 2>actual &&
170 + test_must_be_empty actual &&
171 + test_cmp expect output_file
172 +'
173 +
174 test_expect_success 'run_command listens to stdin' '
175 cat >expect <<-\EOF &&
176 preloaded output of a child