Merge branch 'kn/receive-report-hook' into seen
A new hook 'report' is added to 'git receive-pack', which runs after reference updates and allows the server to filter or modify the packet-line status report sent back to the client. * kn/receive-report-hook: hook: introduce the receive-report hook receive-pack: move message generation to separate function doc: add proc-receive hook info in 'git-receive-pack.adoc'seen
commit
534d7326be
|
|
@ -236,6 +236,21 @@ if the repository is packed and is served via a dumb transport.
|
|||
exec git update-server-info
|
||||
----
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
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
|
||||
----------------------
|
||||
|
|
|
|||
|
|
@ -527,6 +527,49 @@ The exit status of the hook is ignored for any state except for the
|
|||
status will cause the transaction to be aborted. The hook will not be
|
||||
called with "aborted" state in that case.
|
||||
|
||||
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
|
||||
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, 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:
|
||||
|
||||
* To reject individual ref updates while keeping `receive-pack` alive,
|
||||
rewrite the corresponding `ok <refname>` lines to
|
||||
`ng <refname> <reason>` lines in the output and exit with status 0.
|
||||
The client will then mark those specific refs as rejected while
|
||||
treating any `ok` refs as successful. The push as a whole is
|
||||
considered failed if any ref is `ng`, and `git push` will exit with
|
||||
a non-zero status on the client side.
|
||||
|
||||
* 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 '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
|
||||
using 'git-push(1)', regardless of the hook's exit status. Writing to
|
||||
standard error alone does not affect the push outcome.
|
||||
|
||||
Note that by the time this hook runs, all ref updates have already been
|
||||
applied to the repository. Neither a non-zero exit nor rewriting refs
|
||||
to `ng` rolls back any ref changes that were already committed
|
||||
server-side. The hook can cause the client to perceive the push as
|
||||
failed, but cannot undo the server-side updates.
|
||||
|
||||
push-to-checkout
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
|||
|
|
@ -988,6 +988,41 @@ static int run_update_hook(struct command *cmd)
|
|||
return code;
|
||||
}
|
||||
|
||||
static int run_receive_report_hook(struct strbuf *report)
|
||||
{
|
||||
struct child_process proc = CHILD_PROCESS_INIT;
|
||||
struct async sideband_async;
|
||||
int sideband_async_started = 0;
|
||||
int saved_stderr = -1;
|
||||
struct strbuf out = STRBUF_INIT;
|
||||
const char *hook_path;
|
||||
int ret;
|
||||
|
||||
hook_path = find_hook(the_repository, "receive-report");
|
||||
if (!hook_path)
|
||||
return 0;
|
||||
|
||||
strvec_push(&proc.args, hook_path);
|
||||
proc.trace2_hook_name = "receive-report";
|
||||
|
||||
prepare_sideband_async(&sideband_async, &saved_stderr,
|
||||
&sideband_async_started);
|
||||
|
||||
sigchain_push(SIGPIPE, SIG_IGN);
|
||||
ret = pipe_command(&proc, report->buf, report->len, &out,
|
||||
report->len, NULL, 0);
|
||||
sigchain_pop(SIGPIPE);
|
||||
|
||||
finish_sideband_async(&sideband_async, saved_stderr,
|
||||
sideband_async_started);
|
||||
|
||||
if (!ret)
|
||||
strbuf_swap(&out, report);
|
||||
|
||||
strbuf_release(&out);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static struct command *find_command_by_refname(struct command *list,
|
||||
const char *refname)
|
||||
{
|
||||
|
|
@ -2409,22 +2444,70 @@ static void update_shallow_info(struct command *commands,
|
|||
free(ref_status);
|
||||
}
|
||||
|
||||
static void report(struct command *commands, const struct strbuf *unpack_status)
|
||||
/*
|
||||
* 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 struct strbuf *unpack_status, bool add_reports,
|
||||
const char *ref_error)
|
||||
{
|
||||
struct command *cmd;
|
||||
|
||||
packet_buf_write(buf, "unpack %s\n",
|
||||
unpack_status->len ? unpack_status->buf : "ok");
|
||||
|
||||
for (cmd = commands; cmd; cmd = cmd->next) {
|
||||
struct ref_push_report *report;
|
||||
int count = 0;
|
||||
|
||||
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 || ref_error)
|
||||
continue;
|
||||
|
||||
for (report = cmd->report; report; report = report->next) {
|
||||
if (count++ > 0)
|
||||
packet_buf_write(buf, "ok %s\n",
|
||||
cmd->ref_name);
|
||||
if (report->ref_name)
|
||||
packet_buf_write(buf, "option refname %s\n",
|
||||
report->ref_name);
|
||||
if (report->old_oid)
|
||||
packet_buf_write(buf, "option old-oid %s\n",
|
||||
oid_to_hex(report->old_oid));
|
||||
if (report->new_oid)
|
||||
packet_buf_write(buf, "option new-oid %s\n",
|
||||
oid_to_hex(report->new_oid));
|
||||
if (report->forced_update)
|
||||
packet_buf_write(buf, "option forced-update\n");
|
||||
}
|
||||
}
|
||||
|
||||
packet_buf_flush(buf);
|
||||
}
|
||||
|
||||
static void report(struct command *commands, const struct strbuf *unpack_status)
|
||||
{
|
||||
struct strbuf buf = STRBUF_INIT;
|
||||
|
||||
packet_buf_write(&buf, "unpack %s\n",
|
||||
unpack_status->len ? unpack_status->buf : "ok");
|
||||
for (cmd = commands; cmd; cmd = cmd->next) {
|
||||
if (!cmd->error_string)
|
||||
packet_buf_write(&buf, "ok %s\n",
|
||||
cmd->ref_name);
|
||||
else
|
||||
packet_buf_write(&buf, "ng %s %s\n",
|
||||
cmd->ref_name, cmd->error_string);
|
||||
generate_response(&buf, commands, unpack_status, false, NULL);
|
||||
|
||||
if (run_receive_report_hook(&buf)) {
|
||||
strbuf_reset(&buf);
|
||||
generate_response(&buf, commands, unpack_status, false,
|
||||
"receive-report hook failed");
|
||||
}
|
||||
packet_buf_flush(&buf);
|
||||
|
||||
if (use_sideband)
|
||||
send_sideband(1, 1, buf.buf, buf.len, use_sideband);
|
||||
|
|
@ -2435,41 +2518,15 @@ static void report(struct command *commands, const struct strbuf *unpack_status)
|
|||
|
||||
static void report_v2(struct command *commands, const struct strbuf *unpack_status)
|
||||
{
|
||||
struct command *cmd;
|
||||
struct strbuf buf = STRBUF_INIT;
|
||||
struct ref_push_report *report;
|
||||
|
||||
packet_buf_write(&buf, "unpack %s\n",
|
||||
unpack_status->len ? unpack_status->buf : "ok");
|
||||
for (cmd = commands; cmd; cmd = cmd->next) {
|
||||
int count = 0;
|
||||
generate_response(&buf, commands, unpack_status, true, NULL);
|
||||
|
||||
if (cmd->error_string) {
|
||||
packet_buf_write(&buf, "ng %s %s\n",
|
||||
cmd->ref_name,
|
||||
cmd->error_string);
|
||||
continue;
|
||||
}
|
||||
packet_buf_write(&buf, "ok %s\n",
|
||||
cmd->ref_name);
|
||||
for (report = cmd->report; report; report = report->next) {
|
||||
if (count++ > 0)
|
||||
packet_buf_write(&buf, "ok %s\n",
|
||||
cmd->ref_name);
|
||||
if (report->ref_name)
|
||||
packet_buf_write(&buf, "option refname %s\n",
|
||||
report->ref_name);
|
||||
if (report->old_oid)
|
||||
packet_buf_write(&buf, "option old-oid %s\n",
|
||||
oid_to_hex(report->old_oid));
|
||||
if (report->new_oid)
|
||||
packet_buf_write(&buf, "option new-oid %s\n",
|
||||
oid_to_hex(report->new_oid));
|
||||
if (report->forced_update)
|
||||
packet_buf_write(&buf, "option forced-update\n");
|
||||
}
|
||||
if (run_receive_report_hook(&buf)) {
|
||||
strbuf_reset(&buf);
|
||||
generate_response(&buf, commands, unpack_status, true,
|
||||
"receive-report hook failed");
|
||||
}
|
||||
packet_buf_flush(&buf);
|
||||
|
||||
if (use_sideband)
|
||||
send_sideband(1, 1, buf.buf, buf.len, use_sideband);
|
||||
|
|
|
|||
|
|
@ -655,6 +655,7 @@ integration_tests = [
|
|||
't5409-colorize-remote-messages.sh',
|
||||
't5410-receive-pack.sh',
|
||||
't5411-proc-receive-hook.sh',
|
||||
't5412-receive-report-hook.sh',
|
||||
't5500-fetch-pack.sh',
|
||||
't5501-fetch-push-alternates.sh',
|
||||
't5502-quickfetch.sh',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,200 @@
|
|||
#!/bin/sh
|
||||
|
||||
test_description='test receive-report hook'
|
||||
|
||||
GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
|
||||
export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
|
||||
|
||||
. ./test-lib.sh
|
||||
|
||||
. "$TEST_DIRECTORY"/t5411/common-functions.sh
|
||||
|
||||
URL_PREFIX="\.\."
|
||||
|
||||
test_expect_success "setup workbench" '
|
||||
git init workbench &&
|
||||
create_commits_in workbench A B
|
||||
'
|
||||
|
||||
test_expect_success "no report hook, push succeeds" '
|
||||
test_when_finished "rm -rf upstream" &&
|
||||
test_when_finished "git -C workbench remote remove origin" &&
|
||||
git init --bare upstream &&
|
||||
|
||||
git -C workbench remote add origin ../upstream &&
|
||||
git -C workbench push origin $A:refs/heads/main &&
|
||||
git -C workbench push origin $B:refs/heads/main >out 2>&1 &&
|
||||
|
||||
make_user_friendly_and_stable_output <out >actual &&
|
||||
cat >expect <<-\EOF &&
|
||||
To ../upstream
|
||||
<COMMIT-A>..<COMMIT-B> <COMMIT-B> -> main
|
||||
EOF
|
||||
test_cmp expect actual
|
||||
'
|
||||
|
||||
test_expect_success "passthrough does not alter report" '
|
||||
test_when_finished "rm -rf upstream" &&
|
||||
test_when_finished "git -C workbench remote remove origin" &&
|
||||
git init --bare upstream &&
|
||||
|
||||
test_hook -C upstream --setup receive-report <<-\EOF &&
|
||||
cat
|
||||
EOF
|
||||
|
||||
git -C workbench remote add origin ../upstream &&
|
||||
git -C workbench push origin $A:refs/heads/main &&
|
||||
git -C workbench push origin $B:refs/heads/main >out 2>&1 &&
|
||||
|
||||
make_user_friendly_and_stable_output <out >actual &&
|
||||
cat >expect <<-\EOF &&
|
||||
To ../upstream
|
||||
<COMMIT-A>..<COMMIT-B> <COMMIT-B> -> main
|
||||
EOF
|
||||
test_cmp expect actual
|
||||
'
|
||||
|
||||
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" &&
|
||||
|
||||
git init --bare upstream &&
|
||||
git -C workbench remote add origin ../upstream &&
|
||||
git -C workbench push origin $A:refs/heads/main &&
|
||||
|
||||
test_hook -C upstream --setup receive-report <<-\EOF &&
|
||||
exit 1
|
||||
EOF
|
||||
|
||||
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 &&
|
||||
To ../upstream
|
||||
! [remote rejected] <COMMIT-B> -> main (receive-report hook failed)
|
||||
EOF
|
||||
test_cmp expect actual
|
||||
'
|
||||
|
||||
test_expect_success "hook is invoked and receives report on stdin" '
|
||||
test_when_finished "rm -rf upstream" &&
|
||||
test_when_finished "git -C workbench remote remove origin" &&
|
||||
|
||||
git init --bare upstream &&
|
||||
test_hook -C upstream --setup receive-report <<-EOF &&
|
||||
tee raw
|
||||
EOF
|
||||
|
||||
git -C workbench remote add origin ../upstream &&
|
||||
git -C workbench push origin $A:refs/heads/main &&
|
||||
git -C workbench push origin $B:refs/heads/main >out 2>&1 &&
|
||||
|
||||
make_user_friendly_and_stable_output <out >actual &&
|
||||
cat >expect <<-EOF &&
|
||||
To ../upstream
|
||||
<COMMIT-A>..<COMMIT-B> <COMMIT-B> -> main
|
||||
EOF
|
||||
test_cmp expect actual &&
|
||||
|
||||
test-tool pkt-line unpack <upstream/raw >actual-report &&
|
||||
cat >expect-report <<-EOF &&
|
||||
unpack ok
|
||||
ok refs/heads/main
|
||||
0000
|
||||
EOF
|
||||
test_cmp expect-report actual-report
|
||||
'
|
||||
|
||||
test_expect_success "hook can modify the report sent to client" '
|
||||
test_when_finished "rm -rf upstream" &&
|
||||
test_when_finished "git -C workbench remote remove origin" &&
|
||||
|
||||
git init --bare upstream &&
|
||||
git -C workbench remote add origin ../upstream &&
|
||||
git -C workbench push origin $A:refs/heads/main &&
|
||||
|
||||
test_hook -C upstream --setup receive-report <<-\EOF &&
|
||||
test-tool pkt-line unpack |
|
||||
sed "s/^ok /ng /" |
|
||||
test-tool pkt-line pack
|
||||
EOF
|
||||
|
||||
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 &&
|
||||
To ../upstream
|
||||
! [remote rejected] <COMMIT-B> -> main (failed)
|
||||
EOF
|
||||
test_cmp expect actual
|
||||
'
|
||||
|
||||
test_expect_success "hook can report a custom failure message" '
|
||||
test_when_finished "rm -rf upstream" &&
|
||||
test_when_finished "git -C workbench remote remove origin" &&
|
||||
|
||||
git init --bare upstream &&
|
||||
git -C workbench remote add origin ../upstream &&
|
||||
git -C workbench push origin $A:refs/heads/main &&
|
||||
|
||||
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/" |
|
||||
test-tool pkt-line pack |
|
||||
tee raw
|
||||
EOF
|
||||
|
||||
test_must_fail git -C workbench push origin $B:refs/heads/main >out 2>&1 &&
|
||||
test_grep "push rejected: service X is down" out &&
|
||||
|
||||
test-tool pkt-line unpack <upstream/raw >actual-report &&
|
||||
cat >expect-report <<-\EOF &&
|
||||
unpack ok
|
||||
ng refs/heads/main service-x-is-down
|
||||
0000
|
||||
EOF
|
||||
test_cmp expect-report actual-report
|
||||
'
|
||||
|
||||
test_expect_success "hook stderr with zero exit status code" '
|
||||
test_when_finished "rm -rf upstream" &&
|
||||
test_when_finished "git -C workbench remote remove origin" &&
|
||||
|
||||
git init --bare upstream &&
|
||||
git -C workbench remote add origin ../upstream &&
|
||||
git -C workbench push origin $A:refs/heads/main &&
|
||||
|
||||
test_hook -C upstream --setup receive-report <<-\EOF &&
|
||||
echo "push rejected: service X is down" >&2
|
||||
tee raw
|
||||
EOF
|
||||
|
||||
git -C workbench push origin $B:refs/heads/main >out 2>&1 &&
|
||||
test_grep "push rejected: service X is down" out &&
|
||||
|
||||
test-tool pkt-line unpack <upstream/raw >actual-report &&
|
||||
cat >expect-report <<-\EOF &&
|
||||
unpack ok
|
||||
ok refs/heads/main
|
||||
0000
|
||||
EOF
|
||||
test_cmp expect-report actual-report
|
||||
'
|
||||
|
||||
test_expect_success "hook stderr is relayed to client via sideband" '
|
||||
test_when_finished "rm -rf upstream" &&
|
||||
test_when_finished "git -C workbench remote remove origin" &&
|
||||
|
||||
git init --bare upstream &&
|
||||
git -C workbench remote add origin ../upstream &&
|
||||
git -C workbench push origin $A:refs/heads/main &&
|
||||
|
||||
test_hook -C upstream --setup receive-report <<-\EOF &&
|
||||
echo "hook-stderr-message" >&2
|
||||
exit 1
|
||||
EOF
|
||||
|
||||
test_must_fail git -C workbench push origin $B:refs/heads/main >out 2>&1 &&
|
||||
test_grep "remote: hook-stderr-message" out
|
||||
'
|
||||
|
||||
test_done
|
||||
Loading…
Reference in New Issue