refs: skip hooks when deleting uncovered packed refs

When deleting refs from the loose-files refs backend, then we need to be careful to also delete the same ref from the packed refs backend, if it exists. If we don't, then deleting the loose ref would "uncover" the packed ref. We thus always have to queue up deletions of refs for both the loose and the packed refs backend. This is done in two separate transactions, where the end result is that the reference-transaction hook is executed twice for the deleted refs. This behaviour is quite misleading: it's exposing implementation details of how the files backend works to the user, in contrast to the logical updates that we'd really want to expose via the hook. Worse yet, whether the hook gets executed once or twice depends on how well-packed the repository is: if the ref only exists as a loose ref, then we execute it once, otherwise if it is also packed then we execute it twice. Fix this behaviour and don't execute the reference-transaction hook at all when refs in the packed-refs backend if it's driven by the files backend. This works as expected even in case the refs to be deleted only exist in the packed-refs backend because the loose-backend always queues refs in its own transaction even if they don't exist such that they can be locked for concurrent creation. And it also does the right thing in case neither of the backends has the ref because that would cause the transaction to fail completely. Signed-off-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrick Steinhardt committed Jan 17, 2022 at 09:12 UTC 2ed1b64ebdeefc7f9473ae159fb45ff0c6cf121a
2 files changed +7 -9
refs/files-backend.c
+6 -3
@@ -1261,7 +1261,8 @@ static int files_delete_refs(struct ref_store *ref_store, const char *msg,
1261 if (packed_refs_lock(refs->packed_ref_store, 0, &err))
1262 goto error;
1263
1264 - transaction = ref_store_transaction_begin(refs->packed_ref_store, 0, &err);
1264 + transaction = ref_store_transaction_begin(refs->packed_ref_store,
1265 + REF_TRANSACTION_SKIP_HOOK, &err);
1266 if (!transaction)
1267 goto error;
1268
@@ -2776,7 +2777,8 @@ static int files_transaction_prepare(struct ref_store *ref_store,
2777 */
2778 if (!packed_transaction) {
2779 packed_transaction = ref_store_transaction_begin(
2779 - refs->packed_ref_store, 0, err);
2780 + refs->packed_ref_store,
2781 + REF_TRANSACTION_SKIP_HOOK, err);
2782 if (!packed_transaction) {
2783 ret = TRANSACTION_GENERIC_ERROR;
2784 goto cleanup;
@@ -3047,7 +3049,8 @@ static int files_initial_transaction_commit(struct ref_store *ref_store,
3049 &affected_refnames))
3050 BUG("initial ref transaction called with existing refs");
3051
3050 - packed_transaction = ref_store_transaction_begin(refs->packed_ref_store, 0, err);
3052 + packed_transaction = ref_store_transaction_begin(refs->packed_ref_store,
3053 + REF_TRANSACTION_SKIP_HOOK, err);
3054 if (!packed_transaction) {
3055 ret = TRANSACTION_GENERIC_ERROR;
3056 goto cleanup;
t/t1416-ref-transaction-hooks.sh
+1 -6
@@ -175,16 +175,11 @@ test_expect_success 'deleting packed ref calls hook once' '
175 git update-ref -d refs/heads/to-be-deleted $POST_OID &&
176
177 # We only expect a single hook invocation, which is the logical
178 - # deletion. But currently, we see two interleaving transactions, once
179 - # for deleting the loose refs and once for deleting the packed ref.
178 + # deletion.
179 cat >expect <<-EOF &&
181 - prepared
182 - $ZERO_OID $ZERO_OID refs/heads/to-be-deleted
180 prepared
181 $POST_OID $ZERO_OID refs/heads/to-be-deleted
182 committed
186 - $ZERO_OID $ZERO_OID refs/heads/to-be-deleted
187 - committed
183 $POST_OID $ZERO_OID refs/heads/to-be-deleted
184 EOF
185