--- 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@xxxxxxxxx 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@xxxxxxxxx --- 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@xxxxxxxxx> ## 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