Skip to content

Commit 5c697f0

Browse files
KarthikNayakgitster
authored andcommitted
receive-pack: handle reference deletions separately
In 9d2962a (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 <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 15c45c7 commit 5c697f0

File tree

2 files changed

+81
-38
lines changed

2 files changed

+81
-38
lines changed

builtin/receive-pack.c

Lines changed: 68 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1866,47 +1866,81 @@ static void execute_commands_non_atomic(struct command *commands,
18661866
const char *reported_error = NULL;
18671867
struct strmap failed_refs = STRMAP_INIT;
18681868

1869-
transaction = ref_store_transaction_begin(get_main_ref_store(the_repository),
1870-
REF_TRANSACTION_ALLOW_FAILURE, &err);
1871-
if (!transaction) {
1872-
rp_error("%s", err.buf);
1873-
strbuf_reset(&err);
1874-
reported_error = "transaction failed to start";
1875-
goto failure;
1876-
}
1869+
/*
1870+
* Reference updates, where D/F conflicts shouldn't arise due to
1871+
* one reference being deleted, while the other being created
1872+
* are treated as conflicts in batched updates. This is because
1873+
* we don't do conflict resolution inside a transaction. To
1874+
* mitigate this, delete references in a separate batch.
1875+
*
1876+
* NEEDSWORK: Add conflict resolution between deletion and creation
1877+
* of reference updates within a transaction. With that, we can
1878+
* combine the two phases.
1879+
*/
1880+
enum processing_phase {
1881+
PHASE_DELETIONS,
1882+
PHASE_OTHERS
1883+
};
18771884

1878-
for (cmd = commands; cmd; cmd = cmd->next) {
1879-
if (!should_process_cmd(cmd) || cmd->run_proc_receive)
1880-
continue;
1885+
for (enum processing_phase phase = PHASE_DELETIONS; phase <= PHASE_OTHERS; phase++) {
1886+
for (cmd = commands; cmd; cmd = cmd->next) {
1887+
if (!should_process_cmd(cmd) || cmd->run_proc_receive)
1888+
continue;
18811889

1882-
cmd->error_string = update(cmd, si);
1883-
}
1890+
if (phase == PHASE_DELETIONS && !is_null_oid(&cmd->new_oid))
1891+
continue;
1892+
else if (phase == PHASE_OTHERS && is_null_oid(&cmd->new_oid))
1893+
continue;
18841894

1885-
if (ref_transaction_commit(transaction, &err)) {
1886-
rp_error("%s", err.buf);
1887-
reported_error = "failed to update refs";
1888-
goto failure;
1889-
}
1895+
/*
1896+
* Lazily create a transaction only when we know there are
1897+
* updates to be added.
1898+
*/
1899+
if (!transaction) {
1900+
transaction = ref_store_transaction_begin(get_main_ref_store(the_repository),
1901+
REF_TRANSACTION_ALLOW_FAILURE, &err);
1902+
if (!transaction) {
1903+
rp_error("%s", err.buf);
1904+
strbuf_reset(&err);
1905+
reported_error = "transaction failed to start";
1906+
goto failure;
1907+
}
1908+
}
18901909

1891-
ref_transaction_for_each_rejected_update(transaction,
1892-
ref_transaction_rejection_handler,
1893-
&failed_refs);
1910+
cmd->error_string = update(cmd, si);
1911+
}
18941912

1895-
if (strmap_empty(&failed_refs))
1896-
goto cleanup;
1913+
/* No transaction, so nothing to commit */
1914+
if (!transaction)
1915+
goto cleanup;
18971916

1898-
failure:
1899-
for (cmd = commands; cmd; cmd = cmd->next) {
1900-
if (reported_error)
1901-
cmd->error_string = reported_error;
1902-
else if (strmap_contains(&failed_refs, cmd->ref_name))
1903-
cmd->error_string = strmap_get(&failed_refs, cmd->ref_name);
1904-
}
1917+
if (ref_transaction_commit(transaction, &err)) {
1918+
rp_error("%s", err.buf);
1919+
reported_error = "failed to update refs";
1920+
goto failure;
1921+
}
19051922

1906-
cleanup:
1907-
ref_transaction_free(transaction);
1908-
strmap_clear(&failed_refs, 0);
1909-
strbuf_release(&err);
1923+
ref_transaction_for_each_rejected_update(transaction,
1924+
ref_transaction_rejection_handler,
1925+
&failed_refs);
1926+
1927+
if (strmap_empty(&failed_refs))
1928+
goto cleanup;
1929+
1930+
failure:
1931+
for (cmd = commands; cmd; cmd = cmd->next) {
1932+
if (reported_error)
1933+
cmd->error_string = reported_error;
1934+
else if (strmap_contains(&failed_refs, cmd->ref_name))
1935+
cmd->error_string = strmap_get(&failed_refs, cmd->ref_name);
1936+
}
1937+
1938+
cleanup:
1939+
ref_transaction_free(transaction);
1940+
transaction = NULL;
1941+
strmap_clear(&failed_refs, 0);
1942+
strbuf_release(&err);
1943+
}
19101944
}
19111945

19121946
static void execute_commands_atomic(struct command *commands,

t/t5516-fetch-push.sh

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -744,8 +744,8 @@ test_expect_success 'pushing valid refs triggers post-receive and post-update ho
744744
EOF
745745
746746
cat >update.expect <<-EOF &&
747-
refs/heads/main $orgmain $newmain
748747
refs/heads/next $orgnext $newnext
748+
refs/heads/main $orgmain $newmain
749749
EOF
750750
751751
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
808808
EOF
809809
810810
cat >update.expect <<-EOF &&
811-
refs/heads/main $orgmain $newmain
812811
refs/heads/nonexistent $ZERO_OID $ZERO_OID
812+
refs/heads/main $orgmain $newmain
813813
EOF
814814
815815
cat >post-receive.expect <<-EOF &&
@@ -868,10 +868,10 @@ test_expect_success 'mixed ref updates, deletes, invalid deletes trigger hooks w
868868
EOF
869869
870870
cat >update.expect <<-EOF &&
871-
refs/heads/main $orgmain $newmain
872871
refs/heads/next $orgnext $newnext
873-
refs/heads/seen $orgseen $newseen
874872
refs/heads/nonexistent $ZERO_OID $ZERO_OID
873+
refs/heads/main $orgmain $newmain
874+
refs/heads/seen $orgseen $newseen
875875
EOF
876876
877877
cat >post-receive.expect <<-EOF &&
@@ -1909,4 +1909,13 @@ test_expect_success 'push with config push.useBitmaps' '
19091909
--thin --delta-base-offset -q --no-use-bitmap-index <false
19101910
'
19111911

1912+
test_expect_success 'push with F/D conflict with deletion and creation' '
1913+
test_when_finished "git branch -D branch" &&
1914+
git branch branch/conflict &&
1915+
mk_test testrepo heads/branch/conflict &&
1916+
git branch -D branch/conflict &&
1917+
git branch branch &&
1918+
git push testrepo :refs/heads/branch/conflict refs/heads/branch
1919+
'
1920+
19121921
test_done

0 commit comments

Comments
 (0)