[PATCH v4 0/3] hook: introduce the receive-report hook
From: Karthik Nayak <hidden>
Date: 2026-08-26 10:19:56
Introduce a new receive-report hook which kicks in after the reference transaction is complete, but before the report is sent to the client. The hook receives the pkt-line encoded report in its stdin and its stdout replaces the report transferred to the user. If the hook exits with a non-zero exit code, all references are marked as rejected. The first patch, adds missing documentation to 'git-receive-pack.adoc'. The second patch refactors code and the third patch contains the new hook. --- Changes in v4: - Change the name of the hook to be 'receive-report' to avoid ambiguity. - Link to v3: https://patch.msgid.link/20260824-758-introduce-hook-v3-0-499526f0a062@gmail.com Changes in v3: - Move out addition of proc-receive hook doc to 'git-receive-pack.adoc' into a new commit. - Add a new commit to move out the response generation in receive-pack to a new function. - Instead of die-ing on non-zero exit code, we modify each reference to indicate that the hook failed. - Instead of correctly listing out the protocol, link to linkgit:gitprotocol-pack[5], as the protocol also differs between v1 and v2. - Link to v2: https://patch.msgid.link/20260821-758-introduce-hook-v2-1-e90e2f7ac2cf@gmail.com Changes in v2: - Modify the documentation and commit message to be more verbose. - Add documentation to 'git-receive-pack.adoc' - Use 'ret' as the variable name for the return code. - Modify the test to also check for the 'remote:'. - Link to v1: https://patch.msgid.link/20260818-758-introduce-hook-v1-1-8a8d89e65838@gmail.com To: git@vger.kernel.org CC: ps@pks.im CC: gitster@pobox.com CC: jltobler@gmail.com CC: kristofferhaugsbakk@fastmail.com CC: phillip.wood123@gmail.com --- Karthik Nayak (3): doc: add proc-receive hook info in 'git-receive-pack.adoc' receive-pack: move message generation to separate function hook: introduce the receive-report hook Documentation/git-receive-pack.adoc | 15 +++ Documentation/githooks.adoc | 43 ++++++++ builtin/receive-pack.c | 137 ++++++++++++++++-------- t/meson.build | 1 + t/t5412-receive-report-hook.sh | 200 ++++++++++++++++++++++++++++++++++++ 5 files changed, 356 insertions(+), 40 deletions(-) Range-diff versus v3: 1: b899f31ffa = 1: 30784c0448 doc: add proc-receive hook info in 'git-receive-pack.adoc' 2: 335182cd3d = 2: 55d6a46815 receive-pack: move message generation to separate function 3: 80aa575dab ! 3: 99eeafb537 hook: introduce the report hook for git-receive-pack(1) @@ Metadata Author: Karthik Nayak [off-list ref] ## Commit message ## - hook: introduce the report hook for git-receive-pack(1) + hook: introduce the receive-report hook When running 'git-receive-pack(1)', there is no way for the server to intercept and modify the status report before it is sent back to the @@ Commit message too late, at the point where we have already reported success to the client. - Introduce a new 'report' hook. The hook receives the complete pkt-line - encoded status report on standard input, after all ref updates have - been applied to the repository by execute_commands() but before the + Introduce a new 'receive-report' hook. The hook receives the complete + pkt-line encoded status report on standard input, after all ref updates + have been applied to the repository by execute_commands() but before the report is sent to the client. See linkgit:gitprotocol-pack[5] details on the protocol structure. @@ Commit message status if any ref is 'ng'. - Non-zero exit: the hook's stdout is discarded, receive-pack modifies - all references to be rejected with a 'report hook failed' error. + all references to be rejected with a 'receive-report hook failed' + error. In both cases, any output the hook writes to standard error is forwarded to the client over the sideband channel and appears as @@ Documentation/git-receive-pack.adoc: requests. It handles refs whose names match `receive.procReceiveRefs` and executes the actual ref updates. See linkgit:githooks[5] for the full protocol description. -+REPORT HOOK -+----------- ++RECEIVE-REPORT HOOK ++------------------- +This hook is invoked by 'git-receive-pack' after all the ref updates +have been applied but before the report is sent to the client. The hook +receives the complete report in pkt-line format on stdin and its stdout @@ Documentation/githooks.adoc: The exit status of the hook is ignored for any stat status will cause the transaction to be aborted. The hook will not be called with "aborted" state in that case. -+report -+~~~~~~ ++receive-report ++~~~~~~~~~~~~~~ + +This hook is invoked by linkgit:git-receive-pack[1] when it reacts to +`git push` and updates references in its repository. It executes on @@ Documentation/githooks.adoc: The exit status of the hook is ignored for any stat + +* To abort the entire push unconditionally, exit with a non-zero + status. In this case the hook's stdout is discarded, `receive-pack` -+ modifies all references to be rejected with a 'report hook failed' -+ error. ++ modifies all references to be rejected with a 'receive-report hook ++ failed' error. + +Any output written to standard error is forwarded to the client over +the sideband channel and will appear as `remote:` lines on clients @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd) return code; } -+static int run_report_hook(struct strbuf *report) ++static int run_receive_report_hook(struct strbuf *report) +{ + struct child_process proc = CHILD_PROCESS_INIT; + struct async sideband_async; @@ builtin/receive-pack.c: static int run_update_hook(struct command *cmd) + const char *hook_path; + int ret; + -+ hook_path = find_hook(the_repository, "report"); ++ hook_path = find_hook(the_repository, "receive-report"); + if (!hook_path) + return 0; + + strvec_push(&proc.args, hook_path); -+ proc.trace2_hook_name = "report"; ++ proc.trace2_hook_name = "receive-report"; + + prepare_sideband_async(&sideband_async, &saved_stderr, + &sideband_async_started); @@ builtin/receive-pack.c: static void report(struct command *commands, const char - generate_response(&buf, commands, unpack_status, false); + generate_response(&buf, commands, unpack_status, false, NULL); + -+ if (run_report_hook(&buf)) { ++ if (run_receive_report_hook(&buf)) { + strbuf_reset(&buf); + generate_response(&buf, commands, unpack_status, false, -+ "report hook failed"); ++ "receive-report hook failed"); + } if (use_sideband) @@ builtin/receive-pack.c: static void report_v2(struct command *commands, const ch - generate_response(&buf, commands, unpack_status, true); + generate_response(&buf, commands, unpack_status, true, NULL); + -+ if (run_report_hook(&buf)) { ++ if (run_receive_report_hook(&buf)) { + strbuf_reset(&buf); + generate_response(&buf, commands, unpack_status, true, -+ "report hook failed"); ++ "receive-report hook failed"); + } if (use_sideband) @@ t/meson.build: integration_tests = [ 't5409-colorize-remote-messages.sh', 't5410-receive-pack.sh', 't5411-proc-receive-hook.sh', -+ 't5412-report-hook.sh', ++ 't5412-receive-report-hook.sh', 't5500-fetch-pack.sh', 't5501-fetch-push-alternates.sh', 't5502-quickfetch.sh', - ## t/t5412-report-hook.sh (new) ## + ## t/t5412-receive-report-hook.sh (new) ## @@ +#!/bin/sh + -+test_description='test report hook' ++test_description='test receive-report hook' + +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME @@ t/t5412-report-hook.sh (new) + test_when_finished "git -C workbench remote remove origin" && + git init --bare upstream && + -+ test_hook -C upstream --setup report <<-\EOF && ++ test_hook -C upstream --setup receive-report <<-\EOF && + cat + EOF + @@ t/t5412-report-hook.sh (new) + git -C workbench remote add origin ../upstream && + git -C workbench push origin $A:refs/heads/main && + -+ test_hook -C upstream --setup report <<-\EOF && ++ test_hook -C upstream --setup receive-report <<-\EOF && + exit 1 + EOF + @@ t/t5412-report-hook.sh (new) + make_user_friendly_and_stable_output <out >actual && + cat >expect <<-\EOF && + To ../upstream -+ ! [remote rejected] <COMMIT-B> -> main (report hook failed) ++ ! [remote rejected] <COMMIT-B> -> main (receive-report hook failed) + EOF + test_cmp expect actual +' @@ t/t5412-report-hook.sh (new) + test_when_finished "git -C workbench remote remove origin" && + + git init --bare upstream && -+ test_hook -C upstream --setup report <<-EOF && ++ test_hook -C upstream --setup receive-report <<-EOF && + tee raw + EOF + @@ t/t5412-report-hook.sh (new) + git -C workbench remote add origin ../upstream && + git -C workbench push origin $A:refs/heads/main && + -+ test_hook -C upstream --setup report <<-\EOF && ++ test_hook -C upstream --setup receive-report <<-\EOF && + test-tool pkt-line unpack | + sed "s/^ok /ng /" | + test-tool pkt-line pack @@ t/t5412-report-hook.sh (new) + git -C workbench remote add origin ../upstream && + git -C workbench push origin $A:refs/heads/main && + -+ test_hook -C upstream --setup report <<-\EOF && ++ test_hook -C upstream --setup receive-report <<-\EOF && + echo "push rejected: service X is down" >&2 + test-tool pkt-line unpack | + sed "s/^ok \(.*\)/ng \1 service-x-is-down/" | @@ t/t5412-report-hook.sh (new) + git -C workbench remote add origin ../upstream && + git -C workbench push origin $A:refs/heads/main && + -+ test_hook -C upstream --setup report <<-\EOF && ++ test_hook -C upstream --setup receive-report <<-\EOF && + echo "push rejected: service X is down" >&2 + tee raw + EOF @@ t/t5412-report-hook.sh (new) + git -C workbench remote add origin ../upstream && + git -C workbench push origin $A:refs/heads/main && + -+ test_hook -C upstream --setup report <<-\EOF && ++ test_hook -C upstream --setup receive-report <<-\EOF && + echo "hook-stderr-message" >&2 + exit 1 + EOF --- base-commit: 11c6700f10234578d10523faf35656ca491425c9 change-id: 20260812-758-introduce-hook-5b3af9f1a7e8 Thanks - Karthik