diff options
| author | Eric Ju <eric.peijian@gmail.com> | 2026-03-16 22:36:24 -0400 |
|---|---|---|
| committer | Junio C Hamano <gitster@pobox.com> | 2026-03-16 21:00:44 -0700 |
| commit | 60d8c1e97d62c27ef60db0bc3d5deadd6dfdb98d (patch) | |
| tree | 38b7bde5efa93c9e513e803cb0263b11b6f47b75 /t | |
| parent | 7f19e4e1b6a3ad259e2ed66033e01e03b8b74c5e (diff) | |
| download | git-60d8c1e97d62c27ef60db0bc3d5deadd6dfdb98d.tar.xz | |
refs: add 'preparing' phase to the reference-transaction hook
The "reference-transaction" hook is invoked multiple times during a ref
transaction. Each invocation corresponds to a different phase:
- The "prepared" phase indicates that references have been locked.
- The "committed" phase indicates that all updates have been written to disk.
- The "aborted" phase indicates that the transaction has been aborted and that
all changes have been rolled back.
This hook can be used to learn about the updates that Git wants to perform.
For example, forges use it to coordinate reference updates across multiple
nodes.
However, the phases are insufficient for some specific use cases. The earliest
observable phase in the "reference-transaction" hook is "prepared", at which
point Git has already taken exclusive locks on every affected reference. This
makes it suitable for last-chance validation, but not for serialization. So by
the time a hook sees the "prepared" phase, it has no way to defer locking, and
thus it cannot rearrange multiple concurrent ref transactions relative to one
another.
Introduce a new "preparing" phase that runs before the "prepared" phase, that
is before Git acquires any reference lock on disk. This gives callers a
well-defined window to perform validation, enable higher-level ordering of
concurrent transactions, or reject the transaction entirely, all without
interfering with the locking state.
This change is strictly speaking not backwards compatible. Existing hook
scripts that do not know how to handle unknown phases may treat 'preparing'
as an error and return non-zero. But the hook is considered to expose
internal implementation details of how Git works, and as such we have
been a bit more lenient with changing its exact semantics, like for example
in a8ae923f85 (refs: support symrefs in 'reference-transaction' hook, 2024-05-07).
An alternative would be to introduce a "reference-transaction-v2" hook that
knows about the new phase. This feels like a rather heavy-weight option though,
and was thus discarded.
Helped-by: Patrick Steinhardt <ps@pks.im>
Helped-by: Justin Tobler <jltobler@gmail.com>
Helped-by: Karthik Nayak <karthik.188@gmail.com>
Signed-off-by: Eric Ju <eric.peijian@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Diffstat (limited to 't')
| -rwxr-xr-x | t/t1416-ref-transaction-hooks.sh | 30 | ||||
| -rwxr-xr-x | t/t5510-fetch.sh | 7 |
2 files changed, 32 insertions, 5 deletions
diff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh index d91dd3a3b5..4fe9d9b234 100755 --- a/t/t1416-ref-transaction-hooks.sh +++ b/t/t1416-ref-transaction-hooks.sh @@ -20,6 +20,7 @@ test_expect_success 'hook allows updating ref if successful' ' echo "$*" >>actual EOF cat >expect <<-EOF && + preparing prepared committed EOF @@ -27,6 +28,18 @@ test_expect_success 'hook allows updating ref if successful' ' test_cmp expect actual ' +test_expect_success 'hook aborts updating ref in preparing state' ' + git reset --hard PRE && + test_hook reference-transaction <<-\EOF && + if test "$1" = preparing + then + exit 1 + fi + EOF + test_must_fail git update-ref HEAD POST 2>err && + test_grep "in '\''preparing'\'' phase, update aborted by the reference-transaction hook" err +' + test_expect_success 'hook aborts updating ref in prepared state' ' git reset --hard PRE && test_hook reference-transaction <<-\EOF && @@ -36,7 +49,7 @@ test_expect_success 'hook aborts updating ref in prepared state' ' fi EOF test_must_fail git update-ref HEAD POST 2>err && - test_grep "ref updates aborted by hook" err + test_grep "in '\''prepared'\'' phase, update aborted by the reference-transaction hook" err ' test_expect_success 'hook gets all queued updates in prepared state' ' @@ -121,6 +134,7 @@ test_expect_success 'interleaving hook calls succeed' ' cat >expect <<-EOF && hooks/update refs/tags/PRE $ZERO_OID $PRE_OID hooks/update refs/tags/POST $ZERO_OID $POST_OID + hooks/reference-transaction preparing hooks/reference-transaction prepared hooks/reference-transaction committed EOF @@ -143,6 +157,8 @@ test_expect_success 'hook captures git-symbolic-ref updates' ' git symbolic-ref refs/heads/symref refs/heads/main && cat >expect <<-EOF && + preparing + $ZERO_OID ref:refs/heads/main refs/heads/symref prepared $ZERO_OID ref:refs/heads/main refs/heads/symref committed @@ -171,14 +187,20 @@ test_expect_success 'hook gets all queued symref updates' ' # In the files backend, "delete" also triggers an additional transaction # update on the packed-refs backend, which constitutes additional reflog # entries. + cat >expect <<-EOF && + preparing + ref:refs/heads/main $ZERO_OID refs/heads/symref + ref:refs/heads/main $ZERO_OID refs/heads/symrefd + $ZERO_OID ref:refs/heads/main refs/heads/symrefc + ref:refs/heads/main ref:refs/heads/branch refs/heads/symrefu + EOF + if test_have_prereq REFFILES then - cat >expect <<-EOF + cat >>expect <<-EOF aborted $ZERO_OID $ZERO_OID refs/heads/symrefd EOF - else - >expect fi && cat >>expect <<-EOF && diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh index 5dcb4b51a4..6fe21e2b3a 100755 --- a/t/t5510-fetch.sh +++ b/t/t5510-fetch.sh @@ -469,12 +469,17 @@ test_expect_success 'fetch --atomic executes a single reference transaction only head_oid=$(git rev-parse HEAD) && cat >expected <<-EOF && + preparing + $ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-1 + $ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-2 prepared $ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-1 $ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-2 committed $ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-1 $ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-2 + preparing + $ZERO_OID ref:refs/remotes/origin/main refs/remotes/origin/HEAD EOF rm -f atomic/actual && @@ -497,7 +502,7 @@ test_expect_success 'fetch --atomic aborts all reference updates if hook aborts' head_oid=$(git rev-parse HEAD) && cat >expected <<-EOF && - prepared + preparing $ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-abort-1 $ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-abort-2 $ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-abort-3 |
