[PATCH v3 0/3] hook: introduce the report hook for git-receive-pack(1)
- From
Karthik Nayak <karthik.188@gmail.com>
- Date
- Aug 24, 2026, 10:20 UTC
- Message-ID
- <20260824-758-introduce-hook-v3-0-499526f0a062@gmail.com>
- In-Reply-To
- <20260818-758-introduce-hook-v1-1-8a8d89e65838@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
---
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 report hook for git-receive-pack(1)Documentation/git-receive-pack.adoc | 15 +++ Documentation/githooks.adoc | 43 ++++++++ builtin/receive-pack.c | 137 ++++++++++++++++-------- t/meson.build | 1 + t/t5412-report-hook.sh | 200 ++++++++++++++++++++++++++++++++++++ 5 files changed, 356 insertions(+), 40 deletions(-)
Range-diff versus v2:
-: ---------- > 1: 42aaf10403 doc: add proc-receive hook info in 'git-receive-pack.adoc'
-: ---------- > 2: cb55895d2a receive-pack: move message generation to separate function
1: 07fa5ba8bb ! 3: 5bfaea5033 hook: introduce the report hook for git-receive-pack(1)
@@ Commit message
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
- report is sent to the client. The report consists of an 'unpack ok'
- or 'unpack <error>' line, followed by one 'ok <refname>' or
- 'ng <refname> <reason>' line per pushed ref, terminated by a flush
- packet.
+ report is sent to the client. See linkgit:gitprotocol-pack[5] details on
+ the protocol structure.
The hook's stdout fully replaces the report sent to the client.
receive-pack fully buffers the hook's stdout before acting on the exit
status, so the exit code is known before the client receives anything.
- This gives two distinct behaviours depending on exit status:
+ This gives two distinct behaviors depending on exit status:
- Exit 0: the hook's stdout is used as the report. The hook can
rewrite 'ok' lines to 'ng' lines to signal per-ref rejection to the
@@ Commit message
rejected refs as '[remote rejected]' and exits with a non-zero
status if any ref is 'ng'.
- - Non-zero exit: the hook's stdout is discarded, receive-pack calls
- die(), and no report is sent to the client at all. The client
- observes a sideband disconnect and reports 'the remote end hung up
- unexpectedly', treating the entire push as failed.
+ - Non-zero exit: the hook's stdout is discarded, receive-pack modifies
+ all references to be rejected with a '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
@@ Commit message
Signed-off-by: Karthik Nayak <karthik.188@gmail.com>
## Documentation/git-receive-pack.adoc ##
-@@ Documentation/git-receive-pack.adoc: if the repository is packed and is served via a dumb transport.
- exec git update-server-info
- ----
+@@ Documentation/git-receive-pack.adoc: requests. It handles refs whose names match the patterns defined by
+ `receive.procReceiveRefs` and executes the actual ref updates. See
+ linkgit:githooks[5] for the full protocol description.
-+PROC-RECEIVE HOOK
-+-----------------
-+This hook is invoked by 'git-receive-pack' when it processes push
-+requests. It handles refs whose names match the patterns defined by
-+`receive.procReceiveRefs` and executes the actual ref updates. See
-+linkgit:githooks[5] for the full protocol description.
-+
+REPORT HOOK
+-----------
+This hook is invoked by 'git-receive-pack' after all the ref updates
@@ Documentation/git-receive-pack.adoc: if the repository is packed and is served v
+replaces the report sent to the client. Allowing the hook to rewrite
+the outcomes or abort the push completely. See linkgit:githooks[5] for
+the full protocol description.
-
++
QUARANTINE ENVIRONMENT
----------------------
+
## Documentation/githooks.adoc ##
@@ Documentation/githooks.adoc: The exit status of the hook is ignored for any state except for the
@@ Documentation/githooks.adoc: The exit status of the hook is ignored for any stat
+
+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
-+the repository once after all refs have been updated and after
-+`execute_commands()` has applied all accepted ref changes to the
-+repository, but before the pkt-line encoded status report is sent back
-+to the client.
++the repository once after all refs have been updated and after all
++accepted ref changes are applied to the repository, but before the
++pkt-line encoded status report is sent back to the client.
+
+The hook receives the complete pkt-line encoded status report on
-+standard input. The report begins with an `unpack` line indicating
-+whether the object transfer succeeded (`unpack ok` or
-+`unpack <error>`), followed by one `ok <refname>` or
-+`ng <refname> <reason>` line per ref that was pushed, and is
-+terminated by a flush packet.
-+
-+The hook's standard output entirely replaces the report that is sent
-+to the client. The hook must write a valid pkt-line encoded report in
-+the same format it received. The hook's stdout is fully buffered by
-+`receive-pack` before any data is sent to the client, so the hook's
-+exit status is known before the client receives anything.
++standard input, see linkgit:gitprotocol-pack[5] for details on the
++structure. The hook's standard output entirely replaces the report
++that is sent to the client. The hook must write a valid pkt-line
++encoded report in the same format it received. The hook's stdout is
++fully buffered by `receive-pack` before any data is sent to the client,
++so the hook's exit status is known before the client receives anything.
+
+There are two distinct ways the hook can affect the push outcome:
+
@@ 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`
-+ calls `die()`, and no report is sent to the client at all. The client
-+ observes an unexpected sideband disconnect, making the entire push
-+ appear to have failed. In general, the hook should never exit with a
-+ non-zero status code and doing so would indicate a bug.
++ modifies all references to be rejected with a '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)
static struct command *find_command_by_refname(struct command *list,
const char *refname)
{
+@@ builtin/receive-pack.c: static void update_shallow_info(struct command *commands,
+ * Generate the response to be sent to the client invoking 'git-receive-pack(1)'.
+ * For v2 protocol, set `add_reports` to true, which will also add additional
+ * report per reference update.
++ * If `ref_error` is set, then all references will be rejected with the given
++ * error message.
+ */
+ static void generate_response(struct strbuf *buf, struct command *commands,
+- const char *unpack_status, bool add_reports)
++ const char *unpack_status, bool add_reports,
++ const char *ref_error)
+ {
+ struct command *cmd;
+
+@@ builtin/receive-pack.c: static void generate_response(struct strbuf *buf, struct command *commands,
+ if (cmd->error_string)
+ packet_buf_write(buf, "ng %s %s\n",
+ cmd->ref_name, cmd->error_string);
++ else if (ref_error)
++ packet_buf_write(buf, "ng %s %s\n",
++ cmd->ref_name, ref_error);
+ else
+ packet_buf_write(buf, "ok %s\n", cmd->ref_name);
+
+- if (!add_reports || cmd->error_string)
++ if (!add_reports || cmd->error_string || ref_error)
+ continue;
+
+ for (report = cmd->report; report; report = report->next) {
@@ builtin/receive-pack.c: static void report(struct command *commands, const char *unpack_status)
- }
- packet_buf_flush(&buf);
+ {
+ struct strbuf buf = STRBUF_INIT;
+
+- generate_response(&buf, commands, unpack_status, false);
++ generate_response(&buf, commands, unpack_status, false, NULL);
++
++ if (run_report_hook(&buf)) {
++ strbuf_reset(&buf);
++ generate_response(&buf, commands, unpack_status, false,
++ "report hook failed");
++ }
-+ if (run_report_hook(&buf))
-+ die("report hook failed");
-+
if (use_sideband)
send_sideband(1, 1, buf.buf, buf.len, use_sideband);
- else
@@ builtin/receive-pack.c: static void report_v2(struct command *commands, const char *unpack_status)
- }
- packet_buf_flush(&buf);
+ {
+ struct strbuf buf = STRBUF_INIT;
+
+- generate_response(&buf, commands, unpack_status, true);
++ generate_response(&buf, commands, unpack_status, true, NULL);
++
++ if (run_report_hook(&buf)) {
++ strbuf_reset(&buf);
++ generate_response(&buf, commands, unpack_status, true,
++ "report hook failed");
++ }
-+ if (run_report_hook(&buf))
-+ die("report hook failed");
-+
if (use_sideband)
send_sideband(1, 1, buf.buf, buf.len, use_sideband);
- else
## t/meson.build ##
@@ t/meson.build: integration_tests = [
@@ t/t5412-report-hook.sh (new)
+ test_cmp expect actual
+'
+
-+test_expect_success "non-zero exit causes receive-pack to die" '
++test_expect_success "non-zero exit reports as hook failed" '
+ test_when_finished "rm -rf upstream" &&
+ test_when_finished "git -C workbench remote remove origin" &&
+
@@ t/t5412-report-hook.sh (new)
+ test_must_fail git -C workbench push origin $B:refs/heads/main >out 2>&1 &&
+ make_user_friendly_and_stable_output <out >actual &&
+ cat >expect <<-\EOF &&
-+ fatal: report hook failed
-+ send-pack: unexpected disconnect while reading sideband packet
-+ fatal: the remote end hung up unexpectedly
++ To ../upstream
++ ! [remote rejected] <COMMIT-B> -> main (report hook failed)
+ EOF
+ test_cmp expect actual
+'--- base-commit: 11c6700f10234578d10523faf35656ca491425c9 change-id: 20260812-758-introduce-hook-5b3af9f1a7e8
Thanks - Karthik