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 committedApr 10, 2026 at 12:06 UTCf776b77f0032fb342d567156626ef3fe9586443f
5 files changed+60-12
Documentation/config/hook.adoc
+4
index 21800db648..94c7a9808e 100644--- a/Documentation/config/hook.adoc+++ b/Documentation/config/hook.adoc@@ -39,3 +39,7 @@ hook.jobs:: + This setting has no effect unless all configured hooks for the event have `hook.<friendly-name>.parallel` set to `true`.+++For `pre-push` hooks, which normally keep stdout and stderr separate,+setting this to a value greater than 1 (or passing `-j`) will merge stdout+into stderr to allow correct de-interleaving of parallel output.
hook.c
+16-8
index 85c0de5e47..25762b6c8d 100644--- a/hook.c+++ b/hook.c@@ -555,18 +555,24 @@ static void run_hooks_opt_clear(struct run_hooks_opt *options) strvec_clear(&options->args); }+/*+ * When running in parallel, stdout must be merged into stderr so+ * run-command can buffer and de-interleave outputs correctly. This+ * applies even to hooks like pre-push that normally keep stdout and+ * stderr separate: the user has opted into parallelism, so the output+ * stream behavior changes accordingly.+ */+static void merge_output_if_parallel(struct run_hooks_opt *options)+{+ if (options->jobs > 1)+ options->stdout_to_stderr = 1;+}+ /* Determine how many jobs to use for hook execution. */ static unsigned int get_hook_jobs(struct repository *r, struct run_hooks_opt *options, struct string_list *hook_list) {- /*- * Hooks needing separate output streams must run sequentially.- * Next commit will allow parallelizing these as well.- */- if (!options->stdout_to_stderr)- return 1;- /* * An explicit job count overrides everything else: this covers both * FORCE_SERIAL callers (for hooks that must never run in parallel)@@ -575,7 +581,7 @@ static unsigned int get_hook_jobs(struct repository *r, * aggressively than the default. */ if (options->jobs)- return options->jobs;+ goto cleanup; /* * 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, } }+cleanup:+ merge_output_if_parallel(options); return options->jobs; }
hook.h
+4-2
index ba7056f872..01db4226a6 100644--- a/hook.h+++ b/hook.h@@ -106,8 +106,10 @@ struct run_hooks_opt { * Send the hook's stdout to stderr. * * This is the default behavior for all hooks except pre-push,- * which has separate stdout and stderr streams for backwards- * compatibility reasons.+ * which keeps stdout and stderr separate for backwards compatibility.+ * When parallel execution is requested (jobs > 1), get_hook_jobs()+ * overrides this to 1 for all hooks so run-command can de-interleave+ * their outputs correctly. */ unsigned int stdout_to_stderr:1;
index e53936d87b..9406ec4f2d 100644--- a/transport.c+++ b/transport.c@@ -1391,8 +1391,10 @@ static int run_pre_push_hook(struct transport *transport, opt.feed_pipe_cb_data_free = pre_push_hook_data_free; /*- * pre-push hooks expect stdout & stderr to be separate, so don't merge- * them to keep backwards compatibility with existing hooks.+ * pre-push hooks keep stdout and stderr separate by default for+ * backwards compatibility. When the user opts into parallel execution+ * via hook.jobs > 1 or -j, get_hook_jobs() will set stdout_to_stderr=1+ * automatically so run-command can de-interleave the outputs. */ opt.stdout_to_stderr = 0;