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,47 +1866,81 @@ static void execute_commands_non_atomic(struct command *commands,
const char *reported_error = NULL;
struct strmap failed_refs = STRMAP_INIT;
transaction = ref_store_transaction_begin(get_main_ref_store(the_repository),
REF_TRANSACTION_ALLOW_FAILURE, &err);
if (!transaction) {
rp_error("%s", err.buf);
strbuf_reset(&err);
reported_error = "transaction failed to start";
goto failure;
/*
* 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),
REF_TRANSACTION_ALLOW_FAILURE, &err);
if (!transaction) {
rp_error("%s", err.buf);
strbuf_reset(&err);
reported_error = "transaction failed to start";
goto failure;
}
}
cmd->error_string = update(cmd, si);
}
/* No transaction, so nothing to commit */
if (!transaction)
goto cleanup;
if (ref_transaction_commit(transaction, &err)) {
rp_error("%s", err.buf);
reported_error = "failed to update refs";
goto failure;
}
ref_transaction_for_each_rejected_update(transaction,
ref_transaction_rejection_handler,
&failed_refs);
if (strmap_empty(&failed_refs))
goto cleanup;
failure:
for (cmd = commands; cmd; cmd = cmd->next) {
if (reported_error)
cmd->error_string = reported_error;
else if (strmap_contains(&failed_refs, cmd->ref_name))
cmd->error_string = strmap_get(&failed_refs, cmd->ref_name);
}
cleanup:
ref_transaction_free(transaction);
transaction = NULL;
strmap_clear(&failed_refs, 0);
strbuf_release(&err);
}
for (cmd = commands; cmd; cmd = cmd->next) {
if (!should_process_cmd(cmd) || cmd->run_proc_receive)
continue;
cmd->error_string = update(cmd, si);
}
if (ref_transaction_commit(transaction, &err)) {
rp_error("%s", err.buf);
reported_error = "failed to update refs";
goto failure;
}
ref_transaction_for_each_rejected_update(transaction,
ref_transaction_rejection_handler,
&failed_refs);
if (strmap_empty(&failed_refs))
goto cleanup;
failure:
for (cmd = commands; cmd; cmd = cmd->next) {
if (reported_error)
cmd->error_string = reported_error;
else if (strmap_contains(&failed_refs, cmd->ref_name))
cmd->error_string = strmap_get(&failed_refs, cmd->ref_name);
}
cleanup:
ref_transaction_free(transaction);
strmap_clear(&failed_refs, 0);
strbuf_release(&err);
}
static void execute_commands_atomic(struct command *commands,