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:
committed by
Junio C Hamano
parent
15c45c7458
commit
5c697f0b7d
@@ -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";
|
||||||
@@ -1905,9 +1937,11 @@ failure:
|
|||||||
|
|
||||||
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,
|
||||||
struct shallow_info *si)
|
struct shallow_info *si)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user