* [PATCH 1/1] Add preparing state to reference-transaction hook
2026-03-13 19:35 [PATCH 0/1] Add "preparing" phase to reference-transaction hook eric.peijian
@ 2026-03-13 19:35 ` eric.peijian
2026-03-13 21:20 ` Junio C Hamano
2026-03-13 23:05 ` Justin Tobler
2026-03-16 4:51 ` [PATCH v2 0/1] refs: add 'preparing' phase to the " Eric Ju
2026-03-17 2:36 ` [PATCH v3 " Eric Ju
2 siblings, 2 replies; 15+ messages in thread
From: eric.peijian @ 2026-03-13 19:35 UTC (permalink / raw)
To: git; +Cc: ps, jltobler, Eric Ju
From: Eric Ju <eric.peijian@gmail.com>
From: Eric Ju <eju@gitlab.com>
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 "commit" phase indicates that all updates have been written to disk.
- The "abort" 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 to handle unknown phases handle the "preparing" state
string will encounter an unknown phase, and that might cause them to return an
error now. 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>
Signed-off-by: Eric Ju <eric.peijian@gmail.com>
---
Documentation/githooks.adoc | 19 ++++++++++++-------
refs.c | 9 ++++++++-
t/t1416-ref-transaction-hooks.sh | 30 ++++++++++++++++++++++++++----
t/t5510-fetch.sh | 7 ++++++-
4 files changed, 52 insertions(+), 13 deletions(-)
diff --git a/Documentation/githooks.adoc b/Documentation/githooks.adoc
index 056553788d..ed045940d1 100644
--- a/Documentation/githooks.adoc
+++ b/Documentation/githooks.adoc
@@ -484,13 +484,16 @@ reference-transaction
~~~~~~~~~~~~~~~~~~~~~
This hook is invoked by any Git command that performs reference
-updates. It executes whenever a reference transaction is prepared,
-committed or aborted and may thus get called multiple times. The hook
-also supports symbolic reference updates.
+updates. It executes whenever a reference transaction is preparing,
+prepared, committed or aborted and may thus get called multiple times.
+The hook also supports symbolic reference updates.
The hook takes exactly one argument, which is the current state the
given reference transaction is in:
+ - "preparing": All reference updates have been queued to the
+ transaction but references are not yet locked on disk.
+
- "prepared": All reference updates have been queued to the
transaction and references were locked on disk.
@@ -511,16 +514,18 @@ ref and `<ref-name>` is the full name of the ref. When force updating
the reference regardless of its current value or when the reference is
to be created anew, `<old-value>` is the all-zeroes object name. To
distinguish these cases, you can inspect the current value of
-`<ref-name>` via `git rev-parse`.
+`<ref-name>` via `git rev-parse`. During the "preparing" state, symbolic
+references are not resolved: `<ref-name>` will reflect the symbolic reference
+itself rather than the object it points to.
For symbolic reference updates the `<old_value>` and `<new-value>`
fields could denote references instead of objects. A reference will be
denoted with a 'ref:' prefix, like `ref:<ref-target>`.
The exit status of the hook is ignored for any state except for the
-"prepared" state. In the "prepared" state, a non-zero exit status will
-cause the transaction to be aborted. The hook will not be called with
-"aborted" state in that case.
+"preparing" and "prepared" states. In these states, a non-zero exit
+status will cause the transaction to be aborted. The hook will not be
+called with "aborted" state in that case.
push-to-checkout
~~~~~~~~~~~~~~~~
diff --git a/refs.c b/refs.c
index 6fb8f9d10c..f1439476d3 100644
--- a/refs.c
+++ b/refs.c
@@ -2655,6 +2655,13 @@ int ref_transaction_prepare(struct ref_transaction *transaction,
if (ref_update_reject_duplicates(&transaction->refnames, err))
return REF_TRANSACTION_ERROR_GENERIC;
+ /* Preparing checks before locking references */
+ ret = run_transaction_hook(transaction, "preparing");
+ if (ret) {
+ ref_transaction_abort(transaction, err);
+ die(_("ref updates aborted by %s hook"), "preparing");
+ }
+
ret = refs->be->transaction_prepare(refs, transaction, err);
if (ret)
return ret;
@@ -2662,7 +2669,7 @@ int ref_transaction_prepare(struct ref_transaction *transaction,
ret = run_transaction_hook(transaction, "prepared");
if (ret) {
ref_transaction_abort(transaction, err);
- die(_("ref updates aborted by hook"));
+ die(_("ref updates aborted by %s hook"), "prepared");
}
return 0;
diff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh
index d91dd3a3b5..2f452049c3 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 "ref updates aborted by preparing 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 "ref updates aborted by prepared 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
--
2.51.0
^ permalink raw reply related [flat|nested] 15+ messages in thread* Re: [PATCH 1/1] Add preparing state to reference-transaction hook
2026-03-13 19:35 ` [PATCH 1/1] Add preparing state " eric.peijian
@ 2026-03-13 21:20 ` Junio C Hamano
2026-03-16 3:09 ` Peijian Ju
2026-03-13 23:05 ` Justin Tobler
1 sibling, 1 reply; 15+ messages in thread
From: Junio C Hamano @ 2026-03-13 21:20 UTC (permalink / raw)
To: eric.peijian; +Cc: git, ps, jltobler
eric.peijian@gmail.com writes:
> From: Eric Ju <eric.peijian@gmail.com>
>
> From: Eric Ju <eju@gitlab.com>
This is curious. The former matches the sign-off, but I somehow
suspect that the @gitlab.com identity may be what you want to use
for both of them, if this is a company sponsored work by an
employee? I dunno.
Also the commit title deviates from the established "<area>: <what
is done>" format.
Subject: [PATCH] refs: add 'preparing" phase to the transaction hook
or something?
Other than that, both the cover letter and the proposed log message
very well explain the motivation behind the new feature. I wish
everybody wrote their log messages as clearly as this one.
> 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 "commit" phase indicates that all updates have been written to disk.
> - The "abort" phase indicates that the transaction has been aborted and that
> all changes have been rolled back.
"commit" -> "committed" and "abort" -> "aborted", if the existing
documentation is to be trusted.
> 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.
I cannot quite picture how "rearrangement" would happen, though.
Would the hook notice "ah there is a preparing hook invocation
incoming", stall the caller by not immediately returning and instead
wait for a different Git process to invoke the same ref-transaction
hook "preparing" invocation, and somehow decide to let the latter go
first before releasing the former?
> 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 to handle unknown phases handle the "preparing" state
"know to handle unknown phrases handle"?
> string will encounter an unknown phase, and that might cause them to return an
> error now. 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.
And documenting the design alternatives and decision like these two
paragraphs is very much appreciated.
The insertion of a new hook invocation itself is at a very much
expected place in the code path. Well written.
Will queue. Thanks.
^ permalink raw reply [flat|nested] 15+ messages in thread* Re: [PATCH 1/1] Add preparing state to reference-transaction hook
2026-03-13 21:20 ` Junio C Hamano
@ 2026-03-16 3:09 ` Peijian Ju
0 siblings, 0 replies; 15+ messages in thread
From: Peijian Ju @ 2026-03-16 3:09 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git, ps, jltobler
On Fri, Mar 13, 2026 at 5:20 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> eric.peijian@gmail.com writes:
>
> > From: Eric Ju <eric.peijian@gmail.com>
> >
> > From: Eric Ju <eju@gitlab.com>
>
> This is curious. The former matches the sign-off, but I somehow
> suspect that the @gitlab.com identity may be what you want to use
> for both of them, if this is a company sponsored work by an
> employee? I dunno.
>
Thank you for pointing this out. During internal review, I used eju@gitlab.com,
but I intended to use eric.peijian@gmail.com for the mailing list submission.
The two `From:` lines got out of sync as a result. Fixed.
> Also the commit title deviates from the established "<area>: <what
> is done>" format.
>
> Subject: [PATCH] refs: add 'preparing" phase to the transaction hook
>
> or something?
>
Thank you. Fixed.
> Other than that, both the cover letter and the proposed log message
> very well explain the motivation behind the new feature. I wish
> everybody wrote their log messages as clearly as this one.
>
Thank you. Much of the credit goes to Patrick (ps@pks.im), who helped
shape the log message.
> > 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 "commit" phase indicates that all updates have been written to disk.
> > - The "abort" phase indicates that the transaction has been aborted and that
> > all changes have been rolled back.
>
> "commit" -> "committed" and "abort" -> "aborted", if the existing
> documentation is to be trusted.
>
Thank you. Fixed.
> > 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.
>
> I cannot quite picture how "rearrangement" would happen, though.
>
> Would the hook notice "ah there is a preparing hook invocation
> incoming", stall the caller by not immediately returning and instead
> wait for a different Git process to invoke the same ref-transaction
> hook "preparing" invocation, and somehow decide to let the latter go
> first before releasing the former?
>
Thank you for asking, happy to clarify. The intended use case is
serializing concurrent write calls in Gitaly/Praefect.
When the hook fires in the "preparing" state, the hook handler
contacts Praefect asking "can I proceed with these ref updates?"
Praefect coordinates across multiple concurrent hook callbacks and
uses this window to determine ordering:
if all callers vote for the same write, they are allowed to proceed;
other write requests are held or aborted until the current one
completes.
> > 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 to handle unknown phases handle the "preparing" state
>
> "know to handle unknown phrases handle"?
>
Fixed.
> > string will encounter an unknown phase, and that might cause them to return an
> > error now. 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.
>
> And documenting the design alternatives and decision like these two
> paragraphs is very much appreciated.
>
> The insertion of a new hook invocation itself is at a very much
> expected place in the code path. Well written.
>
> Will queue. Thanks.
Thank you.
- Eric
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH 1/1] Add preparing state to reference-transaction hook
2026-03-13 19:35 ` [PATCH 1/1] Add preparing state " eric.peijian
2026-03-13 21:20 ` Junio C Hamano
@ 2026-03-13 23:05 ` Justin Tobler
2026-03-13 23:09 ` Junio C Hamano
1 sibling, 1 reply; 15+ messages in thread
From: Justin Tobler @ 2026-03-13 23:05 UTC (permalink / raw)
To: eric.peijian; +Cc: git, ps
On 26/03/13 03:35PM, eric.peijian@gmail.com wrote:
> diff --git a/refs.c b/refs.c
> index 6fb8f9d10c..f1439476d3 100644
> --- a/refs.c
> +++ b/refs.c
> @@ -2655,6 +2655,13 @@ int ref_transaction_prepare(struct ref_transaction *transaction,
> if (ref_update_reject_duplicates(&transaction->refnames, err))
> return REF_TRANSACTION_ERROR_GENERIC;
>
> + /* Preparing checks before locking references */
> + ret = run_transaction_hook(transaction, "preparing");
> + if (ret) {
> + ref_transaction_abort(transaction, err);
> + die(_("ref updates aborted by %s hook"), "preparing");
Should "preparing" be marked for translation here?
> + }
> +
> ret = refs->be->transaction_prepare(refs, transaction, err);
> if (ret)
> return ret;
> @@ -2662,7 +2669,7 @@ int ref_transaction_prepare(struct ref_transaction *transaction,
> ret = run_transaction_hook(transaction, "prepared");
> if (ret) {
> ref_transaction_abort(transaction, err);
> - die(_("ref updates aborted by hook"));
> + die(_("ref updates aborted by %s hook"), "prepared");
Same question here for "prepared"?
Thanks,
-Justin
^ permalink raw reply [flat|nested] 15+ messages in thread* Re: [PATCH 1/1] Add preparing state to reference-transaction hook
2026-03-13 23:05 ` Justin Tobler
@ 2026-03-13 23:09 ` Junio C Hamano
2026-03-16 3:09 ` Peijian Ju
0 siblings, 1 reply; 15+ messages in thread
From: Junio C Hamano @ 2026-03-13 23:09 UTC (permalink / raw)
To: Justin Tobler; +Cc: eric.peijian, git, ps
Justin Tobler <jltobler@gmail.com> writes:
> On 26/03/13 03:35PM, eric.peijian@gmail.com wrote:
>> diff --git a/refs.c b/refs.c
>> index 6fb8f9d10c..f1439476d3 100644
>> --- a/refs.c
>> +++ b/refs.c
>> @@ -2655,6 +2655,13 @@ int ref_transaction_prepare(struct ref_transaction *transaction,
>> if (ref_update_reject_duplicates(&transaction->refnames, err))
>> return REF_TRANSACTION_ERROR_GENERIC;
>>
>> + /* Preparing checks before locking references */
>> + ret = run_transaction_hook(transaction, "preparing");
>> + if (ret) {
>> + ref_transaction_abort(transaction, err);
>> + die(_("ref updates aborted by %s hook"), "preparing");
>
> Should "preparing" be marked for translation here?
It literally is one of the possible tokens reference-transaction
hook is given as its argument, so no, I do not think "preparing"
should be translated.
But the hook that interrupted the ref update is not "preparing"
hook. It is the "reference-transaction" hook. So the message
probably should say something like
the reference-transaction hook rejected ref updates at its
preparing phase
or something.
>> + }
>> +
>> ret = refs->be->transaction_prepare(refs, transaction, err);
>> if (ret)
>> return ret;
>> @@ -2662,7 +2669,7 @@ int ref_transaction_prepare(struct ref_transaction *transaction,
>> ret = run_transaction_hook(transaction, "prepared");
>> if (ret) {
>> ref_transaction_abort(transaction, err);
>> - die(_("ref updates aborted by hook"));
>> + die(_("ref updates aborted by %s hook"), "prepared");
>
> Same question here for "prepared"?
Ditto.
^ permalink raw reply [flat|nested] 15+ messages in thread* Re: [PATCH 1/1] Add preparing state to reference-transaction hook
2026-03-13 23:09 ` Junio C Hamano
@ 2026-03-16 3:09 ` Peijian Ju
0 siblings, 0 replies; 15+ messages in thread
From: Peijian Ju @ 2026-03-16 3:09 UTC (permalink / raw)
To: Junio C Hamano; +Cc: Justin Tobler, git, ps
On Fri, Mar 13, 2026 at 7:10 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> Justin Tobler <jltobler@gmail.com> writes:
>
> > On 26/03/13 03:35PM, eric.peijian@gmail.com wrote:
> >> diff --git a/refs.c b/refs.c
> >> index 6fb8f9d10c..f1439476d3 100644
> >> --- a/refs.c
> >> +++ b/refs.c
> >> @@ -2655,6 +2655,13 @@ int ref_transaction_prepare(struct ref_transaction *transaction,
> >> if (ref_update_reject_duplicates(&transaction->refnames, err))
> >> return REF_TRANSACTION_ERROR_GENERIC;
> >>
> >> + /* Preparing checks before locking references */
> >> + ret = run_transaction_hook(transaction, "preparing");
> >> + if (ret) {
> >> + ref_transaction_abort(transaction, err);
> >> + die(_("ref updates aborted by %s hook"), "preparing");
> >
> > Should "preparing" be marked for translation here?
>
> It literally is one of the possible tokens reference-transaction
> hook is given as its argument, so no, I do not think "preparing"
> should be translated.
>
> But the hook that interrupted the ref update is not "preparing"
> hook. It is the "reference-transaction" hook. So the message
> probably should say something like
>
> the reference-transaction hook rejected ref updates at its
> preparing phase
>
> or something.
>
Thanks for the clarification. Fixed, the message now reads: "ref
updates aborted by the reference-transaction hook at its preparing
phase" (and likewise for "prepared").
> >> + }
> >> +
> >> ret = refs->be->transaction_prepare(refs, transaction, err);
> >> if (ret)
> >> return ret;
> >> @@ -2662,7 +2669,7 @@ int ref_transaction_prepare(struct ref_transaction *transaction,
> >> ret = run_transaction_hook(transaction, "prepared");
> >> if (ret) {
> >> ref_transaction_abort(transaction, err);
> >> - die(_("ref updates aborted by hook"));
> >> + die(_("ref updates aborted by %s hook"), "prepared");
> >
> > Same question here for "prepared"?
>
> Ditto.
Ditto.
Thank you.
- Eric
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH v2 0/1] refs: add 'preparing' phase to the reference-transaction hook
2026-03-13 19:35 [PATCH 0/1] Add "preparing" phase to reference-transaction hook eric.peijian
2026-03-13 19:35 ` [PATCH 1/1] Add preparing state " eric.peijian
@ 2026-03-16 4:51 ` Eric Ju
2026-03-16 4:51 ` [PATCH v2 1/1] " Eric Ju
2026-03-16 7:03 ` [PATCH v2 0/1] " Patrick Steinhardt
2026-03-17 2:36 ` [PATCH v3 " Eric Ju
2 siblings, 2 replies; 15+ messages in thread
From: Eric Ju @ 2026-03-16 4:51 UTC (permalink / raw)
To: git; +Cc: ps, jltobler, eric.peijian, ericju711
The "reference-transaction" hook currently exposes three phases to callers:
"prepared", "committed", and "aborted". The earliest of these, "prepared",
fires after Git has already acquired exclusive locks on every affected
reference. This is well-suited for last-chance validation, but it arrives
too late for any use case that requires coordination before locking, such
as serializing concurrent transactions across distributed storage nodes.
This series introduces a new "preparing" phase that fires before
refs->be->transaction_prepare() is called, that is, before Git takes any
reference lock on disk. Hook scripts that handle this phase receive the full
list of proposed updates and may reject the transaction by returning a
non-zero exit status, causing Git to abort cleanly before any locks are
acquired.
The motivating use case is Gitaly/Praefect, GitLab's distributed Git storage
layer. Praefect must serialize concurrent writes that target the same
references across replicas. With only the "prepared" phase available, by the
time Praefect can observe a transaction the locks are already held, making
reordering impossible. The "preparing" phase provides the necessary
pre-lock window.
Compatibility note: this change is not strictly backwards compatible. Hook
scripts that do not expect unknown phase strings may return an error when
they encounter "preparing". We consider this acceptable for the same reasons
cited when symref support was added to the hook in a8ae923f85 (refs: support
symrefs in 'reference-transaction' hook, 2024-05-07): the hook is documented
as exposing internal implementation details, and its semantics have been
adjusted before. An alternative of introducing a "reference-transaction-v2"
hook was considered but rejected as unnecessarily heavyweight.
---
Changes since v1:
- Fix commit title to follow "area: description" convention
("refs: add 'preparing' phase to reference-transaction hook")
- Correct phase names in documentation to past tense
("committed", "aborted")
- Fix the sentence about backwards compatibility with unknown phases
- Update die() messages to identify the hook by full name and phase
("ref updates rejected by the reference-transaction hook at its
preparing/prepared phase")
- Consolidate author identity to eric.peijian@gmail.com
- Add clarification in reply to the question about how to use the preparing
phase for write serialization
Eric Ju (1):
refs: add 'preparing' phase to the reference-transaction hook
Documentation/githooks.adoc | 19 ++++++++++++-------
refs.c | 9 ++++++++-
t/t1416-ref-transaction-hooks.sh | 30 ++++++++++++++++++++++++++----
t/t5510-fetch.sh | 7 ++++++-
4 files changed, 52 insertions(+), 13 deletions(-)
Range-diff against v1:
1: 5f9f13a84d ! 1: fb74f21d98 Add preparing state to reference-transaction hook
@@
## Metadata ##
-Author: Eric Ju <eju@gitlab.com>
+Author: Eric Ju <eric.peijian@gmail.com>
## Commit message ##
- Add preparing state to reference-transaction hook
+ 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 "commit" phase indicates that all updates have been written to disk.
- - The "abort" phase indicates that the transaction has been aborted and that
+ - 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.
@@ Commit message
interfering with the locking state.
This change is strictly speaking not backwards compatible. Existing hook
- scripts that do not know to handle unknown phases handle the "preparing" state
- string will encounter an unknown phase, and that might cause them to return an
- error now. But the hook is considered to expose internal implementation details
+ 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).
@@ Commit message
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>
## Documentation/githooks.adoc ##
@@ refs.c: int ref_transaction_prepare(struct ref_transaction *transaction,
+ ret = run_transaction_hook(transaction, "preparing");
+ if (ret) {
+ ref_transaction_abort(transaction, err);
-+ die(_("ref updates aborted by %s hook"), "preparing");
++ die(_("ref updates aborted by the reference-transaction hook at its %s state"), "preparing");
+ }
+
ret = refs->be->transaction_prepare(refs, transaction, err);
@@ refs.c: int ref_transaction_prepare(struct ref_transaction *transaction,
if (ret) {
ref_transaction_abort(transaction, err);
- die(_("ref updates aborted by hook"));
-+ die(_("ref updates aborted by %s hook"), "prepared");
++ die(_("ref updates aborted by the reference-transaction hook at its %s state"), "prepared");
}
return 0;
@@ t/t1416-ref-transaction-hooks.sh: test_expect_success 'hook allows updating ref
+ fi
+ EOF
+ test_must_fail git update-ref HEAD POST 2>err &&
-+ test_grep "ref updates aborted by preparing hook" err
++ test_grep "ref updates aborted by the reference-transaction hook at its preparing state" err
+'
+
test_expect_success 'hook aborts updating ref in prepared state' '
@@ t/t1416-ref-transaction-hooks.sh: test_expect_success 'hook aborts updating ref
EOF
test_must_fail git update-ref HEAD POST 2>err &&
- test_grep "ref updates aborted by hook" err
-+ test_grep "ref updates aborted by prepared hook" err
++ test_grep "ref updates aborted by the reference-transaction hook at its prepared state" err
'
test_expect_success 'hook gets all queued updates in prepared state' '
--
2.51.0
^ permalink raw reply [flat|nested] 15+ messages in thread* [PATCH v2 1/1] refs: add 'preparing' phase to the reference-transaction hook
2026-03-16 4:51 ` [PATCH v2 0/1] refs: add 'preparing' phase to the " Eric Ju
@ 2026-03-16 4:51 ` Eric Ju
2026-03-16 16:24 ` Junio C Hamano
2026-03-16 7:03 ` [PATCH v2 0/1] " Patrick Steinhardt
1 sibling, 1 reply; 15+ messages in thread
From: Eric Ju @ 2026-03-16 4:51 UTC (permalink / raw)
To: git; +Cc: ps, jltobler, eric.peijian, ericju711, Karthik Nayak
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>
---
Documentation/githooks.adoc | 19 ++++++++++++-------
refs.c | 9 ++++++++-
t/t1416-ref-transaction-hooks.sh | 30 ++++++++++++++++++++++++++----
t/t5510-fetch.sh | 7 ++++++-
4 files changed, 52 insertions(+), 13 deletions(-)
diff --git a/Documentation/githooks.adoc b/Documentation/githooks.adoc
index 056553788d..ed045940d1 100644
--- a/Documentation/githooks.adoc
+++ b/Documentation/githooks.adoc
@@ -484,13 +484,16 @@ reference-transaction
~~~~~~~~~~~~~~~~~~~~~
This hook is invoked by any Git command that performs reference
-updates. It executes whenever a reference transaction is prepared,
-committed or aborted and may thus get called multiple times. The hook
-also supports symbolic reference updates.
+updates. It executes whenever a reference transaction is preparing,
+prepared, committed or aborted and may thus get called multiple times.
+The hook also supports symbolic reference updates.
The hook takes exactly one argument, which is the current state the
given reference transaction is in:
+ - "preparing": All reference updates have been queued to the
+ transaction but references are not yet locked on disk.
+
- "prepared": All reference updates have been queued to the
transaction and references were locked on disk.
@@ -511,16 +514,18 @@ ref and `<ref-name>` is the full name of the ref. When force updating
the reference regardless of its current value or when the reference is
to be created anew, `<old-value>` is the all-zeroes object name. To
distinguish these cases, you can inspect the current value of
-`<ref-name>` via `git rev-parse`.
+`<ref-name>` via `git rev-parse`. During the "preparing" state, symbolic
+references are not resolved: `<ref-name>` will reflect the symbolic reference
+itself rather than the object it points to.
For symbolic reference updates the `<old_value>` and `<new-value>`
fields could denote references instead of objects. A reference will be
denoted with a 'ref:' prefix, like `ref:<ref-target>`.
The exit status of the hook is ignored for any state except for the
-"prepared" state. In the "prepared" state, a non-zero exit status will
-cause the transaction to be aborted. The hook will not be called with
-"aborted" state in that case.
+"preparing" and "prepared" states. In these states, a non-zero exit
+status will cause the transaction to be aborted. The hook will not be
+called with "aborted" state in that case.
push-to-checkout
~~~~~~~~~~~~~~~~
diff --git a/refs.c b/refs.c
index 6fb8f9d10c..7da37bbb71 100644
--- a/refs.c
+++ b/refs.c
@@ -2655,6 +2655,13 @@ int ref_transaction_prepare(struct ref_transaction *transaction,
if (ref_update_reject_duplicates(&transaction->refnames, err))
return REF_TRANSACTION_ERROR_GENERIC;
+ /* Preparing checks before locking references */
+ ret = run_transaction_hook(transaction, "preparing");
+ if (ret) {
+ ref_transaction_abort(transaction, err);
+ die(_("ref updates aborted by the reference-transaction hook at its %s state"), "preparing");
+ }
+
ret = refs->be->transaction_prepare(refs, transaction, err);
if (ret)
return ret;
@@ -2662,7 +2669,7 @@ int ref_transaction_prepare(struct ref_transaction *transaction,
ret = run_transaction_hook(transaction, "prepared");
if (ret) {
ref_transaction_abort(transaction, err);
- die(_("ref updates aborted by hook"));
+ die(_("ref updates aborted by the reference-transaction hook at its %s state"), "prepared");
}
return 0;
diff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh
index d91dd3a3b5..c3b1a3c735 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 "ref updates aborted by the reference-transaction hook at its preparing state" 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 "ref updates aborted by the reference-transaction hook at its prepared state" 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
--
2.51.0
^ permalink raw reply related [flat|nested] 15+ messages in thread* Re: [PATCH v2 1/1] refs: add 'preparing' phase to the reference-transaction hook
2026-03-16 4:51 ` [PATCH v2 1/1] " Eric Ju
@ 2026-03-16 16:24 ` Junio C Hamano
2026-03-16 23:08 ` Peijian Ju
0 siblings, 1 reply; 15+ messages in thread
From: Junio C Hamano @ 2026-03-16 16:24 UTC (permalink / raw)
To: Eric Ju; +Cc: git, ps, jltobler, ericju711, Karthik Nayak
Eric Ju <eric.peijian@gmail.com> writes:
> + /* Preparing checks before locking references */
> + ret = run_transaction_hook(transaction, "preparing");
> + if (ret) {
> + ref_transaction_abort(transaction, err);
> + die(_("ref updates aborted by the reference-transaction hook at its %s state"), "preparing");
> + }
On end-user's terminal, the above should look like
fatal: ref updates aborted by the reference-transaction hook at its parparing state
consuming more than 80 columns and having the varying part of the
message at the very end. Can we shorten this and highlight the more
important bits? Here is my attempt
die(_("in '%s' phase, update aborted by the reference-transaction hook"),
"preparing");
Enclosing the phase name in 'quotes' and moving it near the
beginning are both my attempt to make it stand out more.
Another thing you may want to consider is to extract the message to
a separate constant, i.e.,
const char *abort_by_ref_transaction_hook[] =
N_("in '%s' phase, update aborted by the reference-transaction hook");
and reuse at two places, perhaps?
die(_(abort_by_ref_transaction_hook), "preparing");
^ permalink raw reply [flat|nested] 15+ messages in thread* Re: [PATCH v2 1/1] refs: add 'preparing' phase to the reference-transaction hook
2026-03-16 16:24 ` Junio C Hamano
@ 2026-03-16 23:08 ` Peijian Ju
0 siblings, 0 replies; 15+ messages in thread
From: Peijian Ju @ 2026-03-16 23:08 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git, ps, jltobler, ericju711, Karthik Nayak
On Mon, Mar 16, 2026 at 12:24 PM Junio C Hamano <gitster@pobox.com> wrote:
>
> Eric Ju <eric.peijian@gmail.com> writes:
>
> > + /* Preparing checks before locking references */
> > + ret = run_transaction_hook(transaction, "preparing");
> > + if (ret) {
> > + ref_transaction_abort(transaction, err);
> > + die(_("ref updates aborted by the reference-transaction hook at its %s state"), "preparing");
> > + }
>
> On end-user's terminal, the above should look like
>
> fatal: ref updates aborted by the reference-transaction hook at its parparing state
>
> consuming more than 80 columns and having the varying part of the
> message at the very end. Can we shorten this and highlight the more
> important bits? Here is my attempt
>
> die(_("in '%s' phase, update aborted by the reference-transaction hook"),
> "preparing");
>
> Enclosing the phase name in 'quotes' and moving it near the
> beginning are both my attempt to make it stand out more.
>
> Another thing you may want to consider is to extract the message to
> a separate constant, i.e.,
>
> const char *abort_by_ref_transaction_hook[] =
> N_("in '%s' phase, update aborted by the reference-transaction hook");
>
> and reuse at two places, perhaps?
>
> die(_(abort_by_ref_transaction_hook), "preparing");
>
Thank you. Fixed in V3.
- Eric
^ permalink raw reply [flat|nested] 15+ messages in thread
* Re: [PATCH v2 0/1] refs: add 'preparing' phase to the reference-transaction hook
2026-03-16 4:51 ` [PATCH v2 0/1] refs: add 'preparing' phase to the " Eric Ju
2026-03-16 4:51 ` [PATCH v2 1/1] " Eric Ju
@ 2026-03-16 7:03 ` Patrick Steinhardt
2026-03-16 23:08 ` Peijian Ju
1 sibling, 1 reply; 15+ messages in thread
From: Patrick Steinhardt @ 2026-03-16 7:03 UTC (permalink / raw)
To: Eric Ju; +Cc: git, jltobler, ericju711
On Mon, Mar 16, 2026 at 12:51:01AM -0400, Eric Ju wrote:
> Changes since v1:
>
> - Fix commit title to follow "area: description" convention
> ("refs: add 'preparing' phase to reference-transaction hook")
> - Correct phase names in documentation to past tense
> ("committed", "aborted")
> - Fix the sentence about backwards compatibility with unknown phases
> - Update die() messages to identify the hook by full name and phase
> ("ref updates rejected by the reference-transaction hook at its
> preparing/prepared phase")
> - Consolidate author identity to eric.peijian@gmail.com
> - Add clarification in reply to the question about how to use the preparing
> phase for write serialization
All of these changes look good to me, thanks. This patch already looks
good to me, but I'm of course biased as I have been helping out behind
the scenes before the first version of this patch landed on the mailing
list.
> Range-diff against v1:
> 1: 5f9f13a84d ! 1: fb74f21d98 Add preparing state to reference-transaction hook
> @@ Commit message
> interfering with the locking state.
>
> This change is strictly speaking not backwards compatible. Existing hook
> - scripts that do not know to handle unknown phases handle the "preparing" state
> - string will encounter an unknown phase, and that might cause them to return an
> - error now. But the hook is considered to expose internal implementation details
> + 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).
One micro-nit: this paragraph could use some reflowing. But I don't
think it's worth a reroll.
Thanks!
Patrick
^ permalink raw reply [flat|nested] 15+ messages in thread* Re: [PATCH v2 0/1] refs: add 'preparing' phase to the reference-transaction hook
2026-03-16 7:03 ` [PATCH v2 0/1] " Patrick Steinhardt
@ 2026-03-16 23:08 ` Peijian Ju
0 siblings, 0 replies; 15+ messages in thread
From: Peijian Ju @ 2026-03-16 23:08 UTC (permalink / raw)
To: Patrick Steinhardt; +Cc: git, jltobler, ericju711
On Mon, Mar 16, 2026 at 3:03 AM Patrick Steinhardt <ps@pks.im> wrote:
>
> On Mon, Mar 16, 2026 at 12:51:01AM -0400, Eric Ju wrote:
> > Changes since v1:
> >
> > - Fix commit title to follow "area: description" convention
> > ("refs: add 'preparing' phase to reference-transaction hook")
> > - Correct phase names in documentation to past tense
> > ("committed", "aborted")
> > - Fix the sentence about backwards compatibility with unknown phases
> > - Update die() messages to identify the hook by full name and phase
> > ("ref updates rejected by the reference-transaction hook at its
> > preparing/prepared phase")
> > - Consolidate author identity to eric.peijian@gmail.com
> > - Add clarification in reply to the question about how to use the preparing
> > phase for write serialization
>
> All of these changes look good to me, thanks. This patch already looks
> good to me, but I'm of course biased as I have been helping out behind
> the scenes before the first version of this patch landed on the mailing
> list.
>
> > Range-diff against v1:
> > 1: 5f9f13a84d ! 1: fb74f21d98 Add preparing state to reference-transaction hook
> > @@ Commit message
> > interfering with the locking state.
> >
> > This change is strictly speaking not backwards compatible. Existing hook
> > - scripts that do not know to handle unknown phases handle the "preparing" state
> > - string will encounter an unknown phase, and that might cause them to return an
> > - error now. But the hook is considered to expose internal implementation details
> > + 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).
>
> One micro-nit: this paragraph could use some reflowing. But I don't
> think it's worth a reroll.
>
> Thanks!
>
> Patrick
Thank you. I will reflow the paragraph in v3, which I am already
planning to send for the error message and string constant changes.
- Eric
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH v3 0/1] refs: add 'preparing' phase to the reference-transaction hook
2026-03-13 19:35 [PATCH 0/1] Add "preparing" phase to reference-transaction hook eric.peijian
2026-03-13 19:35 ` [PATCH 1/1] Add preparing state " eric.peijian
2026-03-16 4:51 ` [PATCH v2 0/1] refs: add 'preparing' phase to the " Eric Ju
@ 2026-03-17 2:36 ` Eric Ju
2026-03-17 2:36 ` [PATCH v3 1/1] " Eric Ju
2 siblings, 1 reply; 15+ messages in thread
From: Eric Ju @ 2026-03-17 2:36 UTC (permalink / raw)
To: git; +Cc: ps, jltobler, eric.peijian, ericju711
The "reference-transaction" hook currently exposes three phases to callers:
"prepared", "committed", and "aborted". The earliest of these, "prepared",
fires after Git has already acquired exclusive locks on every affected
reference. This is well-suited for last-chance validation, but it arrives
too late for any use case that requires coordination before locking, such
as serializing concurrent transactions across distributed storage nodes.
This series introduces a new "preparing" phase that fires before
refs->be->transaction_prepare() is called, that is, before Git takes any
reference lock on disk. Hook scripts that handle this phase receive the full
list of proposed updates and may reject the transaction by returning a
non-zero exit status, causing Git to abort cleanly before any locks are
acquired.
The motivating use case is Gitaly/Praefect, GitLab's distributed Git storage
layer. Praefect must serialize concurrent writes that target the same
references across replicas. With only the "prepared" phase available, by the
time Praefect can observe a transaction the locks are already held, making
reordering impossible. The "preparing" phase provides the necessary
pre-lock window.
Compatibility note: this change is not strictly backwards compatible. Hook
scripts that do not expect unknown phase strings may return an error when
they encounter "preparing". We consider this acceptable for the same reasons
cited when symref support was added to the hook in a8ae923f85 (refs: support
symrefs in 'reference-transaction' hook, 2024-05-07): the hook is documented
as exposing internal implementation details, and its semantics have been
adjusted before. An alternative of introducing a "reference-transaction-v2"
hook was considered but rejected as unnecessarily heavyweight.
---
Changes since v2:
- Shorten and reorder die() message to highlight the phase name early
- Extract the error message into a file-scope static constant to avoid
duplication across the "preparing" and "prepared" call sites
- Reflow the backwards compatibility paragraph in the commit message
Eric Ju (1):
refs: add 'preparing' phase to the reference-transaction hook
Documentation/githooks.adoc | 19 ++++++++++++-------
refs.c | 12 +++++++++++-
t/t1416-ref-transaction-hooks.sh | 30 ++++++++++++++++++++++++++----
t/t5510-fetch.sh | 7 ++++++-
4 files changed, 55 insertions(+), 13 deletions(-)
Range-diff against v2:
1: 4fff10e694 ! 1: 39f7a2bc4b refs: add 'preparing' phase to the reference-transaction hook
@@ Commit message
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).
+ 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,
@@ Commit message
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>
## Documentation/githooks.adoc ##
@@ Documentation/githooks.adoc: ref and `<ref-name>` is the full name of the ref. W
~~~~~~~~~~~~~~~~
## refs.c ##
+@@ refs.c: const char *ref_storage_format_to_name(enum ref_storage_format ref_storage_forma
+ return be->name;
+ }
+
++static const char *abort_by_ref_transaction_hook =
++ N_("in '%s' phase, update aborted by the reference-transaction hook");
++
+ /*
+ * How to handle various characters in refnames:
+ * 0: An acceptable character for refs
@@ refs.c: int ref_transaction_prepare(struct ref_transaction *transaction,
if (ref_update_reject_duplicates(&transaction->refnames, err))
return REF_TRANSACTION_ERROR_GENERIC;
@@ refs.c: int ref_transaction_prepare(struct ref_transaction *transaction,
+ ret = run_transaction_hook(transaction, "preparing");
+ if (ret) {
+ ref_transaction_abort(transaction, err);
-+ die(_("ref updates aborted by the reference-transaction hook at its %s state"), "preparing");
++ die(_(abort_by_ref_transaction_hook), "preparing");
+ }
+
ret = refs->be->transaction_prepare(refs, transaction, err);
@@ refs.c: int ref_transaction_prepare(struct ref_transaction *transaction,
if (ret) {
ref_transaction_abort(transaction, err);
- die(_("ref updates aborted by hook"));
-+ die(_("ref updates aborted by the reference-transaction hook at its %s state"), "prepared");
++ die(_(abort_by_ref_transaction_hook), "prepared");
}
return 0;
@@ t/t1416-ref-transaction-hooks.sh: test_expect_success 'hook allows updating ref
+ fi
+ EOF
+ test_must_fail git update-ref HEAD POST 2>err &&
-+ test_grep "ref updates aborted by the reference-transaction hook at its preparing state" err
++ test_grep "in '\''preparing'\'' phase, update aborted by the reference-transaction hook" err
+'
+
test_expect_success 'hook aborts updating ref in prepared state' '
@@ t/t1416-ref-transaction-hooks.sh: test_expect_success 'hook aborts updating ref
EOF
test_must_fail git update-ref HEAD POST 2>err &&
- test_grep "ref updates aborted by hook" err
-+ test_grep "ref updates aborted by the reference-transaction hook at its prepared state" 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' '
--
2.51.0
^ permalink raw reply [flat|nested] 15+ messages in thread* [PATCH v3 1/1] refs: add 'preparing' phase to the reference-transaction hook
2026-03-17 2:36 ` [PATCH v3 " Eric Ju
@ 2026-03-17 2:36 ` Eric Ju
0 siblings, 0 replies; 15+ messages in thread
From: Eric Ju @ 2026-03-17 2:36 UTC (permalink / raw)
To: git; +Cc: ps, jltobler, eric.peijian, ericju711, Karthik Nayak
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>
---
Documentation/githooks.adoc | 19 ++++++++++++-------
refs.c | 12 +++++++++++-
t/t1416-ref-transaction-hooks.sh | 30 ++++++++++++++++++++++++++----
t/t5510-fetch.sh | 7 ++++++-
4 files changed, 55 insertions(+), 13 deletions(-)
diff --git a/Documentation/githooks.adoc b/Documentation/githooks.adoc
index 056553788d..ed045940d1 100644
--- a/Documentation/githooks.adoc
+++ b/Documentation/githooks.adoc
@@ -484,13 +484,16 @@ reference-transaction
~~~~~~~~~~~~~~~~~~~~~
This hook is invoked by any Git command that performs reference
-updates. It executes whenever a reference transaction is prepared,
-committed or aborted and may thus get called multiple times. The hook
-also supports symbolic reference updates.
+updates. It executes whenever a reference transaction is preparing,
+prepared, committed or aborted and may thus get called multiple times.
+The hook also supports symbolic reference updates.
The hook takes exactly one argument, which is the current state the
given reference transaction is in:
+ - "preparing": All reference updates have been queued to the
+ transaction but references are not yet locked on disk.
+
- "prepared": All reference updates have been queued to the
transaction and references were locked on disk.
@@ -511,16 +514,18 @@ ref and `<ref-name>` is the full name of the ref. When force updating
the reference regardless of its current value or when the reference is
to be created anew, `<old-value>` is the all-zeroes object name. To
distinguish these cases, you can inspect the current value of
-`<ref-name>` via `git rev-parse`.
+`<ref-name>` via `git rev-parse`. During the "preparing" state, symbolic
+references are not resolved: `<ref-name>` will reflect the symbolic reference
+itself rather than the object it points to.
For symbolic reference updates the `<old_value>` and `<new-value>`
fields could denote references instead of objects. A reference will be
denoted with a 'ref:' prefix, like `ref:<ref-target>`.
The exit status of the hook is ignored for any state except for the
-"prepared" state. In the "prepared" state, a non-zero exit status will
-cause the transaction to be aborted. The hook will not be called with
-"aborted" state in that case.
+"preparing" and "prepared" states. In these states, a non-zero exit
+status will cause the transaction to be aborted. The hook will not be
+called with "aborted" state in that case.
push-to-checkout
~~~~~~~~~~~~~~~~
diff --git a/refs.c b/refs.c
index 6fb8f9d10c..e66cf4861d 100644
--- a/refs.c
+++ b/refs.c
@@ -64,6 +64,9 @@ const char *ref_storage_format_to_name(enum ref_storage_format ref_storage_forma
return be->name;
}
+static const char *abort_by_ref_transaction_hook =
+ N_("in '%s' phase, update aborted by the reference-transaction hook");
+
/*
* How to handle various characters in refnames:
* 0: An acceptable character for refs
@@ -2655,6 +2658,13 @@ int ref_transaction_prepare(struct ref_transaction *transaction,
if (ref_update_reject_duplicates(&transaction->refnames, err))
return REF_TRANSACTION_ERROR_GENERIC;
+ /* Preparing checks before locking references */
+ ret = run_transaction_hook(transaction, "preparing");
+ if (ret) {
+ ref_transaction_abort(transaction, err);
+ die(_(abort_by_ref_transaction_hook), "preparing");
+ }
+
ret = refs->be->transaction_prepare(refs, transaction, err);
if (ret)
return ret;
@@ -2662,7 +2672,7 @@ int ref_transaction_prepare(struct ref_transaction *transaction,
ret = run_transaction_hook(transaction, "prepared");
if (ret) {
ref_transaction_abort(transaction, err);
- die(_("ref updates aborted by hook"));
+ die(_(abort_by_ref_transaction_hook), "prepared");
}
return 0;
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
--
2.51.0
^ permalink raw reply related [flat|nested] 15+ messages in thread