mirror of
https://github.com/git/git.git
synced 2026-10-04 21:57:29 +02:00
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
client. Servers with custom logic may need to transform or gate the
report based on the outcome of external logic post reference updates.
This is specially needed for our usecase at GitLab where we have custom
MVCC logic on top of Git which creates a new version for each push
operation. The new version is only committed when certain external
operations post reference transaction succeed. So reporting the correct
message based on the outcome of these operations is important.
The outcome of these operations is only known after `execute_commands()`
has returned and before the report is written. There is no point in
receive-pack where the server can act on that.
We cannot use any of the existing hooks as:
- The pre-receive hook runs too early, as we haven't updated
references at that point yet and we need to have the full view of
all resulting updates (both objects and references).
- The update hook is too inefficient as it runs once per reference,
and we cannot trivially determine the last update.
- The reference-transaction hook is not suited for this. It fires from
within `ref_transaction_commit()`, which is before the outcome we
need to report is known, so there is no phase at which it could give
us the answer. It also does not contain any knowledge regarding the
push and cannot communicate with the clients.
- The proc-receive hook replaces execute_commands() for references
matching 'receive.procReceiveRefs'. We need to gate the report for
the push as a whole.
- The post-receive and post-update hooks cannot be used as they run
too late, at the point where we have already reported success to the
client.
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.
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 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
client while receive-pack itself exits cleanly. The client marks
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 modifies
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
'remote:' lines on the client terminal. Writing to stderr alone does
not affect the push outcome.
Reference updates applied by execute_commands() are not rolled back in
either failure mode. The hook can cause the client to perceive the push
as failed, but cannot undo server-side changes. This creates a
divergence that the server cannot resolve: the client leaves its
remote-tracking reference at the old value while the update is in fact
applied, and a later fetch may reveal the update that the push reported
as rejected.
The hook is therefore only appropriate for servers which can guarantee
that a rejected update is not observable by any reader. In our case the
transaction committed by execute_commands() produces a candidate version
which is not visible to other readers and is only published once the
subsequent operations succeed, so a report of 'ng' corresponds to a
version that is discarded rather than published. On a repository where a
committed reference update is immediately visible, rejecting a push from
this hook would instead leave the pusher with a view that does not match
the server.
This hook does not use the config-based hook infrastructure, which
supports running multiple scripts per hook event. This hook is a
bidirectional filter: it receives the report on stdin and writes a
modified version to stdout. Running multiple such scripts sequentially
would require piping the output of one into the input of the next,
which the current hook infrastructure does not support. A single-script
design is therefore a natural fit, and is consistent with how
'proc-receive' is structured for the same reason.
Helped-by: Patrick Steinhardt <ps@pks.im>
Signed-off-by: Karthik Nayak <karthik.188@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
258 lines
7.4 KiB
Bash
Executable File
258 lines
7.4 KiB
Bash
Executable File
#!/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 modify the unpack status" '
|
|
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/^unpack ok$/unpack push failed due to server error/" |
|
|
test-tool pkt-line pack
|
|
EOF
|
|
|
|
test_must_fail git -C workbench push origin $B:refs/heads/main >out 2>&1 &&
|
|
test_grep "error: remote unpack failed: push failed due to server error" out &&
|
|
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 "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 "non-zero exit with pre-existing ng from proc-receive" '
|
|
test_when_finished "rm -rf upstream" &&
|
|
test_when_finished "git -C workbench remote remove origin" &&
|
|
|
|
git init --bare upstream &&
|
|
git -C upstream config receive.procReceiveRefs refs/for &&
|
|
git -C workbench remote add origin ../upstream &&
|
|
git -C workbench push origin $A:refs/heads/main &&
|
|
|
|
# Use a proc-receive hook to generate a dynamic error string.
|
|
# This is used to capture any leaks stemming from overriding the
|
|
# error message via the receive-report.
|
|
test_hook -C upstream --setup proc-receive <<-\EOF &&
|
|
test-tool proc-receive -r "ng refs/for/main/topic push-rejected-by-service-x"
|
|
EOF
|
|
|
|
test_hook -C upstream --setup receive-report <<-\EOF &&
|
|
tee raw
|
|
exit 1
|
|
EOF
|
|
|
|
test_must_fail git -C workbench push origin HEAD:refs/for/main/topic >out 2>&1 &&
|
|
test_grep "receive-report hook failed" out &&
|
|
|
|
test-tool pkt-line unpack <upstream/raw >actual-report &&
|
|
cat >expect-report <<-\EOF &&
|
|
unpack ok
|
|
ng refs/for/main/topic push-rejected-by-service-x
|
|
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
|