receive-pack: handle reference deletions separately

In 9d2962a7c4 (receive-pack: use batched reference updates, 2025-05-19)
we updated the 'git-receive-pack(1)' command to use batched reference
updates. One edge case which was missed during this implementation was
when a user pushes multiple branches such as:

  delete refs/heads/branch/conflict
  create refs/heads/branch

Before using batched updates, the references would be applied
sequentially and hence no conflicts would arise. With batched updates,
while the first update applies, the second fails due to D/F conflict. A
similar issue was present in 'git-fetch(1)' and was fixed by separating
out reference pruning into a separate transaction in the commit 'fetch:
use batched reference updates'. Apply a similar mechanism for
'git-receive-pack(1)' and separate out reference deletions into its own
batch.

This means 'git-receive-pack(1)' will now use up to two transactions,
whereas before using batched updates it would use _at least_ two
transactions. So using batched updates is still the better option.

Add a test to validate this behavior.

Signed-off-by: Karthik Nayak <karthik.188@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Karthik Nayak
2025-06-20 09:15:45 +02:00
committed by Junio C Hamano
parent 15c45c7458
commit 5c697f0b7d
2 changed files with 87 additions and 44 deletions

View File

@@ -1866,6 +1866,37 @@ static void execute_commands_non_atomic(struct command *commands,
const char *reported_error = NULL; const char *reported_error = NULL;
struct strmap failed_refs = STRMAP_INIT; struct strmap failed_refs = STRMAP_INIT;
/*
* Reference updates, where D/F conflicts shouldn't arise due to
* one reference being deleted, while the other being created
* are treated as conflicts in batched updates. This is because
* we don't do conflict resolution inside a transaction. To
* mitigate this, delete references in a separate batch.
*
* NEEDSWORK: Add conflict resolution between deletion and creation
* of reference updates within a transaction. With that, we can
* combine the two phases.
*/
enum processing_phase {
PHASE_DELETIONS,
PHASE_OTHERS
};
for (enum processing_phase phase = PHASE_DELETIONS; phase <= PHASE_OTHERS; phase++) {
for (cmd = commands; cmd; cmd = cmd->next) {
if (!should_process_cmd(cmd) || cmd->run_proc_receive)
continue;
if (phase == PHASE_DELETIONS && !is_null_oid(&cmd->new_oid))
continue;
else if (phase == PHASE_OTHERS && is_null_oid(&cmd->new_oid))
continue;
/*
* Lazily create a transaction only when we know there are
* updates to be added.
*/
if (!transaction) {
transaction = ref_store_transaction_begin(get_main_ref_store(the_repository), transaction = ref_store_transaction_begin(get_main_ref_store(the_repository),
REF_TRANSACTION_ALLOW_FAILURE, &err); REF_TRANSACTION_ALLOW_FAILURE, &err);
if (!transaction) { if (!transaction) {
@@ -1874,14 +1905,15 @@ static void execute_commands_non_atomic(struct command *commands,
reported_error = "transaction failed to start"; reported_error = "transaction failed to start";
goto failure; goto failure;
} }
}
for (cmd = commands; cmd; cmd = cmd->next) {
if (!should_process_cmd(cmd) || cmd->run_proc_receive)
continue;
cmd->error_string = update(cmd, si); cmd->error_string = update(cmd, si);
} }
/* No transaction, so nothing to commit */
if (!transaction)
goto cleanup;
if (ref_transaction_commit(transaction, &err)) { if (ref_transaction_commit(transaction, &err)) {
rp_error("%s", err.buf); rp_error("%s", err.buf);
reported_error = "failed to update refs"; reported_error = "failed to update refs";
@@ -1895,7 +1927,7 @@ static void execute_commands_non_atomic(struct command *commands,
if (strmap_empty(&failed_refs)) if (strmap_empty(&failed_refs))
goto cleanup; goto cleanup;
failure: failure:
for (cmd = commands; cmd; cmd = cmd->next) { for (cmd = commands; cmd; cmd = cmd->next) {
if (reported_error) if (reported_error)
cmd->error_string = reported_error; cmd->error_string = reported_error;
@@ -1903,10 +1935,12 @@ failure:
cmd->error_string = strmap_get(&failed_refs, cmd->ref_name); cmd->error_string = strmap_get(&failed_refs, cmd->ref_name);
} }
cleanup: cleanup:
ref_transaction_free(transaction); ref_transaction_free(transaction);
transaction = NULL;
strmap_clear(&failed_refs, 0); strmap_clear(&failed_refs, 0);
strbuf_release(&err); strbuf_release(&err);
}
} }
static void execute_commands_atomic(struct command *commands, static void execute_commands_atomic(struct command *commands,

View File

@@ -744,8 +744,8 @@ test_expect_success 'pushing valid refs triggers post-receive and post-update ho
EOF EOF
cat >update.expect <<-EOF && cat >update.expect <<-EOF &&
refs/heads/main $orgmain $newmain
refs/heads/next $orgnext $newnext refs/heads/next $orgnext $newnext
refs/heads/main $orgmain $newmain
EOF EOF
cat >post-receive.expect <<-EOF && cat >post-receive.expect <<-EOF &&
@@ -808,8 +808,8 @@ test_expect_success 'deletion of a non-existent ref is not fed to post-receive a
EOF EOF
cat >update.expect <<-EOF && cat >update.expect <<-EOF &&
refs/heads/main $orgmain $newmain
refs/heads/nonexistent $ZERO_OID $ZERO_OID refs/heads/nonexistent $ZERO_OID $ZERO_OID
refs/heads/main $orgmain $newmain
EOF EOF
cat >post-receive.expect <<-EOF && cat >post-receive.expect <<-EOF &&
@@ -868,10 +868,10 @@ test_expect_success 'mixed ref updates, deletes, invalid deletes trigger hooks w
EOF EOF
cat >update.expect <<-EOF && cat >update.expect <<-EOF &&
refs/heads/main $orgmain $newmain
refs/heads/next $orgnext $newnext refs/heads/next $orgnext $newnext
refs/heads/seen $orgseen $newseen
refs/heads/nonexistent $ZERO_OID $ZERO_OID refs/heads/nonexistent $ZERO_OID $ZERO_OID
refs/heads/main $orgmain $newmain
refs/heads/seen $orgseen $newseen
EOF EOF
cat >post-receive.expect <<-EOF && cat >post-receive.expect <<-EOF &&
@@ -1909,4 +1909,13 @@ test_expect_success 'push with config push.useBitmaps' '
--thin --delta-base-offset -q --no-use-bitmap-index <false --thin --delta-base-offset -q --no-use-bitmap-index <false
' '
test_expect_success 'push with F/D conflict with deletion and creation' '
test_when_finished "git branch -D branch" &&
git branch branch/conflict &&
mk_test testrepo heads/branch/conflict &&
git branch -D branch/conflict &&
git branch branch &&
git push testrepo :refs/heads/branch/conflict refs/heads/branch
'
test_done test_done