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
|
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
|
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
|
status will cause the transaction to be aborted. The hook will not be
|
||||||
called with "aborted" state in that case.
|
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
|
push-to-checkout
|
||||||
~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -988,6 +988,41 @@ static int run_update_hook(struct command *cmd)
|
||||||
return code;
|
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,
|
static struct command *find_command_by_refname(struct command *list,
|
||||||
const char *refname)
|
const char *refname)
|
||||||
{
|
{
|
||||||
|
|
@ -2409,22 +2444,70 @@ static void update_shallow_info(struct command *commands,
|
||||||
free(ref_status);
|
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;
|
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;
|
struct strbuf buf = STRBUF_INIT;
|
||||||
|
|
||||||
packet_buf_write(&buf, "unpack %s\n",
|
generate_response(&buf, commands, unpack_status, false, NULL);
|
||||||
unpack_status->len ? unpack_status->buf : "ok");
|
|
||||||
for (cmd = commands; cmd; cmd = cmd->next) {
|
if (run_receive_report_hook(&buf)) {
|
||||||
if (!cmd->error_string)
|
strbuf_reset(&buf);
|
||||||
packet_buf_write(&buf, "ok %s\n",
|
generate_response(&buf, commands, unpack_status, false,
|
||||||
cmd->ref_name);
|
"receive-report hook failed");
|
||||||
else
|
|
||||||
packet_buf_write(&buf, "ng %s %s\n",
|
|
||||||
cmd->ref_name, cmd->error_string);
|
|
||||||
}
|
}
|
||||||
packet_buf_flush(&buf);
|
|
||||||
|
|
||||||
if (use_sideband)
|
if (use_sideband)
|
||||||
send_sideband(1, 1, buf.buf, buf.len, 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)
|
static void report_v2(struct command *commands, const struct strbuf *unpack_status)
|
||||||
{
|
{
|
||||||
struct command *cmd;
|
|
||||||
struct strbuf buf = STRBUF_INIT;
|
struct strbuf buf = STRBUF_INIT;
|
||||||
struct ref_push_report *report;
|
|
||||||
|
|
||||||
packet_buf_write(&buf, "unpack %s\n",
|
generate_response(&buf, commands, unpack_status, true, NULL);
|
||||||
unpack_status->len ? unpack_status->buf : "ok");
|
|
||||||
for (cmd = commands; cmd; cmd = cmd->next) {
|
|
||||||
int count = 0;
|
|
||||||
|
|
||||||
if (cmd->error_string) {
|
if (run_receive_report_hook(&buf)) {
|
||||||
packet_buf_write(&buf, "ng %s %s\n",
|
strbuf_reset(&buf);
|
||||||
cmd->ref_name,
|
generate_response(&buf, commands, unpack_status, true,
|
||||||
cmd->error_string);
|
"receive-report hook failed");
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
packet_buf_flush(&buf);
|
|
||||||
|
|
||||||
if (use_sideband)
|
if (use_sideband)
|
||||||
send_sideband(1, 1, buf.buf, buf.len, use_sideband);
|
send_sideband(1, 1, buf.buf, buf.len, use_sideband);
|
||||||
|
|
|
||||||
|
|
@ -655,6 +655,7 @@ integration_tests = [
|
||||||
't5409-colorize-remote-messages.sh',
|
't5409-colorize-remote-messages.sh',
|
||||||
't5410-receive-pack.sh',
|
't5410-receive-pack.sh',
|
||||||
't5411-proc-receive-hook.sh',
|
't5411-proc-receive-hook.sh',
|
||||||
|
't5412-receive-report-hook.sh',
|
||||||
't5500-fetch-pack.sh',
|
't5500-fetch-pack.sh',
|
||||||
't5501-fetch-push-alternates.sh',
|
't5501-fetch-push-alternates.sh',
|
||||||
't5502-quickfetch.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