Skip to content

Commit 5916b99

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 F/D conflict. A similar issue was present in 'git-fetch(1)' and was fixed by using separating out reference pruning into a separate transaction. 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 upto 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 0562390 commit 5916b99

File tree

2 files changed

+79
-38
lines changed

2 files changed

+79
-38
lines changed

builtin/receive-pack.c

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

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

1879-
for (cmd = commands; cmd; cmd = cmd->next) {
1880-
if (!should_process_cmd(cmd) || cmd->run_proc_receive)
1881-
continue;
1882+
for (enum processing_phase phase = PHASE_DELETIONS; phase <= PHASE_OTHERS; phase++) {
1883+
for (cmd = commands; cmd; cmd = cmd->next) {
1884+
if (!should_process_cmd(cmd) || cmd->run_proc_receive)
1885+
continue;
18821886

1883-
cmd->error_string = update(cmd, si);
1884-
}
1887+
if (phase == PHASE_DELETIONS && !is_null_oid(&cmd->new_oid))
1888+
continue;
1889+
else if (phase == PHASE_OTHERS && is_null_oid(&cmd->new_oid))
1890+
continue;
18851891

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

1892-
ref_transaction_for_each_rejected_update(transaction,
1893-
ref_transaction_rejection_handler,
1894-
&failed_refs);
1907+
cmd->error_string = update(cmd, si);
1908+
}
18951909

1896-
if (strmap_empty(&failed_refs))
1897-
goto cleanup;
1910+
/*
1911+
* If no transaction was created, there is nothing to commit.
1912+
*/
1913+
if (!transaction)
1914+
goto cleanup;
18981915

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

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

19131945
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)