{"thread":{"id":"65240","subject":"[PATCH 0/1] Add \"preparing\" phase to reference-transaction hook","startedAt":"2026-03-13T19:36:18Z","lastAt":"2026-03-17T02:36:30Z","messageCount":15,"participants":["eric.peijian@gmail.com","Junio C Hamano","Justin Tobler","Peijian Ju","Eric Ju","Patrick Steinhardt"],"isPatch":true,"patchVersion":1,"patchTotal":1},"messages":[{"id":"538927","messageId":"20260313193537.62827-1-eric.peijian@gmail.com","threadId":"65240","inReplyTo":null,"subject":"[PATCH 0/1] Add \"preparing\" phase to reference-transaction hook","fromName":"","fromEmail":"eric.peijian@gmail.com","sentAt":"2026-03-13T19:35:35Z","receivedAt":"2026-03-13T19:36:18Z","isPatch":true,"sender":{"key":"eric.peijian@gmail.com","avatar":"https://avatars.githubusercontent.com/u/7400645?v=4"},"body":"From: Eric Ju <eric.peijian@gmail.com>\n\nThe \"reference-transaction\" hook currently exposes three phases to callers:\n\"prepared\", \"committed\", and \"aborted\". The earliest of these, \"prepared\",\nfires after Git has already acquired exclusive locks on every affected\nreference. This is well-suited for last-chance validation, but it arrives\ntoo late for any use case that requires coordination before locking, such\nas serializing concurrent transactions across distributed storage nodes.\n\nThis series introduces a new \"preparing\" phase that fires before\nrefs->be->transaction_prepare() is called, that is, before Git takes any\nreference lock on disk. Hook scripts that handle this phase receive the full\nlist of proposed updates and may reject the transaction by returning a\nnon-zero exit status, causing Git to abort cleanly before any locks are\nacquired.\n\nThe motivating use case is Gitaly/Praefect, GitLab's distributed Git storage\nlayer. Praefect must serialize concurrent writes that target the same\nreferences across replicas. With only the \"prepared\" phase available, by the\ntime Praefect can observe a transaction the locks are already held, making\nreordering impossible. The \"preparing\" phase provides the necessary\npre-lock window.\n\nCompatibility note: this change is not strictly backwards compatible. Hook\nscripts that do not expect unknown phase strings may return an error when\nthey encounter \"preparing\". We consider this acceptable for the same reasons\ncited when symref support was added to the hook in a8ae923f85 (refs: support\nsymrefs in 'reference-transaction' hook, 2024-05-07): the hook is documented\nas exposing internal implementation details, and its semantics have been\nadjusted before. An alternative of introducing a \"reference-transaction-v2\"\nhook was considered but rejected as unnecessarily heavyweight.\n\nEric Ju (1):\n  Add preparing state to reference-transaction hook\n\n Documentation/githooks.adoc      | 19 ++++++++++++-------\n refs.c                           |  9 ++++++++-\n t/t1416-ref-transaction-hooks.sh | 30 ++++++++++++++++++++++++++----\n t/t5510-fetch.sh                 |  7 ++++++-\n 4 files changed, 52 insertions(+), 13 deletions(-)\n\n-- \n2.51.0\n\n"},{"id":"538928","messageId":"20260313193537.62827-2-eric.peijian@gmail.com","threadId":"65240","inReplyTo":"20260313193537.62827-1-eric.peijian@gmail.com","subject":"[PATCH 1/1] Add preparing state to reference-transaction hook","fromName":"","fromEmail":"eric.peijian@gmail.com","sentAt":"2026-03-13T19:35:36Z","receivedAt":"2026-03-13T19:36:20Z","isPatch":true,"sender":{"key":"eric.peijian@gmail.com","avatar":"https://avatars.githubusercontent.com/u/7400645?v=4"},"body":"From: Eric Ju <eric.peijian@gmail.com>\n\nFrom: Eric Ju <eju@gitlab.com>\n\nThe \"reference-transaction\" hook is invoked multiple times during a ref\ntransaction. Each invocation corresponds to a different phase:\n\n- The \"prepared\" phase indicates that references have been locked.\n- The \"commit\" phase indicates that all updates have been written to disk.\n- The \"abort\" phase indicates that the transaction has been aborted and that\n  all changes have been rolled back.\n\nThis hook can be used to learn about the updates that Git wants to perform.\nFor example, forges use it to coordinate reference updates across multiple\nnodes.\n\nHowever, the phases are insufficient for some specific use cases. The earliest\nobservable phase in the \"reference-transaction\" hook is \"prepared\", at which\npoint Git has already taken exclusive locks on every affected reference. This\nmakes it suitable for last-chance validation, but not for serialization. So by\nthe time a hook sees the \"prepared\" phase, it has no way to defer locking, and\nthus it cannot rearrange multiple concurrent ref transactions relative to one\nanother.\n\nIntroduce a new \"preparing\" phase that runs before the \"prepared\" phase, that\nis before Git acquires any reference lock on disk. This gives callers a\nwell-defined window to perform validation, enable higher-level ordering of\nconcurrent transactions, or reject the transaction entirely, all without\ninterfering with the locking state.\n\nThis change is strictly speaking not backwards compatible. Existing hook\nscripts that do not know to handle unknown phases handle the \"preparing\" state\nstring will encounter an unknown phase, and that might cause them to return an\nerror now. But the hook is considered to expose internal implementation details\nof how Git works, and as such we have been a bit more lenient with changing its\nexact semantics, like for example in a8ae923f85 (refs: support symrefs in\n'reference-transaction' hook, 2024-05-07).\n\nAn alternative would be to introduce a \"reference-transaction-v2\" hook that\nknows about the new phase. This feels like a rather heavy-weight option though,\nand was thus discarded.\n\nHelped-by: Patrick Steinhardt <ps@pks.im>\nHelped-by: Justin Tobler <jltobler@gmail.com>\nSigned-off-by: Eric Ju <eric.peijian@gmail.com>\n---\n Documentation/githooks.adoc      | 19 ++++++++++++-------\n refs.c                           |  9 ++++++++-\n t/t1416-ref-transaction-hooks.sh | 30 ++++++++++++++++++++++++++----\n t/t5510-fetch.sh                 |  7 ++++++-\n 4 files changed, 52 insertions(+), 13 deletions(-)\n\ndiff --git a/Documentation/githooks.adoc b/Documentation/githooks.adoc\nindex 056553788d..ed045940d1 100644\n--- a/Documentation/githooks.adoc\n+++ b/Documentation/githooks.adoc\n@@ -484,13 +484,16 @@ reference-transaction\n ~~~~~~~~~~~~~~~~~~~~~\n \n This hook is invoked by any Git command that performs reference\n-updates. It executes whenever a reference transaction is prepared,\n-committed or aborted and may thus get called multiple times. The hook\n-also supports symbolic reference updates.\n+updates. It executes whenever a reference transaction is preparing,\n+prepared, committed or aborted and may thus get called multiple times.\n+The hook also supports symbolic reference updates.\n \n The hook takes exactly one argument, which is the current state the\n given reference transaction is in:\n \n+    - \"preparing\": All reference updates have been queued to the\n+      transaction but references are not yet locked on disk.\n+\n     - \"prepared\": All reference updates have been queued to the\n       transaction and references were locked on disk.\n \n@@ -511,16 +514,18 @@ ref and `<ref-name>` is the full name of the ref. When force updating\n the reference regardless of its current value or when the reference is\n to be created anew, `<old-value>` is the all-zeroes object name. To\n distinguish these cases, you can inspect the current value of\n-`<ref-name>` via `git rev-parse`.\n+`<ref-name>` via `git rev-parse`. During the \"preparing\" state, symbolic\n+references are not resolved: `<ref-name>` will reflect the symbolic reference\n+itself rather than the object it points to.\n \n For symbolic reference updates the `<old_value>` and `<new-value>`\n fields could denote references instead of objects. A reference will be\n denoted with a 'ref:' prefix, like `ref:<ref-target>`.\n \n The exit status of the hook is ignored for any state except for the\n-\"prepared\" state. In the \"prepared\" state, a non-zero exit status will\n-cause the transaction to be aborted. The hook will not be called with\n-\"aborted\" state in that case.\n+\"preparing\" and \"prepared\" states. In these states, a non-zero exit\n+status will cause the transaction to be aborted. The hook will not be\n+called with \"aborted\" state in that case.\n \n push-to-checkout\n ~~~~~~~~~~~~~~~~\ndiff --git a/refs.c b/refs.c\nindex 6fb8f9d10c..f1439476d3 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2655,6 +2655,13 @@ int ref_transaction_prepare(struct ref_transaction *transaction,\n \tif (ref_update_reject_duplicates(&transaction->refnames, err))\n \t\treturn REF_TRANSACTION_ERROR_GENERIC;\n \n+\t/* Preparing checks before locking references */\n+\tret = run_transaction_hook(transaction, \"preparing\");\n+\tif (ret) {\n+\t\tref_transaction_abort(transaction, err);\n+\t\tdie(_(\"ref updates aborted by %s hook\"), \"preparing\");\n+\t}\n+\n \tret = refs->be->transaction_prepare(refs, transaction, err);\n \tif (ret)\n \t\treturn ret;\n@@ -2662,7 +2669,7 @@ int ref_transaction_prepare(struct ref_transaction *transaction,\n \tret = run_transaction_hook(transaction, \"prepared\");\n \tif (ret) {\n \t\tref_transaction_abort(transaction, err);\n-\t\tdie(_(\"ref updates aborted by hook\"));\n+\t\tdie(_(\"ref updates aborted by %s hook\"), \"prepared\");\n \t}\n \n \treturn 0;\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex d91dd3a3b5..2f452049c3 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -20,6 +20,7 @@ test_expect_success 'hook allows updating ref if successful' '\n \t\techo \"$*\" >>actual\n \tEOF\n \tcat >expect <<-EOF &&\n+\t\tpreparing\n \t\tprepared\n \t\tcommitted\n \tEOF\n@@ -27,6 +28,18 @@ test_expect_success 'hook allows updating ref if successful' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'hook aborts updating ref in preparing state' '\n+\tgit reset --hard PRE &&\n+\ttest_hook reference-transaction <<-\\EOF &&\n+\t\tif test \"$1\" = preparing\n+\t\tthen\n+\t\t\texit 1\n+\t\tfi\n+\tEOF\n+\ttest_must_fail git update-ref HEAD POST 2>err &&\n+\ttest_grep \"ref updates aborted by preparing hook\" err\n+'\n+\n test_expect_success 'hook aborts updating ref in prepared state' '\n \tgit reset --hard PRE &&\n \ttest_hook reference-transaction <<-\\EOF &&\n@@ -36,7 +49,7 @@ test_expect_success 'hook aborts updating ref in prepared state' '\n \t\tfi\n \tEOF\n \ttest_must_fail git update-ref HEAD POST 2>err &&\n-\ttest_grep \"ref updates aborted by hook\" err\n+\ttest_grep \"ref updates aborted by prepared hook\" err\n '\n \n test_expect_success 'hook gets all queued updates in prepared state' '\n@@ -121,6 +134,7 @@ test_expect_success 'interleaving hook calls succeed' '\n \tcat >expect <<-EOF &&\n \t\thooks/update refs/tags/PRE $ZERO_OID $PRE_OID\n \t\thooks/update refs/tags/POST $ZERO_OID $POST_OID\n+\t\thooks/reference-transaction preparing\n \t\thooks/reference-transaction prepared\n \t\thooks/reference-transaction committed\n \tEOF\n@@ -143,6 +157,8 @@ test_expect_success 'hook captures git-symbolic-ref updates' '\n \tgit symbolic-ref refs/heads/symref refs/heads/main &&\n \n \tcat >expect <<-EOF &&\n+\tpreparing\n+\t$ZERO_OID ref:refs/heads/main refs/heads/symref\n \tprepared\n \t$ZERO_OID ref:refs/heads/main refs/heads/symref\n \tcommitted\n@@ -171,14 +187,20 @@ test_expect_success 'hook gets all queued symref updates' '\n \t# In the files backend, \"delete\" also triggers an additional transaction\n \t# update on the packed-refs backend, which constitutes additional reflog\n \t# entries.\n+\tcat >expect <<-EOF &&\n+\tpreparing\n+\tref:refs/heads/main $ZERO_OID refs/heads/symref\n+\tref:refs/heads/main $ZERO_OID refs/heads/symrefd\n+\t$ZERO_OID ref:refs/heads/main refs/heads/symrefc\n+\tref:refs/heads/main ref:refs/heads/branch refs/heads/symrefu\n+\tEOF\n+\n \tif test_have_prereq REFFILES\n \tthen\n-\t\tcat >expect <<-EOF\n+\t\tcat >>expect <<-EOF\n \t\taborted\n \t\t$ZERO_OID $ZERO_OID refs/heads/symrefd\n \t\tEOF\n-\telse\n-\t\t>expect\n \tfi &&\n \n \tcat >>expect <<-EOF &&\ndiff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh\nindex 5dcb4b51a4..6fe21e2b3a 100755\n--- a/t/t5510-fetch.sh\n+++ b/t/t5510-fetch.sh\n@@ -469,12 +469,17 @@ test_expect_success 'fetch --atomic executes a single reference transaction only\n \thead_oid=$(git rev-parse HEAD) &&\n \n \tcat >expected <<-EOF &&\n+\t\tpreparing\n+\t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-1\n+\t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-2\n \t\tprepared\n \t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-1\n \t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-2\n \t\tcommitted\n \t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-1\n \t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-2\n+\t\tpreparing\n+\t\t$ZERO_OID ref:refs/remotes/origin/main refs/remotes/origin/HEAD\n \tEOF\n \n \trm -f atomic/actual &&\n@@ -497,7 +502,7 @@ test_expect_success 'fetch --atomic aborts all reference updates if hook aborts'\n \thead_oid=$(git rev-parse HEAD) &&\n \n \tcat >expected <<-EOF &&\n-\t\tprepared\n+\t\tpreparing\n \t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-abort-1\n \t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-abort-2\n \t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-abort-3\n-- \n2.51.0\n\n"},{"id":"538935","messageId":"xmqqpl572zq2.fsf@gitster.g","threadId":"65240","inReplyTo":"20260313193537.62827-2-eric.peijian@gmail.com","subject":"Re: [PATCH 1/1] Add preparing state to reference-transaction hook","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-13T21:20:37Z","receivedAt":"2026-03-13T21:20:41Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"eric.peijian@gmail.com writes:\n\n> From: Eric Ju <eric.peijian@gmail.com>\n>\n> From: Eric Ju <eju@gitlab.com>\n\nThis is curious.  The former matches the sign-off, but I somehow\nsuspect that the @gitlab.com identity may be what you want to use\nfor both of them, if this is a company sponsored work by an\nemployee?  I dunno.\n\nAlso the commit title deviates from the established \"<area>: <what\nis done>\" format.\n\n    Subject: [PATCH] refs: add 'preparing\" phase to the transaction hook\n\nor something?\n\nOther than that, both the cover letter and the proposed log message\nvery well explain the motivation behind the new feature.  I wish\neverybody wrote their log messages as clearly as this one.\n\n> The \"reference-transaction\" hook is invoked multiple times during a ref\n> transaction. Each invocation corresponds to a different phase:\n>\n> - The \"prepared\" phase indicates that references have been locked.\n> - The \"commit\" phase indicates that all updates have been written to disk.\n> - The \"abort\" phase indicates that the transaction has been aborted and that\n>   all changes have been rolled back.\n\n\"commit\" -> \"committed\" and \"abort\" -> \"aborted\", if the existing\ndocumentation is to be trusted.\n\n> This hook can be used to learn about the updates that Git wants to perform.\n> For example, forges use it to coordinate reference updates across multiple\n> nodes.\n>\n> However, the phases are insufficient for some specific use cases. The earliest\n> observable phase in the \"reference-transaction\" hook is \"prepared\", at which\n> point Git has already taken exclusive locks on every affected reference. This\n> makes it suitable for last-chance validation, but not for serialization. So by\n> the time a hook sees the \"prepared\" phase, it has no way to defer locking, and\n> thus it cannot rearrange multiple concurrent ref transactions relative to one\n> another.\n\nI cannot quite picture how \"rearrangement\" would happen, though.\n\nWould the hook notice \"ah there is a preparing hook invocation\nincoming\", stall the caller by not immediately returning and instead\nwait for a different Git process to invoke the same ref-transaction\nhook \"preparing\" invocation, and somehow decide to let the latter go\nfirst before releasing the former?\n\n> Introduce a new \"preparing\" phase that runs before the \"prepared\" phase, that\n> is before Git acquires any reference lock on disk. This gives callers a\n> well-defined window to perform validation, enable higher-level ordering of\n> concurrent transactions, or reject the transaction entirely, all without\n> interfering with the locking state.\n>\n> This change is strictly speaking not backwards compatible. Existing hook\n> scripts that do not know to handle unknown phases handle the \"preparing\" state\n\n\"know to handle unknown phrases handle\"?\n\n> string will encounter an unknown phase, and that might cause them to return an\n> error now. But the hook is considered to expose internal implementation details\n> of how Git works, and as such we have been a bit more lenient with changing its\n> exact semantics, like for example in a8ae923f85 (refs: support symrefs in\n> 'reference-transaction' hook, 2024-05-07).\n>\n> An alternative would be to introduce a \"reference-transaction-v2\" hook that\n> knows about the new phase. This feels like a rather heavy-weight option though,\n> and was thus discarded.\n\nAnd documenting the design alternatives and decision like these two\nparagraphs is very much appreciated.\n\nThe insertion of a new hook invocation itself is at a very much\nexpected place in the code path.  Well written.\n\nWill queue.  Thanks.\n"},{"id":"538942","messageId":"abSWrcvm-smx92MY@denethor","threadId":"65240","inReplyTo":"20260313193537.62827-2-eric.peijian@gmail.com","subject":"Re: [PATCH 1/1] Add preparing state to reference-transaction hook","fromName":"Justin Tobler","fromEmail":"jltobler@gmail.com","sentAt":"2026-03-13T23:05:46Z","receivedAt":"2026-03-13T23:05:50Z","isPatch":true,"sender":{"key":"jltobler@gmail.com","avatar":"https://avatars.githubusercontent.com/u/53454972?v=4"},"body":"On 26/03/13 03:35PM, eric.peijian@gmail.com wrote:\n> diff --git a/refs.c b/refs.c\n> index 6fb8f9d10c..f1439476d3 100644\n> --- a/refs.c\n> +++ b/refs.c\n> @@ -2655,6 +2655,13 @@ int ref_transaction_prepare(struct ref_transaction *transaction,\n>  \tif (ref_update_reject_duplicates(&transaction->refnames, err))\n>  \t\treturn REF_TRANSACTION_ERROR_GENERIC;\n>  \n> +\t/* Preparing checks before locking references */\n> +\tret = run_transaction_hook(transaction, \"preparing\");\n> +\tif (ret) {\n> +\t\tref_transaction_abort(transaction, err);\n> +\t\tdie(_(\"ref updates aborted by %s hook\"), \"preparing\");\n\nShould \"preparing\" be marked for translation here?\n\n> +\t}\n> +\n>  \tret = refs->be->transaction_prepare(refs, transaction, err);\n>  \tif (ret)\n>  \t\treturn ret;\n> @@ -2662,7 +2669,7 @@ int ref_transaction_prepare(struct ref_transaction *transaction,\n>  \tret = run_transaction_hook(transaction, \"prepared\");\n>  \tif (ret) {\n>  \t\tref_transaction_abort(transaction, err);\n> -\t\tdie(_(\"ref updates aborted by hook\"));\n> +\t\tdie(_(\"ref updates aborted by %s hook\"), \"prepared\");\n\nSame question here for \"prepared\"?\n\nThanks,\n-Justin\n"},{"id":"538944","messageId":"xmqq7brf1g3e.fsf@gitster.g","threadId":"65240","inReplyTo":"abSWrcvm-smx92MY@denethor","subject":"Re: [PATCH 1/1] Add preparing state to reference-transaction hook","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-13T23:09:57Z","receivedAt":"2026-03-13T23:10:00Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Justin Tobler <jltobler@gmail.com> writes:\n\n> On 26/03/13 03:35PM, eric.peijian@gmail.com wrote:\n>> diff --git a/refs.c b/refs.c\n>> index 6fb8f9d10c..f1439476d3 100644\n>> --- a/refs.c\n>> +++ b/refs.c\n>> @@ -2655,6 +2655,13 @@ int ref_transaction_prepare(struct ref_transaction *transaction,\n>>  \tif (ref_update_reject_duplicates(&transaction->refnames, err))\n>>  \t\treturn REF_TRANSACTION_ERROR_GENERIC;\n>>  \n>> +\t/* Preparing checks before locking references */\n>> +\tret = run_transaction_hook(transaction, \"preparing\");\n>> +\tif (ret) {\n>> +\t\tref_transaction_abort(transaction, err);\n>> +\t\tdie(_(\"ref updates aborted by %s hook\"), \"preparing\");\n>\n> Should \"preparing\" be marked for translation here?\n\nIt literally is one of the possible tokens reference-transaction\nhook is given as its argument, so no, I do not think \"preparing\"\nshould be translated.\n\nBut the hook that interrupted the ref update is not \"preparing\"\nhook.  It is the \"reference-transaction\" hook.  So the message\nprobably should say something like\n\n    the reference-transaction hook rejected ref updates at its\n    preparing phase\n\nor something.\n\n>> +\t}\n>> +\n>>  \tret = refs->be->transaction_prepare(refs, transaction, err);\n>>  \tif (ret)\n>>  \t\treturn ret;\n>> @@ -2662,7 +2669,7 @@ int ref_transaction_prepare(struct ref_transaction *transaction,\n>>  \tret = run_transaction_hook(transaction, \"prepared\");\n>>  \tif (ret) {\n>>  \t\tref_transaction_abort(transaction, err);\n>> -\t\tdie(_(\"ref updates aborted by hook\"));\n>> +\t\tdie(_(\"ref updates aborted by %s hook\"), \"prepared\");\n>\n> Same question here for \"prepared\"?\n\nDitto.\n"},{"id":"539059","messageId":"CAN2LT1D+_JRz4uknimQw0Fw559gxAwSJvhjfqGNZbZCwG6oNkg@mail.gmail.com","threadId":"65240","inReplyTo":"xmqqpl572zq2.fsf@gitster.g","subject":"Re: [PATCH 1/1] Add preparing state to reference-transaction hook","fromName":"Peijian Ju","fromEmail":"eric.peijian@gmail.com","sentAt":"2026-03-16T03:09:27Z","receivedAt":"2026-03-16T03:09:38Z","isPatch":true,"sender":{"key":"eric.peijian@gmail.com","avatar":"https://avatars.githubusercontent.com/u/7400645?v=4"},"body":"On Fri, Mar 13, 2026 at 5:20 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> eric.peijian@gmail.com writes:\n>\n> > From: Eric Ju <eric.peijian@gmail.com>\n> >\n> > From: Eric Ju <eju@gitlab.com>\n>\n> This is curious.  The former matches the sign-off, but I somehow\n> suspect that the @gitlab.com identity may be what you want to use\n> for both of them, if this is a company sponsored work by an\n> employee?  I dunno.\n>\n\nThank you for pointing this out. During internal review, I used eju@gitlab.com,\nbut I intended to use eric.peijian@gmail.com for the mailing list submission.\nThe two `From:` lines got out of sync as a result. Fixed.\n\n> Also the commit title deviates from the established \"<area>: <what\n> is done>\" format.\n>\n>     Subject: [PATCH] refs: add 'preparing\" phase to the transaction hook\n>\n> or something?\n>\n\nThank you. Fixed.\n\n> Other than that, both the cover letter and the proposed log message\n> very well explain the motivation behind the new feature.  I wish\n> everybody wrote their log messages as clearly as this one.\n>\n\nThank you. Much of the credit goes to Patrick (ps@pks.im), who helped\nshape the log message.\n\n> > The \"reference-transaction\" hook is invoked multiple times during a ref\n> > transaction. Each invocation corresponds to a different phase:\n> >\n> > - The \"prepared\" phase indicates that references have been locked.\n> > - The \"commit\" phase indicates that all updates have been written to disk.\n> > - The \"abort\" phase indicates that the transaction has been aborted and that\n> >   all changes have been rolled back.\n>\n> \"commit\" -> \"committed\" and \"abort\" -> \"aborted\", if the existing\n> documentation is to be trusted.\n>\n\nThank you. Fixed.\n\n> > This hook can be used to learn about the updates that Git wants to perform.\n> > For example, forges use it to coordinate reference updates across multiple\n> > nodes.\n> >\n> > However, the phases are insufficient for some specific use cases. The earliest\n> > observable phase in the \"reference-transaction\" hook is \"prepared\", at which\n> > point Git has already taken exclusive locks on every affected reference. This\n> > makes it suitable for last-chance validation, but not for serialization. So by\n> > the time a hook sees the \"prepared\" phase, it has no way to defer locking, and\n> > thus it cannot rearrange multiple concurrent ref transactions relative to one\n> > another.\n>\n> I cannot quite picture how \"rearrangement\" would happen, though.\n>\n> Would the hook notice \"ah there is a preparing hook invocation\n> incoming\", stall the caller by not immediately returning and instead\n> wait for a different Git process to invoke the same ref-transaction\n> hook \"preparing\" invocation, and somehow decide to let the latter go\n> first before releasing the former?\n>\n\nThank you for asking, happy to clarify. The intended use case is\nserializing concurrent write calls in Gitaly/Praefect.\nWhen the hook fires in the \"preparing\" state, the hook handler\ncontacts Praefect asking \"can I proceed with these ref updates?\"\nPraefect coordinates across multiple concurrent hook callbacks and\nuses this window to determine ordering:\nif all callers vote for the same write, they are allowed to proceed;\nother write requests are held or aborted until the current one\ncompletes.\n\n> > Introduce a new \"preparing\" phase that runs before the \"prepared\" phase, that\n> > is before Git acquires any reference lock on disk. This gives callers a\n> > well-defined window to perform validation, enable higher-level ordering of\n> > concurrent transactions, or reject the transaction entirely, all without\n> > interfering with the locking state.\n> >\n> > This change is strictly speaking not backwards compatible. Existing hook\n> > scripts that do not know to handle unknown phases handle the \"preparing\" state\n>\n> \"know to handle unknown phrases handle\"?\n>\n\nFixed.\n\n> > string will encounter an unknown phase, and that might cause them to return an\n> > error now. But the hook is considered to expose internal implementation details\n> > of how Git works, and as such we have been a bit more lenient with changing its\n> > exact semantics, like for example in a8ae923f85 (refs: support symrefs in\n> > 'reference-transaction' hook, 2024-05-07).\n> >\n> > An alternative would be to introduce a \"reference-transaction-v2\" hook that\n> > knows about the new phase. This feels like a rather heavy-weight option though,\n> > and was thus discarded.\n>\n> And documenting the design alternatives and decision like these two\n> paragraphs is very much appreciated.\n>\n> The insertion of a new hook invocation itself is at a very much\n> expected place in the code path.  Well written.\n>\n> Will queue.  Thanks.\n\nThank you.\n\n- Eric\n"},{"id":"539060","messageId":"CAN2LT1A=yMpMSDZoHGEFL7T8fw5OC5vbgL3DJ7U8xP6tOfVudQ@mail.gmail.com","threadId":"65240","inReplyTo":"xmqq7brf1g3e.fsf@gitster.g","subject":"Re: [PATCH 1/1] Add preparing state to reference-transaction hook","fromName":"Peijian Ju","fromEmail":"eric.peijian@gmail.com","sentAt":"2026-03-16T03:09:33Z","receivedAt":"2026-03-16T03:09:45Z","isPatch":true,"sender":{"key":"eric.peijian@gmail.com","avatar":"https://avatars.githubusercontent.com/u/7400645?v=4"},"body":"On Fri, Mar 13, 2026 at 7:10 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Justin Tobler <jltobler@gmail.com> writes:\n>\n> > On 26/03/13 03:35PM, eric.peijian@gmail.com wrote:\n> >> diff --git a/refs.c b/refs.c\n> >> index 6fb8f9d10c..f1439476d3 100644\n> >> --- a/refs.c\n> >> +++ b/refs.c\n> >> @@ -2655,6 +2655,13 @@ int ref_transaction_prepare(struct ref_transaction *transaction,\n> >>      if (ref_update_reject_duplicates(&transaction->refnames, err))\n> >>              return REF_TRANSACTION_ERROR_GENERIC;\n> >>\n> >> +    /* Preparing checks before locking references */\n> >> +    ret = run_transaction_hook(transaction, \"preparing\");\n> >> +    if (ret) {\n> >> +            ref_transaction_abort(transaction, err);\n> >> +            die(_(\"ref updates aborted by %s hook\"), \"preparing\");\n> >\n> > Should \"preparing\" be marked for translation here?\n>\n> It literally is one of the possible tokens reference-transaction\n> hook is given as its argument, so no, I do not think \"preparing\"\n> should be translated.\n>\n> But the hook that interrupted the ref update is not \"preparing\"\n> hook.  It is the \"reference-transaction\" hook.  So the message\n> probably should say something like\n>\n>     the reference-transaction hook rejected ref updates at its\n>     preparing phase\n>\n> or something.\n>\n\nThanks for the clarification. Fixed, the message now reads: \"ref\nupdates aborted by the reference-transaction hook at its preparing\nphase\" (and likewise for \"prepared\").\n\n\n> >> +    }\n> >> +\n> >>      ret = refs->be->transaction_prepare(refs, transaction, err);\n> >>      if (ret)\n> >>              return ret;\n> >> @@ -2662,7 +2669,7 @@ int ref_transaction_prepare(struct ref_transaction *transaction,\n> >>      ret = run_transaction_hook(transaction, \"prepared\");\n> >>      if (ret) {\n> >>              ref_transaction_abort(transaction, err);\n> >> -            die(_(\"ref updates aborted by hook\"));\n> >> +            die(_(\"ref updates aborted by %s hook\"), \"prepared\");\n> >\n> > Same question here for \"prepared\"?\n>\n> Ditto.\n\nDitto.\n\nThank you.\n- Eric\n"},{"id":"539063","messageId":"20260316045102.70551-1-eric.peijian@gmail.com","threadId":"65240","inReplyTo":"20260313193537.62827-1-eric.peijian@gmail.com","subject":"[PATCH v2 0/1] refs: add 'preparing' phase to the reference-transaction hook","fromName":"Eric Ju","fromEmail":"eric.peijian@gmail.com","sentAt":"2026-03-16T04:51:01Z","receivedAt":"2026-03-16T04:51:18Z","isPatch":true,"sender":{"key":"eric.peijian@gmail.com","avatar":"https://avatars.githubusercontent.com/u/7400645?v=4"},"body":"\nThe \"reference-transaction\" hook currently exposes three phases to callers:\n\"prepared\", \"committed\", and \"aborted\". The earliest of these, \"prepared\",\nfires after Git has already acquired exclusive locks on every affected\nreference. This is well-suited for last-chance validation, but it arrives\ntoo late for any use case that requires coordination before locking, such\nas serializing concurrent transactions across distributed storage nodes.\n\nThis series introduces a new \"preparing\" phase that fires before\nrefs->be->transaction_prepare() is called, that is, before Git takes any\nreference lock on disk. Hook scripts that handle this phase receive the full\nlist of proposed updates and may reject the transaction by returning a\nnon-zero exit status, causing Git to abort cleanly before any locks are\nacquired.\n\nThe motivating use case is Gitaly/Praefect, GitLab's distributed Git storage\nlayer. Praefect must serialize concurrent writes that target the same\nreferences across replicas. With only the \"prepared\" phase available, by the\ntime Praefect can observe a transaction the locks are already held, making\nreordering impossible. The \"preparing\" phase provides the necessary\npre-lock window.\n\nCompatibility note: this change is not strictly backwards compatible. Hook\nscripts that do not expect unknown phase strings may return an error when\nthey encounter \"preparing\". We consider this acceptable for the same reasons\ncited when symref support was added to the hook in a8ae923f85 (refs: support\nsymrefs in 'reference-transaction' hook, 2024-05-07): the hook is documented\nas exposing internal implementation details, and its semantics have been\nadjusted before. An alternative of introducing a \"reference-transaction-v2\"\nhook was considered but rejected as unnecessarily heavyweight.\n\n---\nChanges since v1:\n\n- Fix commit title to follow \"area: description\" convention\n  (\"refs: add 'preparing' phase to reference-transaction hook\")\n- Correct phase names in documentation to past tense\n  (\"committed\", \"aborted\")\n- Fix the sentence about backwards compatibility with unknown phases\n- Update die() messages to identify the hook by full name and phase\n  (\"ref updates rejected by the reference-transaction hook at its\n  preparing/prepared phase\")\n- Consolidate author identity to eric.peijian@gmail.com\n- Add clarification in reply to the question about how to use the preparing\n  phase for write serialization\n\nEric Ju (1):\n  refs: add 'preparing' phase to the reference-transaction hook\n\n Documentation/githooks.adoc      | 19 ++++++++++++-------\n refs.c                           |  9 ++++++++-\n t/t1416-ref-transaction-hooks.sh | 30 ++++++++++++++++++++++++++----\n t/t5510-fetch.sh                 |  7 ++++++-\n 4 files changed, 52 insertions(+), 13 deletions(-)\n\nRange-diff against v1:\n1:  5f9f13a84d ! 1:  fb74f21d98 Add preparing state to reference-transaction hook\n    @@\n      ## Metadata ##\n    -Author: Eric Ju <eju@gitlab.com>\n    +Author: Eric Ju <eric.peijian@gmail.com>\n     \n      ## Commit message ##\n    -    Add preparing state to reference-transaction hook\n    +    refs: add 'preparing' phase to the reference-transaction hook\n     \n         The \"reference-transaction\" hook is invoked multiple times during a ref\n         transaction. Each invocation corresponds to a different phase:\n     \n         - The \"prepared\" phase indicates that references have been locked.\n    -    - The \"commit\" phase indicates that all updates have been written to disk.\n    -    - The \"abort\" phase indicates that the transaction has been aborted and that\n    +    - The \"committed\" phase indicates that all updates have been written to disk.\n    +    - The \"aborted\" phase indicates that the transaction has been aborted and that\n           all changes have been rolled back.\n     \n         This hook can be used to learn about the updates that Git wants to perform.\n    @@ Commit message\n         interfering with the locking state.\n     \n         This change is strictly speaking not backwards compatible. Existing hook\n    -    scripts that do not know to handle unknown phases handle the \"preparing\" state\n    -    string will encounter an unknown phase, and that might cause them to return an\n    -    error now. But the hook is considered to expose internal implementation details\n    +    scripts that do not know how to handle unknown phases may treat\n    +    'preparing' as an error and return non-zero.\n    +    But the hook is considered to expose internal implementation details\n         of how Git works, and as such we have been a bit more lenient with changing its\n         exact semantics, like for example in a8ae923f85 (refs: support symrefs in\n         'reference-transaction' hook, 2024-05-07).\n    @@ Commit message\n     \n         Helped-by: Patrick Steinhardt <ps@pks.im>\n         Helped-by: Justin Tobler <jltobler@gmail.com>\n    +    Helped-by: Karthik Nayak <karthik.188@gmail.com>\n         Signed-off-by: Eric Ju <eric.peijian@gmail.com>\n     \n      ## Documentation/githooks.adoc ##\n    @@ refs.c: int ref_transaction_prepare(struct ref_transaction *transaction,\n     +\tret = run_transaction_hook(transaction, \"preparing\");\n     +\tif (ret) {\n     +\t\tref_transaction_abort(transaction, err);\n    -+\t\tdie(_(\"ref updates aborted by %s hook\"), \"preparing\");\n    ++\t\tdie(_(\"ref updates aborted by the reference-transaction hook at its %s state\"), \"preparing\");\n     +\t}\n     +\n      \tret = refs->be->transaction_prepare(refs, transaction, err);\n    @@ refs.c: int ref_transaction_prepare(struct ref_transaction *transaction,\n      \tif (ret) {\n      \t\tref_transaction_abort(transaction, err);\n     -\t\tdie(_(\"ref updates aborted by hook\"));\n    -+\t\tdie(_(\"ref updates aborted by %s hook\"), \"prepared\");\n    ++\t\tdie(_(\"ref updates aborted by the reference-transaction hook at its %s state\"), \"prepared\");\n      \t}\n      \n      \treturn 0;\n    @@ t/t1416-ref-transaction-hooks.sh: test_expect_success 'hook allows updating ref\n     +\t\tfi\n     +\tEOF\n     +\ttest_must_fail git update-ref HEAD POST 2>err &&\n    -+\ttest_grep \"ref updates aborted by preparing hook\" err\n    ++\ttest_grep \"ref updates aborted by the reference-transaction hook at its preparing state\" err\n     +'\n     +\n      test_expect_success 'hook aborts updating ref in prepared state' '\n    @@ t/t1416-ref-transaction-hooks.sh: test_expect_success 'hook aborts updating ref\n      \tEOF\n      \ttest_must_fail git update-ref HEAD POST 2>err &&\n     -\ttest_grep \"ref updates aborted by hook\" err\n    -+\ttest_grep \"ref updates aborted by prepared hook\" err\n    ++\ttest_grep \"ref updates aborted by the reference-transaction hook at its prepared state\" err\n      '\n      \n      test_expect_success 'hook gets all queued updates in prepared state' '\n-- \n2.51.0\n\n"},{"id":"539064","messageId":"20260316045102.70551-2-eric.peijian@gmail.com","threadId":"65240","inReplyTo":"20260316045102.70551-1-eric.peijian@gmail.com","subject":"[PATCH v2 1/1] refs: add 'preparing' phase to the reference-transaction hook","fromName":"Eric Ju","fromEmail":"eric.peijian@gmail.com","sentAt":"2026-03-16T04:51:02Z","receivedAt":"2026-03-16T04:51:19Z","isPatch":true,"sender":{"key":"eric.peijian@gmail.com","avatar":"https://avatars.githubusercontent.com/u/7400645?v=4"},"body":"The \"reference-transaction\" hook is invoked multiple times during a ref\ntransaction. Each invocation corresponds to a different phase:\n\n- The \"prepared\" phase indicates that references have been locked.\n- The \"committed\" phase indicates that all updates have been written to disk.\n- The \"aborted\" phase indicates that the transaction has been aborted and that\n  all changes have been rolled back.\n\nThis hook can be used to learn about the updates that Git wants to perform.\nFor example, forges use it to coordinate reference updates across multiple\nnodes.\n\nHowever, the phases are insufficient for some specific use cases. The earliest\nobservable phase in the \"reference-transaction\" hook is \"prepared\", at which\npoint Git has already taken exclusive locks on every affected reference. This\nmakes it suitable for last-chance validation, but not for serialization. So by\nthe time a hook sees the \"prepared\" phase, it has no way to defer locking, and\nthus it cannot rearrange multiple concurrent ref transactions relative to one\nanother.\n\nIntroduce a new \"preparing\" phase that runs before the \"prepared\" phase, that\nis before Git acquires any reference lock on disk. This gives callers a\nwell-defined window to perform validation, enable higher-level ordering of\nconcurrent transactions, or reject the transaction entirely, all without\ninterfering with the locking state.\n\nThis change is strictly speaking not backwards compatible. Existing hook\nscripts that do not know how to handle unknown phases may treat\n'preparing' as an error and return non-zero.\nBut the hook is considered to expose internal implementation details\nof how Git works, and as such we have been a bit more lenient with changing its\nexact semantics, like for example in a8ae923f85 (refs: support symrefs in\n'reference-transaction' hook, 2024-05-07).\n\nAn alternative would be to introduce a \"reference-transaction-v2\" hook that\nknows about the new phase. This feels like a rather heavy-weight option though,\nand was thus discarded.\n\nHelped-by: Patrick Steinhardt <ps@pks.im>\nHelped-by: Justin Tobler <jltobler@gmail.com>\nHelped-by: Karthik Nayak <karthik.188@gmail.com>\nSigned-off-by: Eric Ju <eric.peijian@gmail.com>\n---\n Documentation/githooks.adoc      | 19 ++++++++++++-------\n refs.c                           |  9 ++++++++-\n t/t1416-ref-transaction-hooks.sh | 30 ++++++++++++++++++++++++++----\n t/t5510-fetch.sh                 |  7 ++++++-\n 4 files changed, 52 insertions(+), 13 deletions(-)\n\ndiff --git a/Documentation/githooks.adoc b/Documentation/githooks.adoc\nindex 056553788d..ed045940d1 100644\n--- a/Documentation/githooks.adoc\n+++ b/Documentation/githooks.adoc\n@@ -484,13 +484,16 @@ reference-transaction\n ~~~~~~~~~~~~~~~~~~~~~\n \n This hook is invoked by any Git command that performs reference\n-updates. It executes whenever a reference transaction is prepared,\n-committed or aborted and may thus get called multiple times. The hook\n-also supports symbolic reference updates.\n+updates. It executes whenever a reference transaction is preparing,\n+prepared, committed or aborted and may thus get called multiple times.\n+The hook also supports symbolic reference updates.\n \n The hook takes exactly one argument, which is the current state the\n given reference transaction is in:\n \n+    - \"preparing\": All reference updates have been queued to the\n+      transaction but references are not yet locked on disk.\n+\n     - \"prepared\": All reference updates have been queued to the\n       transaction and references were locked on disk.\n \n@@ -511,16 +514,18 @@ ref and `<ref-name>` is the full name of the ref. When force updating\n the reference regardless of its current value or when the reference is\n to be created anew, `<old-value>` is the all-zeroes object name. To\n distinguish these cases, you can inspect the current value of\n-`<ref-name>` via `git rev-parse`.\n+`<ref-name>` via `git rev-parse`. During the \"preparing\" state, symbolic\n+references are not resolved: `<ref-name>` will reflect the symbolic reference\n+itself rather than the object it points to.\n \n For symbolic reference updates the `<old_value>` and `<new-value>`\n fields could denote references instead of objects. A reference will be\n denoted with a 'ref:' prefix, like `ref:<ref-target>`.\n \n The exit status of the hook is ignored for any state except for the\n-\"prepared\" state. In the \"prepared\" state, a non-zero exit status will\n-cause the transaction to be aborted. The hook will not be called with\n-\"aborted\" state in that case.\n+\"preparing\" and \"prepared\" states. In these states, a non-zero exit\n+status will cause the transaction to be aborted. The hook will not be\n+called with \"aborted\" state in that case.\n \n push-to-checkout\n ~~~~~~~~~~~~~~~~\ndiff --git a/refs.c b/refs.c\nindex 6fb8f9d10c..7da37bbb71 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -2655,6 +2655,13 @@ int ref_transaction_prepare(struct ref_transaction *transaction,\n \tif (ref_update_reject_duplicates(&transaction->refnames, err))\n \t\treturn REF_TRANSACTION_ERROR_GENERIC;\n \n+\t/* Preparing checks before locking references */\n+\tret = run_transaction_hook(transaction, \"preparing\");\n+\tif (ret) {\n+\t\tref_transaction_abort(transaction, err);\n+\t\tdie(_(\"ref updates aborted by the reference-transaction hook at its %s state\"), \"preparing\");\n+\t}\n+\n \tret = refs->be->transaction_prepare(refs, transaction, err);\n \tif (ret)\n \t\treturn ret;\n@@ -2662,7 +2669,7 @@ int ref_transaction_prepare(struct ref_transaction *transaction,\n \tret = run_transaction_hook(transaction, \"prepared\");\n \tif (ret) {\n \t\tref_transaction_abort(transaction, err);\n-\t\tdie(_(\"ref updates aborted by hook\"));\n+\t\tdie(_(\"ref updates aborted by the reference-transaction hook at its %s state\"), \"prepared\");\n \t}\n \n \treturn 0;\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex d91dd3a3b5..c3b1a3c735 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -20,6 +20,7 @@ test_expect_success 'hook allows updating ref if successful' '\n \t\techo \"$*\" >>actual\n \tEOF\n \tcat >expect <<-EOF &&\n+\t\tpreparing\n \t\tprepared\n \t\tcommitted\n \tEOF\n@@ -27,6 +28,18 @@ test_expect_success 'hook allows updating ref if successful' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'hook aborts updating ref in preparing state' '\n+\tgit reset --hard PRE &&\n+\ttest_hook reference-transaction <<-\\EOF &&\n+\t\tif test \"$1\" = preparing\n+\t\tthen\n+\t\t\texit 1\n+\t\tfi\n+\tEOF\n+\ttest_must_fail git update-ref HEAD POST 2>err &&\n+\ttest_grep \"ref updates aborted by the reference-transaction hook at its preparing state\" err\n+'\n+\n test_expect_success 'hook aborts updating ref in prepared state' '\n \tgit reset --hard PRE &&\n \ttest_hook reference-transaction <<-\\EOF &&\n@@ -36,7 +49,7 @@ test_expect_success 'hook aborts updating ref in prepared state' '\n \t\tfi\n \tEOF\n \ttest_must_fail git update-ref HEAD POST 2>err &&\n-\ttest_grep \"ref updates aborted by hook\" err\n+\ttest_grep \"ref updates aborted by the reference-transaction hook at its prepared state\" err\n '\n \n test_expect_success 'hook gets all queued updates in prepared state' '\n@@ -121,6 +134,7 @@ test_expect_success 'interleaving hook calls succeed' '\n \tcat >expect <<-EOF &&\n \t\thooks/update refs/tags/PRE $ZERO_OID $PRE_OID\n \t\thooks/update refs/tags/POST $ZERO_OID $POST_OID\n+\t\thooks/reference-transaction preparing\n \t\thooks/reference-transaction prepared\n \t\thooks/reference-transaction committed\n \tEOF\n@@ -143,6 +157,8 @@ test_expect_success 'hook captures git-symbolic-ref updates' '\n \tgit symbolic-ref refs/heads/symref refs/heads/main &&\n \n \tcat >expect <<-EOF &&\n+\tpreparing\n+\t$ZERO_OID ref:refs/heads/main refs/heads/symref\n \tprepared\n \t$ZERO_OID ref:refs/heads/main refs/heads/symref\n \tcommitted\n@@ -171,14 +187,20 @@ test_expect_success 'hook gets all queued symref updates' '\n \t# In the files backend, \"delete\" also triggers an additional transaction\n \t# update on the packed-refs backend, which constitutes additional reflog\n \t# entries.\n+\tcat >expect <<-EOF &&\n+\tpreparing\n+\tref:refs/heads/main $ZERO_OID refs/heads/symref\n+\tref:refs/heads/main $ZERO_OID refs/heads/symrefd\n+\t$ZERO_OID ref:refs/heads/main refs/heads/symrefc\n+\tref:refs/heads/main ref:refs/heads/branch refs/heads/symrefu\n+\tEOF\n+\n \tif test_have_prereq REFFILES\n \tthen\n-\t\tcat >expect <<-EOF\n+\t\tcat >>expect <<-EOF\n \t\taborted\n \t\t$ZERO_OID $ZERO_OID refs/heads/symrefd\n \t\tEOF\n-\telse\n-\t\t>expect\n \tfi &&\n \n \tcat >>expect <<-EOF &&\ndiff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh\nindex 5dcb4b51a4..6fe21e2b3a 100755\n--- a/t/t5510-fetch.sh\n+++ b/t/t5510-fetch.sh\n@@ -469,12 +469,17 @@ test_expect_success 'fetch --atomic executes a single reference transaction only\n \thead_oid=$(git rev-parse HEAD) &&\n \n \tcat >expected <<-EOF &&\n+\t\tpreparing\n+\t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-1\n+\t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-2\n \t\tprepared\n \t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-1\n \t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-2\n \t\tcommitted\n \t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-1\n \t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-2\n+\t\tpreparing\n+\t\t$ZERO_OID ref:refs/remotes/origin/main refs/remotes/origin/HEAD\n \tEOF\n \n \trm -f atomic/actual &&\n@@ -497,7 +502,7 @@ test_expect_success 'fetch --atomic aborts all reference updates if hook aborts'\n \thead_oid=$(git rev-parse HEAD) &&\n \n \tcat >expected <<-EOF &&\n-\t\tprepared\n+\t\tpreparing\n \t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-abort-1\n \t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-abort-2\n \t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-abort-3\n-- \n2.51.0\n\n"},{"id":"539068","messageId":"aberRbSCbMtZrqxk@pks.im","threadId":"65240","inReplyTo":"20260316045102.70551-1-eric.peijian@gmail.com","subject":"Re: [PATCH v2 0/1] refs: add 'preparing' phase to the reference-transaction hook","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-16T07:03:33Z","receivedAt":"2026-03-16T07:03:39Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Mar 16, 2026 at 12:51:01AM -0400, Eric Ju wrote:\n> Changes since v1:\n> \n> - Fix commit title to follow \"area: description\" convention\n>   (\"refs: add 'preparing' phase to reference-transaction hook\")\n> - Correct phase names in documentation to past tense\n>   (\"committed\", \"aborted\")\n> - Fix the sentence about backwards compatibility with unknown phases\n> - Update die() messages to identify the hook by full name and phase\n>   (\"ref updates rejected by the reference-transaction hook at its\n>   preparing/prepared phase\")\n> - Consolidate author identity to eric.peijian@gmail.com\n> - Add clarification in reply to the question about how to use the preparing\n>   phase for write serialization\n\nAll of these changes look good to me, thanks. This patch already looks\ngood to me, but I'm of course biased as I have been helping out behind\nthe scenes before the first version of this patch landed on the mailing\nlist.\n\n> Range-diff against v1:\n> 1:  5f9f13a84d ! 1:  fb74f21d98 Add preparing state to reference-transaction hook\n>     @@ Commit message\n>          interfering with the locking state.\n>      \n>          This change is strictly speaking not backwards compatible. Existing hook\n>     -    scripts that do not know to handle unknown phases handle the \"preparing\" state\n>     -    string will encounter an unknown phase, and that might cause them to return an\n>     -    error now. But the hook is considered to expose internal implementation details\n>     +    scripts that do not know how to handle unknown phases may treat\n>     +    'preparing' as an error and return non-zero.\n>     +    But the hook is considered to expose internal implementation details\n>          of how Git works, and as such we have been a bit more lenient with changing its\n>          exact semantics, like for example in a8ae923f85 (refs: support symrefs in\n>          'reference-transaction' hook, 2024-05-07).\n\nOne micro-nit: this paragraph could use some reflowing. But I don't\nthink it's worth a reroll.\n\nThanks!\n\nPatrick\n"},{"id":"539129","messageId":"xmqqv7evpwrr.fsf@gitster.g","threadId":"65240","inReplyTo":"20260316045102.70551-2-eric.peijian@gmail.com","subject":"Re: [PATCH v2 1/1] refs: add 'preparing' phase to the reference-transaction hook","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-16T16:24:56Z","receivedAt":"2026-03-16T16:24:58Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Eric Ju <eric.peijian@gmail.com> writes:\n\n> +\t/* Preparing checks before locking references */\n> +\tret = run_transaction_hook(transaction, \"preparing\");\n> +\tif (ret) {\n> +\t\tref_transaction_abort(transaction, err);\n> +\t\tdie(_(\"ref updates aborted by the reference-transaction hook at its %s state\"), \"preparing\");\n> +\t}\n\nOn end-user's terminal, the above should look like\n\nfatal: ref updates aborted by the reference-transaction hook at its parparing state\n\nconsuming more than 80 columns and having the varying part of the\nmessage at the very end.  Can we shorten this and highlight the more\nimportant bits?  Here is my attempt\n\n\t\tdie(_(\"in '%s' phase, update aborted by the reference-transaction hook\"),\n\t\t\t\"preparing\");\n\nEnclosing the phase name in 'quotes' and moving it near the\nbeginning are both my attempt to make it stand out more.\n\nAnother thing you may want to consider is to extract the message to\na separate constant, i.e.,\n\n\tconst char *abort_by_ref_transaction_hook[] =\n\tN_(\"in '%s' phase, update aborted by the reference-transaction hook\");\n\nand reuse at two places, perhaps?\n\n\t\tdie(_(abort_by_ref_transaction_hook), \"preparing\");\n\n"},{"id":"539174","messageId":"CAN2LT1DJcSEKuQOk2PHgUwORKwR4Vqo5=f2_FtNXHMH0BxvLZQ@mail.gmail.com","threadId":"65240","inReplyTo":"aberRbSCbMtZrqxk@pks.im","subject":"Re: [PATCH v2 0/1] refs: add 'preparing' phase to the reference-transaction hook","fromName":"Peijian Ju","fromEmail":"eric.peijian@gmail.com","sentAt":"2026-03-16T23:08:11Z","receivedAt":"2026-03-16T23:08:23Z","isPatch":true,"sender":{"key":"eric.peijian@gmail.com","avatar":"https://avatars.githubusercontent.com/u/7400645?v=4"},"body":"On Mon, Mar 16, 2026 at 3:03 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Mon, Mar 16, 2026 at 12:51:01AM -0400, Eric Ju wrote:\n> > Changes since v1:\n> >\n> > - Fix commit title to follow \"area: description\" convention\n> >   (\"refs: add 'preparing' phase to reference-transaction hook\")\n> > - Correct phase names in documentation to past tense\n> >   (\"committed\", \"aborted\")\n> > - Fix the sentence about backwards compatibility with unknown phases\n> > - Update die() messages to identify the hook by full name and phase\n> >   (\"ref updates rejected by the reference-transaction hook at its\n> >   preparing/prepared phase\")\n> > - Consolidate author identity to eric.peijian@gmail.com\n> > - Add clarification in reply to the question about how to use the preparing\n> >   phase for write serialization\n>\n> All of these changes look good to me, thanks. This patch already looks\n> good to me, but I'm of course biased as I have been helping out behind\n> the scenes before the first version of this patch landed on the mailing\n> list.\n>\n> > Range-diff against v1:\n> > 1:  5f9f13a84d ! 1:  fb74f21d98 Add preparing state to reference-transaction hook\n> >     @@ Commit message\n> >          interfering with the locking state.\n> >\n> >          This change is strictly speaking not backwards compatible. Existing hook\n> >     -    scripts that do not know to handle unknown phases handle the \"preparing\" state\n> >     -    string will encounter an unknown phase, and that might cause them to return an\n> >     -    error now. But the hook is considered to expose internal implementation details\n> >     +    scripts that do not know how to handle unknown phases may treat\n> >     +    'preparing' as an error and return non-zero.\n> >     +    But the hook is considered to expose internal implementation details\n> >          of how Git works, and as such we have been a bit more lenient with changing its\n> >          exact semantics, like for example in a8ae923f85 (refs: support symrefs in\n> >          'reference-transaction' hook, 2024-05-07).\n>\n> One micro-nit: this paragraph could use some reflowing. But I don't\n> think it's worth a reroll.\n>\n> Thanks!\n>\n> Patrick\n\nThank you. I will reflow the paragraph in v3, which I am already\nplanning to send for the error message and string constant changes.\n\n- Eric\n"},{"id":"539175","messageId":"CAN2LT1AeEYbCFvhUSnWPvCUtahVQP_cG8edVhURHg2N3OgMuwQ@mail.gmail.com","threadId":"65240","inReplyTo":"xmqqv7evpwrr.fsf@gitster.g","subject":"Re: [PATCH v2 1/1] refs: add 'preparing' phase to the reference-transaction hook","fromName":"Peijian Ju","fromEmail":"eric.peijian@gmail.com","sentAt":"2026-03-16T23:08:12Z","receivedAt":"2026-03-16T23:08:24Z","isPatch":true,"sender":{"key":"eric.peijian@gmail.com","avatar":"https://avatars.githubusercontent.com/u/7400645?v=4"},"body":"On Mon, Mar 16, 2026 at 12:24 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Eric Ju <eric.peijian@gmail.com> writes:\n>\n> > +     /* Preparing checks before locking references */\n> > +     ret = run_transaction_hook(transaction, \"preparing\");\n> > +     if (ret) {\n> > +             ref_transaction_abort(transaction, err);\n> > +             die(_(\"ref updates aborted by the reference-transaction hook at its %s state\"), \"preparing\");\n> > +     }\n>\n> On end-user's terminal, the above should look like\n>\n> fatal: ref updates aborted by the reference-transaction hook at its parparing state\n>\n> consuming more than 80 columns and having the varying part of the\n> message at the very end.  Can we shorten this and highlight the more\n> important bits?  Here is my attempt\n>\n>                 die(_(\"in '%s' phase, update aborted by the reference-transaction hook\"),\n>                         \"preparing\");\n>\n> Enclosing the phase name in 'quotes' and moving it near the\n> beginning are both my attempt to make it stand out more.\n>\n> Another thing you may want to consider is to extract the message to\n> a separate constant, i.e.,\n>\n>         const char *abort_by_ref_transaction_hook[] =\n>         N_(\"in '%s' phase, update aborted by the reference-transaction hook\");\n>\n> and reuse at two places, perhaps?\n>\n>                 die(_(abort_by_ref_transaction_hook), \"preparing\");\n>\n\nThank you. Fixed in V3.\n\n- Eric\n"},{"id":"539198","messageId":"20260317023624.43070-1-eric.peijian@gmail.com","threadId":"65240","inReplyTo":"20260313193537.62827-1-eric.peijian@gmail.com","subject":"[PATCH v3 0/1] refs: add 'preparing' phase to the reference-transaction hook","fromName":"Eric Ju","fromEmail":"eric.peijian@gmail.com","sentAt":"2026-03-17T02:36:23Z","receivedAt":"2026-03-17T02:36:29Z","isPatch":true,"sender":{"key":"eric.peijian@gmail.com","avatar":"https://avatars.githubusercontent.com/u/7400645?v=4"},"body":"The \"reference-transaction\" hook currently exposes three phases to callers:\n\"prepared\", \"committed\", and \"aborted\". The earliest of these, \"prepared\",\nfires after Git has already acquired exclusive locks on every affected\nreference. This is well-suited for last-chance validation, but it arrives\ntoo late for any use case that requires coordination before locking, such\nas serializing concurrent transactions across distributed storage nodes.\n\nThis series introduces a new \"preparing\" phase that fires before\nrefs->be->transaction_prepare() is called, that is, before Git takes any\nreference lock on disk. Hook scripts that handle this phase receive the full\nlist of proposed updates and may reject the transaction by returning a\nnon-zero exit status, causing Git to abort cleanly before any locks are\nacquired.\n\nThe motivating use case is Gitaly/Praefect, GitLab's distributed Git storage\nlayer. Praefect must serialize concurrent writes that target the same\nreferences across replicas. With only the \"prepared\" phase available, by the\ntime Praefect can observe a transaction the locks are already held, making\nreordering impossible. The \"preparing\" phase provides the necessary\npre-lock window.\n\nCompatibility note: this change is not strictly backwards compatible. Hook\nscripts that do not expect unknown phase strings may return an error when\nthey encounter \"preparing\". We consider this acceptable for the same reasons\ncited when symref support was added to the hook in a8ae923f85 (refs: support\nsymrefs in 'reference-transaction' hook, 2024-05-07): the hook is documented\nas exposing internal implementation details, and its semantics have been\nadjusted before. An alternative of introducing a \"reference-transaction-v2\"\nhook was considered but rejected as unnecessarily heavyweight.\n\n---\nChanges since v2:\n\n- Shorten and reorder die() message to highlight the phase name early\n- Extract the error message into a file-scope static constant to avoid\n  duplication across the \"preparing\" and \"prepared\" call sites\n- Reflow the backwards compatibility paragraph in the commit message\n\nEric Ju (1):\n  refs: add 'preparing' phase to the reference-transaction hook\n\n Documentation/githooks.adoc      | 19 ++++++++++++-------\n refs.c                           | 12 +++++++++++-\n t/t1416-ref-transaction-hooks.sh | 30 ++++++++++++++++++++++++++----\n t/t5510-fetch.sh                 |  7 ++++++-\n 4 files changed, 55 insertions(+), 13 deletions(-)\n\nRange-diff against v2:\n1:  4fff10e694 ! 1:  39f7a2bc4b refs: add 'preparing' phase to the reference-transaction hook\n    @@ Commit message\n         interfering with the locking state.\n     \n         This change is strictly speaking not backwards compatible. Existing hook\n    -    scripts that do not know how to handle unknown phases may treat\n    -    'preparing' as an error and return non-zero.\n    -    But the hook is considered to expose internal implementation details\n    -    of how Git works, and as such we have been a bit more lenient with changing its\n    -    exact semantics, like for example in a8ae923f85 (refs: support symrefs in\n    -    'reference-transaction' hook, 2024-05-07).\n    +    scripts that do not know how to handle unknown phases may treat 'preparing'\n    +    as an error and return non-zero. But the hook is considered to expose\n    +    internal implementation details of how Git works, and as such we have\n    +    been a bit more lenient with changing its exact semantics, like for example\n    +    in a8ae923f85 (refs: support symrefs in 'reference-transaction' hook, 2024-05-07).\n     \n         An alternative would be to introduce a \"reference-transaction-v2\" hook that\n         knows about the new phase. This feels like a rather heavy-weight option though,\n    @@ Commit message\n     \n         Helped-by: Patrick Steinhardt <ps@pks.im>\n         Helped-by: Justin Tobler <jltobler@gmail.com>\n    +    Helped-by: Karthik Nayak <karthik.188@gmail.com>\n         Signed-off-by: Eric Ju <eric.peijian@gmail.com>\n     \n      ## Documentation/githooks.adoc ##\n    @@ Documentation/githooks.adoc: ref and `<ref-name>` is the full name of the ref. W\n      ~~~~~~~~~~~~~~~~\n     \n      ## refs.c ##\n    +@@ refs.c: const char *ref_storage_format_to_name(enum ref_storage_format ref_storage_forma\n    + \treturn be->name;\n    + }\n    + \n    ++static const char *abort_by_ref_transaction_hook =\n    ++\tN_(\"in '%s' phase, update aborted by the reference-transaction hook\");\n    ++\n    + /*\n    +  * How to handle various characters in refnames:\n    +  * 0: An acceptable character for refs\n     @@ refs.c: int ref_transaction_prepare(struct ref_transaction *transaction,\n      \tif (ref_update_reject_duplicates(&transaction->refnames, err))\n      \t\treturn REF_TRANSACTION_ERROR_GENERIC;\n    @@ refs.c: int ref_transaction_prepare(struct ref_transaction *transaction,\n     +\tret = run_transaction_hook(transaction, \"preparing\");\n     +\tif (ret) {\n     +\t\tref_transaction_abort(transaction, err);\n    -+\t\tdie(_(\"ref updates aborted by the reference-transaction hook at its %s state\"), \"preparing\");\n    ++\t\tdie(_(abort_by_ref_transaction_hook), \"preparing\");\n     +\t}\n     +\n      \tret = refs->be->transaction_prepare(refs, transaction, err);\n    @@ refs.c: int ref_transaction_prepare(struct ref_transaction *transaction,\n      \tif (ret) {\n      \t\tref_transaction_abort(transaction, err);\n     -\t\tdie(_(\"ref updates aborted by hook\"));\n    -+\t\tdie(_(\"ref updates aborted by the reference-transaction hook at its %s state\"), \"prepared\");\n    ++\t\tdie(_(abort_by_ref_transaction_hook), \"prepared\");\n      \t}\n      \n      \treturn 0;\n    @@ t/t1416-ref-transaction-hooks.sh: test_expect_success 'hook allows updating ref\n     +\t\tfi\n     +\tEOF\n     +\ttest_must_fail git update-ref HEAD POST 2>err &&\n    -+\ttest_grep \"ref updates aborted by the reference-transaction hook at its preparing state\" err\n    ++\ttest_grep \"in '\\''preparing'\\'' phase, update aborted by the reference-transaction hook\" err\n     +'\n     +\n      test_expect_success 'hook aborts updating ref in prepared state' '\n    @@ t/t1416-ref-transaction-hooks.sh: test_expect_success 'hook aborts updating ref\n      \tEOF\n      \ttest_must_fail git update-ref HEAD POST 2>err &&\n     -\ttest_grep \"ref updates aborted by hook\" err\n    -+\ttest_grep \"ref updates aborted by the reference-transaction hook at its prepared state\" err\n    ++\ttest_grep \"in '\\''prepared'\\'' phase, update aborted by the reference-transaction hook\" err\n      '\n      \n      test_expect_success 'hook gets all queued updates in prepared state' '\n-- \n2.51.0\n\n"},{"id":"539199","messageId":"20260317023624.43070-2-eric.peijian@gmail.com","threadId":"65240","inReplyTo":"20260317023624.43070-1-eric.peijian@gmail.com","subject":"[PATCH v3 1/1] refs: add 'preparing' phase to the reference-transaction hook","fromName":"Eric Ju","fromEmail":"eric.peijian@gmail.com","sentAt":"2026-03-17T02:36:24Z","receivedAt":"2026-03-17T02:36:30Z","isPatch":true,"sender":{"key":"eric.peijian@gmail.com","avatar":"https://avatars.githubusercontent.com/u/7400645?v=4"},"body":"The \"reference-transaction\" hook is invoked multiple times during a ref\ntransaction. Each invocation corresponds to a different phase:\n\n- The \"prepared\" phase indicates that references have been locked.\n- The \"committed\" phase indicates that all updates have been written to disk.\n- The \"aborted\" phase indicates that the transaction has been aborted and that\n  all changes have been rolled back.\n\nThis hook can be used to learn about the updates that Git wants to perform.\nFor example, forges use it to coordinate reference updates across multiple\nnodes.\n\nHowever, the phases are insufficient for some specific use cases. The earliest\nobservable phase in the \"reference-transaction\" hook is \"prepared\", at which\npoint Git has already taken exclusive locks on every affected reference. This\nmakes it suitable for last-chance validation, but not for serialization. So by\nthe time a hook sees the \"prepared\" phase, it has no way to defer locking, and\nthus it cannot rearrange multiple concurrent ref transactions relative to one\nanother.\n\nIntroduce a new \"preparing\" phase that runs before the \"prepared\" phase, that\nis before Git acquires any reference lock on disk. This gives callers a\nwell-defined window to perform validation, enable higher-level ordering of\nconcurrent transactions, or reject the transaction entirely, all without\ninterfering with the locking state.\n\nThis change is strictly speaking not backwards compatible. Existing hook\nscripts that do not know how to handle unknown phases may treat 'preparing'\nas an error and return non-zero. But the hook is considered to expose\ninternal implementation details of how Git works, and as such we have\nbeen a bit more lenient with changing its exact semantics, like for example\nin a8ae923f85 (refs: support symrefs in 'reference-transaction' hook, 2024-05-07).\n\nAn alternative would be to introduce a \"reference-transaction-v2\" hook that\nknows about the new phase. This feels like a rather heavy-weight option though,\nand was thus discarded.\n\nHelped-by: Patrick Steinhardt <ps@pks.im>\nHelped-by: Justin Tobler <jltobler@gmail.com>\nHelped-by: Karthik Nayak <karthik.188@gmail.com>\nSigned-off-by: Eric Ju <eric.peijian@gmail.com>\n---\n Documentation/githooks.adoc      | 19 ++++++++++++-------\n refs.c                           | 12 +++++++++++-\n t/t1416-ref-transaction-hooks.sh | 30 ++++++++++++++++++++++++++----\n t/t5510-fetch.sh                 |  7 ++++++-\n 4 files changed, 55 insertions(+), 13 deletions(-)\n\ndiff --git a/Documentation/githooks.adoc b/Documentation/githooks.adoc\nindex 056553788d..ed045940d1 100644\n--- a/Documentation/githooks.adoc\n+++ b/Documentation/githooks.adoc\n@@ -484,13 +484,16 @@ reference-transaction\n ~~~~~~~~~~~~~~~~~~~~~\n \n This hook is invoked by any Git command that performs reference\n-updates. It executes whenever a reference transaction is prepared,\n-committed or aborted and may thus get called multiple times. The hook\n-also supports symbolic reference updates.\n+updates. It executes whenever a reference transaction is preparing,\n+prepared, committed or aborted and may thus get called multiple times.\n+The hook also supports symbolic reference updates.\n \n The hook takes exactly one argument, which is the current state the\n given reference transaction is in:\n \n+    - \"preparing\": All reference updates have been queued to the\n+      transaction but references are not yet locked on disk.\n+\n     - \"prepared\": All reference updates have been queued to the\n       transaction and references were locked on disk.\n \n@@ -511,16 +514,18 @@ ref and `<ref-name>` is the full name of the ref. When force updating\n the reference regardless of its current value or when the reference is\n to be created anew, `<old-value>` is the all-zeroes object name. To\n distinguish these cases, you can inspect the current value of\n-`<ref-name>` via `git rev-parse`.\n+`<ref-name>` via `git rev-parse`. During the \"preparing\" state, symbolic\n+references are not resolved: `<ref-name>` will reflect the symbolic reference\n+itself rather than the object it points to.\n \n For symbolic reference updates the `<old_value>` and `<new-value>`\n fields could denote references instead of objects. A reference will be\n denoted with a 'ref:' prefix, like `ref:<ref-target>`.\n \n The exit status of the hook is ignored for any state except for the\n-\"prepared\" state. In the \"prepared\" state, a non-zero exit status will\n-cause the transaction to be aborted. The hook will not be called with\n-\"aborted\" state in that case.\n+\"preparing\" and \"prepared\" states. In these states, a non-zero exit\n+status will cause the transaction to be aborted. The hook will not be\n+called with \"aborted\" state in that case.\n \n push-to-checkout\n ~~~~~~~~~~~~~~~~\ndiff --git a/refs.c b/refs.c\nindex 6fb8f9d10c..e66cf4861d 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -64,6 +64,9 @@ const char *ref_storage_format_to_name(enum ref_storage_format ref_storage_forma\n \treturn be->name;\n }\n \n+static const char *abort_by_ref_transaction_hook =\n+\tN_(\"in '%s' phase, update aborted by the reference-transaction hook\");\n+\n /*\n  * How to handle various characters in refnames:\n  * 0: An acceptable character for refs\n@@ -2655,6 +2658,13 @@ int ref_transaction_prepare(struct ref_transaction *transaction,\n \tif (ref_update_reject_duplicates(&transaction->refnames, err))\n \t\treturn REF_TRANSACTION_ERROR_GENERIC;\n \n+\t/* Preparing checks before locking references */\n+\tret = run_transaction_hook(transaction, \"preparing\");\n+\tif (ret) {\n+\t\tref_transaction_abort(transaction, err);\n+\t\tdie(_(abort_by_ref_transaction_hook), \"preparing\");\n+\t}\n+\n \tret = refs->be->transaction_prepare(refs, transaction, err);\n \tif (ret)\n \t\treturn ret;\n@@ -2662,7 +2672,7 @@ int ref_transaction_prepare(struct ref_transaction *transaction,\n \tret = run_transaction_hook(transaction, \"prepared\");\n \tif (ret) {\n \t\tref_transaction_abort(transaction, err);\n-\t\tdie(_(\"ref updates aborted by hook\"));\n+\t\tdie(_(abort_by_ref_transaction_hook), \"prepared\");\n \t}\n \n \treturn 0;\ndiff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh\nindex d91dd3a3b5..4fe9d9b234 100755\n--- a/t/t1416-ref-transaction-hooks.sh\n+++ b/t/t1416-ref-transaction-hooks.sh\n@@ -20,6 +20,7 @@ test_expect_success 'hook allows updating ref if successful' '\n \t\techo \"$*\" >>actual\n \tEOF\n \tcat >expect <<-EOF &&\n+\t\tpreparing\n \t\tprepared\n \t\tcommitted\n \tEOF\n@@ -27,6 +28,18 @@ test_expect_success 'hook allows updating ref if successful' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'hook aborts updating ref in preparing state' '\n+\tgit reset --hard PRE &&\n+\ttest_hook reference-transaction <<-\\EOF &&\n+\t\tif test \"$1\" = preparing\n+\t\tthen\n+\t\t\texit 1\n+\t\tfi\n+\tEOF\n+\ttest_must_fail git update-ref HEAD POST 2>err &&\n+\ttest_grep \"in '\\''preparing'\\'' phase, update aborted by the reference-transaction hook\" err\n+'\n+\n test_expect_success 'hook aborts updating ref in prepared state' '\n \tgit reset --hard PRE &&\n \ttest_hook reference-transaction <<-\\EOF &&\n@@ -36,7 +49,7 @@ test_expect_success 'hook aborts updating ref in prepared state' '\n \t\tfi\n \tEOF\n \ttest_must_fail git update-ref HEAD POST 2>err &&\n-\ttest_grep \"ref updates aborted by hook\" err\n+\ttest_grep \"in '\\''prepared'\\'' phase, update aborted by the reference-transaction hook\" err\n '\n \n test_expect_success 'hook gets all queued updates in prepared state' '\n@@ -121,6 +134,7 @@ test_expect_success 'interleaving hook calls succeed' '\n \tcat >expect <<-EOF &&\n \t\thooks/update refs/tags/PRE $ZERO_OID $PRE_OID\n \t\thooks/update refs/tags/POST $ZERO_OID $POST_OID\n+\t\thooks/reference-transaction preparing\n \t\thooks/reference-transaction prepared\n \t\thooks/reference-transaction committed\n \tEOF\n@@ -143,6 +157,8 @@ test_expect_success 'hook captures git-symbolic-ref updates' '\n \tgit symbolic-ref refs/heads/symref refs/heads/main &&\n \n \tcat >expect <<-EOF &&\n+\tpreparing\n+\t$ZERO_OID ref:refs/heads/main refs/heads/symref\n \tprepared\n \t$ZERO_OID ref:refs/heads/main refs/heads/symref\n \tcommitted\n@@ -171,14 +187,20 @@ test_expect_success 'hook gets all queued symref updates' '\n \t# In the files backend, \"delete\" also triggers an additional transaction\n \t# update on the packed-refs backend, which constitutes additional reflog\n \t# entries.\n+\tcat >expect <<-EOF &&\n+\tpreparing\n+\tref:refs/heads/main $ZERO_OID refs/heads/symref\n+\tref:refs/heads/main $ZERO_OID refs/heads/symrefd\n+\t$ZERO_OID ref:refs/heads/main refs/heads/symrefc\n+\tref:refs/heads/main ref:refs/heads/branch refs/heads/symrefu\n+\tEOF\n+\n \tif test_have_prereq REFFILES\n \tthen\n-\t\tcat >expect <<-EOF\n+\t\tcat >>expect <<-EOF\n \t\taborted\n \t\t$ZERO_OID $ZERO_OID refs/heads/symrefd\n \t\tEOF\n-\telse\n-\t\t>expect\n \tfi &&\n \n \tcat >>expect <<-EOF &&\ndiff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh\nindex 5dcb4b51a4..6fe21e2b3a 100755\n--- a/t/t5510-fetch.sh\n+++ b/t/t5510-fetch.sh\n@@ -469,12 +469,17 @@ test_expect_success 'fetch --atomic executes a single reference transaction only\n \thead_oid=$(git rev-parse HEAD) &&\n \n \tcat >expected <<-EOF &&\n+\t\tpreparing\n+\t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-1\n+\t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-2\n \t\tprepared\n \t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-1\n \t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-2\n \t\tcommitted\n \t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-1\n \t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-2\n+\t\tpreparing\n+\t\t$ZERO_OID ref:refs/remotes/origin/main refs/remotes/origin/HEAD\n \tEOF\n \n \trm -f atomic/actual &&\n@@ -497,7 +502,7 @@ test_expect_success 'fetch --atomic aborts all reference updates if hook aborts'\n \thead_oid=$(git rev-parse HEAD) &&\n \n \tcat >expected <<-EOF &&\n-\t\tprepared\n+\t\tpreparing\n \t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-abort-1\n \t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-abort-2\n \t\t$ZERO_OID $head_oid refs/remotes/origin/atomic-hooks-abort-3\n-- \n2.51.0\n\n"}]}