From 7c7389a817174fe595afb67042f3d2cc1b27165a Mon Sep 17 00:00:00 2001 From: Karthik Nayak Date: Mon, 12 Oct 2026 17:29:17 +0200 Subject: [PATCH] fetch: commit references fetched before backfilling tags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In 0e358de64a (fetch: use batched reference updates, 2025-05-19), the fetch code was modified to use batched updates to provide a good performance improvement. Wherein batched updates were used to fetch both references and backfill tags. When using batched updates, the references aren't yet committed to disk when we start backfilling tags. This means in situations such as shallow fetching the negotiation during backfilling tags, the client doesn't have any references to report in the 'have' section. Since backfilling doesn't use a depth limit, this can cause the server to send all the objects present in the repository. Fix this by committing the previous batched update and initiating a new one for backfilling tags. Also add a test which captures this regression. While this does make it a little slower than master, due to two transactions: Benchmark 1: fetch: many refs (refformat = reftable, refcount = 10000, revision = master) Time (mean ± σ): 82.1 ms ± 3.3 ms [User: 60.2 ms, System: 24.4 ms] Range (min … max): 77.4 ms … 89.4 ms 33 runs Benchmark 2: fetch: many refs (refformat = reftable, refcount = 10000, revision = HEAD) Time (mean ± σ): 82.6 ms ± 2.4 ms [User: 59.3 ms, System: 25.2 ms] Range (min … max): 78.1 ms … 88.7 ms 31 runs Summary fetch: many refs (refformat = reftable, refcount = 10000, revision = master) ran 1.01 ± 0.05 times faster than fetch: many refs (refformat = reftable, refcount = 10000, revision = HEAD) It is still faster than not using batched updates: Benchmark 1: fetch: many refs (refformat = reftable, refcount = 10000, revision = 0e358de64a9e014575d11ef884bfc9beb931e37f~1) Time (mean ± σ): 1.468 s ± 0.041 s [User: 0.839 s, System: 0.587 s] Range (min … max): 1.427 s … 1.558 s 10 runs Benchmark 2: fetch: many refs (refformat = reftable, refcount = 10000, revision = HEAD) Time (mean ± σ): 84.4 ms ± 1.7 ms [User: 60.9 ms, System: 25.8 ms] Range (min … max): 81.4 ms … 88.6 ms 29 runs Summary fetch: many refs (refformat = reftable, refcount = 10000, revision = HEAD) ran 17.38 ± 0.61 times faster than fetch: many refs (refformat = reftable, refcount = 10000, revision = 0e358de64a9e014575d11ef884bfc9beb931e37f~1) Reported-by: Mitja Bezenšek Signed-off-by: Karthik Nayak Signed-off-by: Junio C Hamano --- builtin/fetch.c | 21 +++++++++++++++++++++ t/t5510-fetch.sh | 16 ++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/builtin/fetch.c b/builtin/fetch.c index c1d7c672f4..57a0a5983c 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -2038,6 +2038,27 @@ static int do_fetch(struct transport *transport, struct ref *tags_ref_map = NULL, **tail = &tags_ref_map; find_non_local_tags(remote_refs, transaction, &tags_ref_map, &tail); + + /* + * Backfilling tags has no depth limit. If we don't commit + * the fetched references, the backfill will report no refs + * in the 'have' section of the negotiation. This can cause + * the server to send all objects. + */ + if (tags_ref_map && !atomic_fetch) { + retcode |= commit_ref_transaction(&transaction, false, + transport->remote->name, + &rejected_refs, &err); + + transaction = ref_store_transaction_begin(get_main_ref_store(the_repository), + REF_TRANSACTION_ALLOW_FAILURE, &err); + if (!transaction) { + free_refs(tags_ref_map); + retcode = -1; + goto cleanup; + } + } + if (tags_ref_map) { /* * If backfilling of tags fails then we want to tell diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh index eca9a973b5..725c91b5c0 100755 --- a/t/t5510-fetch.sh +++ b/t/t5510-fetch.sh @@ -1836,6 +1836,22 @@ test_expect_success "backfill tags when providing a refspec" ' test_cmp expect actual ' +test_expect_success 'shallow fetch does not fetch objects again for tags' ' + test_when_finished rm -rf source target && + + git init source && + test_commit_bulk -C source 10 && + git -C source tag -a tag -m tag HEAD~2 && + root_oid=$(git -C source rev-parse HEAD~9) && + + git init target && + git -C target remote add origin ../source && + git -C target fetch --depth 5 origin && + + # Should not fetch objects beyond the pre-specified depth. + test_must_fail git -C target cat-file -e "$root_oid" +' + test_expect_success REFFILES "FETCH_HEAD is updated even if ref updates fail" ' test_when_finished rm -rf base repo &&