Subject: [PATCH v7 11/12] receive-pack: convert update hooks to new API

From: Adrian Ratiu <[email-masked]>
Date: Wed, 21 Jan 2026 23:54:35 +0200

The hook API avoids creating a custom struct child_process and other
internal hook plumbing (e.g. calling find_hook()) and prepares for
the specification of hooks via configs or running parallel hooks.

Execution is still sequential through the run_hooks_opt .jobs == 1,
which is the unchanged default for all hooks.

When jobs==1 the async muxer thread reads the hook stderr and writes
to sideband 2, so run-command's poll loop is avoided and there's no
need for ungroup=0 when running sequentially (Jeff's suggestion).

When running in parallel, run-command with ungroup=0 will capture
and de-interleave the output of each hook, then write to the parent
stderr which is redirected via dup2 to the sideband muxer, so that
parallel hook output is presented clearly to the client.

Suggested-by: Jeff King <[email-masked]>
Signed-off-by: Emily Shaffer <[email-masked]>
Signed-off-by: Ævar Arnfjörð Bjarmason <[email-masked]>
Signed-off-by: Adrian Ratiu <[email-masked]>
---
 builtin/receive-pack.c | 91 +++++++++++++++++++++++++-----------------
 1 file changed, 55 insertions(+), 36 deletions(-)

diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c
index 9c49174616..bcd019786e 100644
--- a/builtin/receive-pack.c
+++ b/builtin/receive-pack.c
@@ -941,29 +941,41 @@ static int run_receive_hook(struct command *commands,
 
 static int run_update_hook(struct command *cmd)
 {
-	struct child_process proc = CHILD_PROCESS_INIT;
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+	struct async muxer;
 	int code;
-	const char *hook_path = find_hook(the_repository, "update");
+	int saved_stderr = -1;
+	int muxer_started = 0;
 
-	if (!hook_path)
-		return 0;
+	strvec_pushl(&opt.args,
+		     cmd->ref_name,
+		     oid_to_hex(&cmd->old_oid),
+		     oid_to_hex(&cmd->new_oid),
+		     NULL);
 
-	strvec_push(&proc.args, hook_path);
-	strvec_push(&proc.args, cmd->ref_name);
-	strvec_push(&proc.args, oid_to_hex(&cmd->old_oid));
-	strvec_push(&proc.args, oid_to_hex(&cmd->new_oid));
+	if (use_sideband) {
+		memset(&muxer, 0, sizeof(muxer));
+		muxer.proc = copy_to_sideband;
+		muxer.in = -1;
+		if (!start_async(&muxer)) {
+			muxer_started = 1;
+			saved_stderr = dup(STDERR_FILENO);
+			if (saved_stderr >= 0)
+				dup2(muxer.in, STDERR_FILENO);
+			close(muxer.in);
+		}
+	}
 
-	proc.no_stdin = 1;
-	proc.stdout_to_stderr = 1;
-	proc.err = use_sideband ? -1 : 0;
-	proc.trace2_hook_name = "update";
+	code = run_hooks_opt(the_repository, "update", &opt);
 
-	code = start_command(&proc);
-	if (code)
-		return code;
-	if (use_sideband)
-		copy_to_sideband(proc.err, -1, NULL);
-	return finish_command(&proc);
+	if (saved_stderr >= 0) {
+		dup2(saved_stderr, STDERR_FILENO);
+		close(saved_stderr);
+	}
+	if (muxer_started)
+		finish_async(&muxer);
+
+	return code;
 }
 
 static struct command *find_command_by_refname(struct command *list,
@@ -1639,34 +1651,41 @@ static const char *update(struct command *cmd, struct shallow_info *si)
 
 static void run_update_post_hook(struct command *commands)
 {
+	struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT;
+	struct async muxer;
 	struct command *cmd;
-	struct child_process proc = CHILD_PROCESS_INIT;
-	const char *hook;
-
-	hook = find_hook(the_repository, "post-update");
-	if (!hook)
-		return;
+	int saved_stderr = -1;
+	int muxer_started = 0;
 
 	for (cmd = commands; cmd; cmd = cmd->next) {
 		if (cmd->error_string || cmd->did_not_exist)
 			continue;
-		if (!proc.args.nr)
-			strvec_push(&proc.args, hook);
-		strvec_push(&proc.args, cmd->ref_name);
+		strvec_push(&opt.args, cmd->ref_name);
 	}
-	if (!proc.args.nr)
+	if (!opt.args.nr)
 		return;
 
-	proc.no_stdin = 1;
-	proc.stdout_to_stderr = 1;
-	proc.err = use_sideband ? -1 : 0;
-	proc.trace2_hook_name = "post-update";
+	if (use_sideband) {
+		memset(&muxer, 0, sizeof(muxer));
+		muxer.proc = copy_to_sideband;
+		muxer.in = -1;
+		if (!start_async(&muxer)) {
+			muxer_started = 1;
+			saved_stderr = dup(STDERR_FILENO);
+			if (saved_stderr >= 0)
+				dup2(muxer.in, STDERR_FILENO);
+			close(muxer.in);
+		}
+	}
 
-	if (!start_command(&proc)) {
-		if (use_sideband)
-			copy_to_sideband(proc.err, -1, NULL);
-		finish_command(&proc);
+	run_hooks_opt(the_repository, "post-update", &opt);
+
+	if (saved_stderr >= 0) {
+		dup2(saved_stderr, STDERR_FILENO);
+		close(saved_stderr);
 	}
+	if (muxer_started)
+		finish_async(&muxer);
 }
 
 static void check_aliased_update_internal(struct command *cmd,
-- 
2.52.0.732.gb351b5166d.dirty