hook: allow pre-push parallel execution

pre-push is the only hook that keeps stdout and stderr separate (for backwards compatibility with git-lfs and potentially other users). This prevents parallelizing it because run-command needs stdout_to_stderr=1 to buffer and de-interleave parallel outputs. Since we now default to jobs=1, backwards compatibility is maintained without needing any extension or extra config: when no parallelism is requested, pre-push behaves exactly as before. When the user explicitly opts into parallelism via hook.jobs > 1, hook.<event>.jobs > 1, or -jN, they accept the changed output behavior. Document this and let get_hook_jobs() set stdout_to_stderr=1 automatically when jobs > 1, removing the need for any extension infrastructure. Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Adrian Ratiu committed Apr 10, 2026 at 12:06 UTC f776b77f0032fb342d567156626ef3fe9586443f
5 files changed +60 -12
Documentation/config/hook.adoc
+4
@@ -39,3 +39,7 @@ hook.jobs::
39 +
40 This setting has no effect unless all configured hooks for the event have
41 `hook.<friendly-name>.parallel` set to `true`.
42 ++
43 +For `pre-push` hooks, which normally keep stdout and stderr separate,
44 +setting this to a value greater than 1 (or passing `-j`) will merge stdout
45 +into stderr to allow correct de-interleaving of parallel output.
hook.c
+16 -8
@@ -555,18 +555,24 @@ static void run_hooks_opt_clear(struct run_hooks_opt *options)
555 strvec_clear(&options->args);
556 }
557
558 +/*
559 + * When running in parallel, stdout must be merged into stderr so
560 + * run-command can buffer and de-interleave outputs correctly. This
561 + * applies even to hooks like pre-push that normally keep stdout and
562 + * stderr separate: the user has opted into parallelism, so the output
563 + * stream behavior changes accordingly.
564 + */
565 +static void merge_output_if_parallel(struct run_hooks_opt *options)
566 +{
567 + if (options->jobs > 1)
568 + options->stdout_to_stderr = 1;
569 +}
570 +
571 /* Determine how many jobs to use for hook execution. */
572 static unsigned int get_hook_jobs(struct repository *r,
573 struct run_hooks_opt *options,
574 struct string_list *hook_list)
575 {
563 - /*
564 - * Hooks needing separate output streams must run sequentially.
565 - * Next commit will allow parallelizing these as well.
566 - */
567 - if (!options->stdout_to_stderr)
568 - return 1;
569 -
576 /*
577 * An explicit job count overrides everything else: this covers both
578 * FORCE_SERIAL callers (for hooks that must never run in parallel)
@@ -575,7 +581,7 @@ static unsigned int get_hook_jobs(struct repository *r,
581 * aggressively than the default.
582 */
583 if (options->jobs)
578 - return options->jobs;
584 + goto cleanup;
585
586 /*
587 * Use hook.jobs from the already-parsed config cache (in-repo), or
@@ -603,6 +609,8 @@ static unsigned int get_hook_jobs(struct repository *r,
609 }
610 }
611
612 +cleanup:
613 + merge_output_if_parallel(options);
614 return options->jobs;
615 }
616
hook.h
+4 -2
@@ -106,8 +106,10 @@ struct run_hooks_opt {
106 * Send the hook's stdout to stderr.
107 *
108 * This is the default behavior for all hooks except pre-push,
109 - * which has separate stdout and stderr streams for backwards
110 - * compatibility reasons.
109 + * which keeps stdout and stderr separate for backwards compatibility.
110 + * When parallel execution is requested (jobs > 1), get_hook_jobs()
111 + * overrides this to 1 for all hooks so run-command can de-interleave
112 + * their outputs correctly.
113 */
114 unsigned int stdout_to_stderr:1;
115
t/t1800-hook.sh
+32
@@ -800,4 +800,36 @@ test_expect_success 'one non-parallel hook forces the whole event to run seriall
800 test_cmp expect hook.order
801 '
802
803 +test_expect_success 'client hooks: pre-push parallel execution merges stdout to stderr' '
804 + test_when_finished "rm -rf remote-par stdout.actual stderr.actual" &&
805 + git init --bare remote-par &&
806 + git remote add origin-par remote-par &&
807 + test_commit par-commit &&
808 + mkdir -p .git/hooks &&
809 + setup_hooks pre-push &&
810 + test_config hook.jobs 2 &&
811 + git push origin-par HEAD:main >stdout.actual 2>stderr.actual &&
812 + check_stdout_merged_to_stderr pre-push
813 +'
814 +
815 +test_expect_success 'client hooks: pre-push runs in parallel when hook.jobs > 1' '
816 + test_when_finished "rm -rf repo-parallel remote-parallel" &&
817 + git init --bare remote-parallel &&
818 + git init repo-parallel &&
819 + git -C repo-parallel remote add origin ../remote-parallel &&
820 + test_commit -C repo-parallel A &&
821 +
822 + write_sentinel_hook repo-parallel/.git/hooks/pre-push &&
823 + git -C repo-parallel config hook.hook-2.event pre-push &&
824 + git -C repo-parallel config hook.hook-2.command \
825 + "$(sentinel_detector sentinel hook.order)" &&
826 + git -C repo-parallel config hook.hook-2.parallel true &&
827 +
828 + git -C repo-parallel config hook.jobs 2 &&
829 +
830 + git -C repo-parallel push origin HEAD >out 2>err &&
831 + echo parallel >expect &&
832 + test_cmp expect repo-parallel/hook.order
833 +'
834 +
835 test_done
transport.c
+4 -2
@@ -1391,8 +1391,10 @@ static int run_pre_push_hook(struct transport *transport,
1391 opt.feed_pipe_cb_data_free = pre_push_hook_data_free;
1392
1393 /*
1394 - * pre-push hooks expect stdout & stderr to be separate, so don't merge
1395 - * them to keep backwards compatibility with existing hooks.
1394 + * pre-push hooks keep stdout and stderr separate by default for
1395 + * backwards compatibility. When the user opts into parallel execution
1396 + * via hook.jobs > 1 or -j, get_hook_jobs() will set stdout_to_stderr=1
1397 + * automatically so run-command can de-interleave the outputs.
1398 */
1399 opt.stdout_to_stderr = 0;
1400