Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPatrick Steinhardt <psteinhardt@gitlab.com>2022-02-15 11:01:54 +0300
committerPatrick Steinhardt <psteinhardt@gitlab.com>2022-03-02 10:40:18 +0300
commitc0d05650be681c2accb4cec5aac74a6dd77a2fa6 (patch)
tree934b099957d53ae2fec0eeb5dda4a81ae65b5e23
parent6d0bb8bd1409d6bb886b3f616177c25efc3e4923 (diff)
Makefile: Install Git v2.35.1 as part of bundled Git
We're currently still stuck with Git v2.33.1.gl2 because we haven't yet been able to roll out bundled Git to production, and we do want to make use of feature flags to switch to newly introduced Git versions. Bundled Git should soon be landing in production though, so this commit starts to build and install Git v2.35.1.gl1 such that we're prepared to quickly roll out bundled Git and then the new Git version. Note that this also backports two patch series from the upcoming Git v2.36.0 release: - Patches 21-26 fix an issue with the files refs backend, which would execute the reference-transaction hook for references that were about to be deleted twice: once for the loose ref itself, and once for the packed backend. This is a performance issue, but also required us to have a workaround that filters out any reference transactions which only consisted of deletes. - Patches 27-34 fix the `--atomic` flag of git-fetch(1) to also cover deletion of references and backfilling of tags. This has caused excessive voting via reference transactions in production because the reference-transaction hook was executed twice for each reference modified via those mechanisms. It is thus a correctness fix because we don't commit changes to disk anymore if any of the other ref updates fails, and a performance fix for mirror fetches because we perform less voting.
-rw-r--r--Makefile50
-rw-r--r--_support/git-patches/0021-refs-extract-packed_refs_delete_refs-to-allow-contro.patch156
-rw-r--r--_support/git-patches/0022-refs-allow-passing-flags-when-beginning-transactions.patch183
-rw-r--r--_support/git-patches/0023-refs-allow-skipping-the-reference-transaction-hook.patch60
-rw-r--r--_support/git-patches/0024-refs-demonstrate-excessive-execution-of-the-referenc.patch97
-rw-r--r--_support/git-patches/0025-refs-do-not-execute-reference-transaction-hook-on-pa.patch81
-rw-r--r--_support/git-patches/0026-refs-skip-hooks-when-deleting-uncovered-packed-refs.patch99
-rw-r--r--_support/git-patches/0027-fetch-prune-exit-with-error-if-pruning-fails.patch74
-rw-r--r--_support/git-patches/0028-fetch-increase-test-coverage-of-fetches.patch169
-rw-r--r--_support/git-patches/0029-fetch-backfill-tags-before-setting-upstream.patch116
-rw-r--r--_support/git-patches/0030-fetch-control-lifecycle-of-FETCH_HEAD-in-a-single-pl.patch170
-rw-r--r--_support/git-patches/0031-fetch-report-errors-when-backfilling-tags-fails.patch100
-rw-r--r--_support/git-patches/0032-refs-add-interface-to-iterate-over-queued-transactio.patch98
-rw-r--r--_support/git-patches/0033-fetch-make-atomic-flag-cover-backfilling-of-tags.patch310
-rw-r--r--_support/git-patches/0034-fetch-make-atomic-flag-cover-pruning-of-refs.patch149
15 files changed, 1910 insertions, 2 deletions
diff --git a/Makefile b/Makefile
index 53a681fcd..9ab1c0cef 100644
--- a/Makefile
+++ b/Makefile
@@ -356,12 +356,16 @@ install: build
.PHONY: build-bundled-git
## Build bundled Git binaries.
-build-bundled-git: $(patsubst %,${BUILD_DIR}/bin/gitaly-%,${GIT_EXECUTABLES})
+build-bundled-git: build-bundled-git-v2.33.1.gl2 build-bundled-git-v2.35.1.gl1
+build-bundled-git-v2.33.1.gl2: $(patsubst %,${BUILD_DIR}/bin/gitaly-%,${GIT_EXECUTABLES})
+build-bundled-git-v2.35.1.gl1: $(patsubst %,${BUILD_DIR}/bin/gitaly-%-v2.35.1.gl1,${GIT_EXECUTABLES})
.PHONY: install-bundled-git
## Install bundled Git binaries. The target directory can be modified by
## setting PREFIX and DESTDIR.
-install-bundled-git: $(patsubst %,${INSTALL_DEST_DIR}/gitaly-%,${GIT_EXECUTABLES})
+install-bundled-git: install-bundled-git-v2.33.1.gl2 install-bundled-git-v2.35.1.gl1
+install-bundled-git-v2.33.1.gl2: $(patsubst %,${INSTALL_DEST_DIR}/gitaly-%,${GIT_EXECUTABLES})
+install-bundled-git-v2.35.1.gl1: $(patsubst %,${INSTALL_DEST_DIR}/gitaly-%-v2.35.1.gl1,${GIT_EXECUTABLES})
ifdef WITH_BUNDLED_GIT
build: build-bundled-git
@@ -643,6 +647,48 @@ ${GIT_PREFIX}/bin/git: ${DEPENDENCY_DIR}/git-${GIT_VERSION}.${GIT_EXTRA_VERSION}
${BUILD_DIR}/bin/gitaly-%: ${DEPENDENCY_DIR}/git-${GIT_VERSION}.${GIT_EXTRA_VERSION}/% | ${BUILD_DIR}/bin
${Q}install $< $@
+# Speed up fetches by making better use of the commit-graph and by not
+# computing the output-width if not requested. Merged into next via
+# 2b331293fb (Merge branch 'ps/fetch-optim-with-commit-graph' into next,
+# 2022-02-14).
+${BUILD_DIR}/bin/gitaly-%-v2.35.1.gl1: override GIT_PATCHES := 0019-fetch-pack-use-commit-graph-when-computing-cutoff.patch
+${BUILD_DIR}/bin/gitaly-%-v2.35.1.gl1: override GIT_PATCHES += 0020-fetch-skip-computing-output-width-when-not-printing-.patch
+
+# Skip execution of the reference-transaction hook a second time via the
+# packed-refs backend in case loose references are deleted. This will
+# eventually make a workaround obsolete where we had to filter out all
+# invocations of the hook where we only saw force-deletions of references such
+# that we don't depend on whether refs are packed or not. Merged into main via
+# 991b4d47f0 (Merge branch
+# 'ps/avoid-unnecessary-hook-invocation-with-packed-refs', 2022-02-18).
+${BUILD_DIR}/bin/gitaly-%-v2.35.1.gl1: override GIT_PATCHES += 0021-refs-extract-packed_refs_delete_refs-to-allow-contro.patch
+${BUILD_DIR}/bin/gitaly-%-v2.35.1.gl1: override GIT_PATCHES += 0022-refs-allow-passing-flags-when-beginning-transactions.patch
+${BUILD_DIR}/bin/gitaly-%-v2.35.1.gl1: override GIT_PATCHES += 0023-refs-allow-skipping-the-reference-transaction-hook.patch
+${BUILD_DIR}/bin/gitaly-%-v2.35.1.gl1: override GIT_PATCHES += 0024-refs-demonstrate-excessive-execution-of-the-referenc.patch
+${BUILD_DIR}/bin/gitaly-%-v2.35.1.gl1: override GIT_PATCHES += 0025-refs-do-not-execute-reference-transaction-hook-on-pa.patch
+${BUILD_DIR}/bin/gitaly-%-v2.35.1.gl1: override GIT_PATCHES += 0026-refs-skip-hooks-when-deleting-uncovered-packed-refs.patch
+
+# Fix atomicity of git-fetch(1) to also cover pruning of references and
+# backfilling of tags. Previously, each reference modified via any of both
+# means would have created its own transaction and thus led to multiple hook
+# invocations. Merged into next via 3824153b23 (Merge branch 'ps/fetch-atomic'
+# into next, 2022-02-18). The first patch is unrelated, but required to fix a
+# merge conflict. It has been merged to main via c73d46b3a8 (Merge branch
+# 'tg/fetch-prune-exit-code-fix', 2022-02-11).
+${BUILD_DIR}/bin/gitaly-%-v2.35.1.gl1: override GIT_PATCHES += 0027-fetch-prune-exit-with-error-if-pruning-fails.patch
+${BUILD_DIR}/bin/gitaly-%-v2.35.1.gl1: override GIT_PATCHES += 0028-fetch-increase-test-coverage-of-fetches.patch
+${BUILD_DIR}/bin/gitaly-%-v2.35.1.gl1: override GIT_PATCHES += 0029-fetch-backfill-tags-before-setting-upstream.patch
+${BUILD_DIR}/bin/gitaly-%-v2.35.1.gl1: override GIT_PATCHES += 0030-fetch-control-lifecycle-of-FETCH_HEAD-in-a-single-pl.patch
+${BUILD_DIR}/bin/gitaly-%-v2.35.1.gl1: override GIT_PATCHES += 0031-fetch-report-errors-when-backfilling-tags-fails.patch
+${BUILD_DIR}/bin/gitaly-%-v2.35.1.gl1: override GIT_PATCHES += 0032-refs-add-interface-to-iterate-over-queued-transactio.patch
+${BUILD_DIR}/bin/gitaly-%-v2.35.1.gl1: override GIT_PATCHES += 0033-fetch-make-atomic-flag-cover-backfilling-of-tags.patch
+${BUILD_DIR}/bin/gitaly-%-v2.35.1.gl1: override GIT_PATCHES += 0034-fetch-make-atomic-flag-cover-pruning-of-refs.patch
+
+${BUILD_DIR}/bin/gitaly-%-v2.35.1.gl1: override GIT_VERSION = v2.35.1
+${BUILD_DIR}/bin/gitaly-%-v2.35.1.gl1: override GIT_EXTRA_VERSION = gl1
+${BUILD_DIR}/bin/gitaly-%-v2.35.1.gl1: ${DEPENDENCY_DIR}/git-v2.35.1.gl1/% | ${BUILD_DIR}/bin
+ ${Q}install $< $@
+
${INSTALL_DEST_DIR}/gitaly-%: ${BUILD_DIR}/bin/gitaly-%
${Q}mkdir -p ${@D}
${Q}install $< $@
diff --git a/_support/git-patches/0021-refs-extract-packed_refs_delete_refs-to-allow-contro.patch b/_support/git-patches/0021-refs-extract-packed_refs_delete_refs-to-allow-contro.patch
new file mode 100644
index 000000000..47dd3e41c
--- /dev/null
+++ b/_support/git-patches/0021-refs-extract-packed_refs_delete_refs-to-allow-contro.patch
@@ -0,0 +1,156 @@
+From c74f385fb46855ac0db222b6845ddb95e6a36264 Mon Sep 17 00:00:00 2001
+Message-Id: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+From: Patrick Steinhardt <ps@pks.im>
+Date: Mon, 17 Jan 2022 09:12:31 +0100
+Subject: [PATCH 21/34] refs: extract packed_refs_delete_refs() to allow
+ control of transaction
+
+When deleting loose refs, then we also have to delete the refs in the
+packed backend. This is done by calling `refs_delete_refs()`, which
+then uses the packed-backend's logic to delete refs. This doesn't allow
+us to exercise any control over the reference transaction which is being
+created in the packed backend, which is required in a subsequent commit.
+
+Extract a new function `packed_refs_delete_refs()`, which hosts most of
+the logic to delete refs except for creating the transaction itself.
+Like this, we can easily create the transaction in the files backend
+and thus exert more control over it.
+
+Signed-off-by: Patrick Steinhardt <ps@pks.im>
+Signed-off-by: Junio C Hamano <gitster@pobox.com>
+(cherry picked from commit 69840cc0f7b4f3352903bd2b8f3de7077916c26b)
+---
+ refs/files-backend.c | 13 ++++++++++---
+ refs/packed-backend.c | 26 ++++++++++++++++++++------
+ refs/packed-backend.h | 7 +++++++
+ 3 files changed, 37 insertions(+), 9 deletions(-)
+
+diff --git a/refs/files-backend.c b/refs/files-backend.c
+index 43a3b882d7..18baea4c6a 100644
+--- a/refs/files-backend.c
++++ b/refs/files-backend.c
+@@ -1244,6 +1244,7 @@ static int files_delete_refs(struct ref_store *ref_store, const char *msg,
+ {
+ struct files_ref_store *refs =
+ files_downcast(ref_store, REF_STORE_WRITE, "delete_refs");
++ struct ref_transaction *transaction = NULL;
+ struct strbuf err = STRBUF_INIT;
+ int i, result = 0;
+
+@@ -1253,10 +1254,14 @@ static int files_delete_refs(struct ref_store *ref_store, const char *msg,
+ if (packed_refs_lock(refs->packed_ref_store, 0, &err))
+ goto error;
+
+- if (refs_delete_refs(refs->packed_ref_store, msg, refnames, flags)) {
+- packed_refs_unlock(refs->packed_ref_store);
++ transaction = ref_store_transaction_begin(refs->packed_ref_store, &err);
++ if (!transaction)
++ goto error;
++
++ result = packed_refs_delete_refs(refs->packed_ref_store,
++ transaction, msg, refnames, flags);
++ if (result)
+ goto error;
+- }
+
+ packed_refs_unlock(refs->packed_ref_store);
+
+@@ -1267,6 +1272,7 @@ static int files_delete_refs(struct ref_store *ref_store, const char *msg,
+ result |= error(_("could not remove reference %s"), refname);
+ }
+
++ ref_transaction_free(transaction);
+ strbuf_release(&err);
+ return result;
+
+@@ -1283,6 +1289,7 @@ static int files_delete_refs(struct ref_store *ref_store, const char *msg,
+ else
+ error(_("could not delete references: %s"), err.buf);
+
++ ref_transaction_free(transaction);
+ strbuf_release(&err);
+ return -1;
+ }
+diff --git a/refs/packed-backend.c b/refs/packed-backend.c
+index d91a2018f6..960b43ff76 100644
+--- a/refs/packed-backend.c
++++ b/refs/packed-backend.c
+@@ -1521,15 +1521,10 @@ static int packed_initial_transaction_commit(struct ref_store *ref_store,
+ static int packed_delete_refs(struct ref_store *ref_store, const char *msg,
+ struct string_list *refnames, unsigned int flags)
+ {
+- struct packed_ref_store *refs =
+- packed_downcast(ref_store, REF_STORE_WRITE, "delete_refs");
+ struct strbuf err = STRBUF_INIT;
+ struct ref_transaction *transaction;
+- struct string_list_item *item;
+ int ret;
+
+- (void)refs; /* We need the check above, but don't use the variable */
+-
+ if (!refnames->nr)
+ return 0;
+
+@@ -1543,6 +1538,26 @@ static int packed_delete_refs(struct ref_store *ref_store, const char *msg,
+ if (!transaction)
+ return -1;
+
++ ret = packed_refs_delete_refs(ref_store, transaction,
++ msg, refnames, flags);
++
++ ref_transaction_free(transaction);
++ return ret;
++}
++
++int packed_refs_delete_refs(struct ref_store *ref_store,
++ struct ref_transaction *transaction,
++ const char *msg,
++ struct string_list *refnames,
++ unsigned int flags)
++{
++ struct strbuf err = STRBUF_INIT;
++ struct string_list_item *item;
++ int ret;
++
++ /* Assert that the ref store refers to a packed backend. */
++ packed_downcast(ref_store, REF_STORE_WRITE, "delete_refs");
++
+ for_each_string_list_item(item, refnames) {
+ if (ref_transaction_delete(transaction, item->string, NULL,
+ flags, msg, &err)) {
+@@ -1562,7 +1577,6 @@ static int packed_delete_refs(struct ref_store *ref_store, const char *msg,
+ error(_("could not delete references: %s"), err.buf);
+ }
+
+- ref_transaction_free(transaction);
+ strbuf_release(&err);
+ return ret;
+ }
+diff --git a/refs/packed-backend.h b/refs/packed-backend.h
+index 9dd8a344c3..52e0490753 100644
+--- a/refs/packed-backend.h
++++ b/refs/packed-backend.h
+@@ -3,6 +3,7 @@
+
+ struct repository;
+ struct ref_transaction;
++struct string_list;
+
+ /*
+ * Support for storing references in a `packed-refs` file.
+@@ -27,6 +28,12 @@ int packed_refs_lock(struct ref_store *ref_store, int flags, struct strbuf *err)
+ void packed_refs_unlock(struct ref_store *ref_store);
+ int packed_refs_is_locked(struct ref_store *ref_store);
+
++int packed_refs_delete_refs(struct ref_store *ref_store,
++ struct ref_transaction *transaction,
++ const char *msg,
++ struct string_list *refnames,
++ unsigned int flags);
++
+ /*
+ * Return true if `transaction` really needs to be carried out against
+ * the specified packed_ref_store, or false if it can be skipped
+--
+2.35.1
+
diff --git a/_support/git-patches/0022-refs-allow-passing-flags-when-beginning-transactions.patch b/_support/git-patches/0022-refs-allow-passing-flags-when-beginning-transactions.patch
new file mode 100644
index 000000000..8038daca2
--- /dev/null
+++ b/_support/git-patches/0022-refs-allow-passing-flags-when-beginning-transactions.patch
@@ -0,0 +1,183 @@
+From 60d0682e02db5c197c09bc4cd5a643dc2269cca2 Mon Sep 17 00:00:00 2001
+Message-Id: <60d0682e02db5c197c09bc4cd5a643dc2269cca2.1646206541.git.ps@pks.im>
+In-Reply-To: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+References: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+From: Patrick Steinhardt <ps@pks.im>
+Date: Mon, 17 Jan 2022 09:12:35 +0100
+Subject: [PATCH 22/34] refs: allow passing flags when beginning transactions
+
+We do not currently have any flags when creating reference transactions,
+but we'll add one to disable execution of the reference transaction hook
+in some cases.
+
+Allow passing flags to `ref_store_transaction_begin()` to prepare for
+this change.
+
+Signed-off-by: Patrick Steinhardt <ps@pks.im>
+Signed-off-by: Junio C Hamano <gitster@pobox.com>
+(cherry picked from commit fbe73f61cbc29f6c4a85478cf792c37dbe5aa26c)
+---
+ refs.c | 8 +++++---
+ refs.h | 3 ++-
+ refs/files-backend.c | 10 +++++-----
+ refs/packed-backend.c | 2 +-
+ refs/refs-internal.h | 1 +
+ sequencer.c | 2 +-
+ 6 files changed, 15 insertions(+), 11 deletions(-)
+
+diff --git a/refs.c b/refs.c
+index addb26293b..f498d232e5 100644
+--- a/refs.c
++++ b/refs.c
+@@ -800,7 +800,7 @@ int refs_delete_ref(struct ref_store *refs, const char *msg,
+ struct ref_transaction *transaction;
+ struct strbuf err = STRBUF_INIT;
+
+- transaction = ref_store_transaction_begin(refs, &err);
++ transaction = ref_store_transaction_begin(refs, 0, &err);
+ if (!transaction ||
+ ref_transaction_delete(transaction, refname, old_oid,
+ flags, msg, &err) ||
+@@ -1005,6 +1005,7 @@ int read_ref_at(struct ref_store *refs, const char *refname,
+ }
+
+ struct ref_transaction *ref_store_transaction_begin(struct ref_store *refs,
++ unsigned int flags,
+ struct strbuf *err)
+ {
+ struct ref_transaction *tr;
+@@ -1012,12 +1013,13 @@ struct ref_transaction *ref_store_transaction_begin(struct ref_store *refs,
+
+ CALLOC_ARRAY(tr, 1);
+ tr->ref_store = refs;
++ tr->flags = flags;
+ return tr;
+ }
+
+ struct ref_transaction *ref_transaction_begin(struct strbuf *err)
+ {
+- return ref_store_transaction_begin(get_main_ref_store(the_repository), err);
++ return ref_store_transaction_begin(get_main_ref_store(the_repository), 0, err);
+ }
+
+ void ref_transaction_free(struct ref_transaction *transaction)
+@@ -1156,7 +1158,7 @@ int refs_update_ref(struct ref_store *refs, const char *msg,
+ struct strbuf err = STRBUF_INIT;
+ int ret = 0;
+
+- t = ref_store_transaction_begin(refs, &err);
++ t = ref_store_transaction_begin(refs, 0, &err);
+ if (!t ||
+ ref_transaction_update(t, refname, new_oid, old_oid, flags, msg,
+ &err) ||
+diff --git a/refs.h b/refs.h
+index 8f91a7f9ff..3a141d7066 100644
+--- a/refs.h
++++ b/refs.h
+@@ -231,7 +231,7 @@ char *repo_default_branch_name(struct repository *r, int quiet);
+ * struct strbuf err = STRBUF_INIT;
+ * int ret = 0;
+ *
+- * transaction = ref_store_transaction_begin(refs, &err);
++ * transaction = ref_store_transaction_begin(refs, 0, &err);
+ * if (!transaction ||
+ * ref_transaction_update(...) ||
+ * ref_transaction_create(...) ||
+@@ -573,6 +573,7 @@ enum action_on_err {
+ * be freed by calling ref_transaction_free().
+ */
+ struct ref_transaction *ref_store_transaction_begin(struct ref_store *refs,
++ unsigned int flags,
+ struct strbuf *err);
+ struct ref_transaction *ref_transaction_begin(struct strbuf *err);
+
+diff --git a/refs/files-backend.c b/refs/files-backend.c
+index 18baea4c6a..758d12a0fa 100644
+--- a/refs/files-backend.c
++++ b/refs/files-backend.c
+@@ -1116,7 +1116,7 @@ static void prune_ref(struct files_ref_store *refs, struct ref_to_prune *r)
+ if (check_refname_format(r->name, 0))
+ return;
+
+- transaction = ref_store_transaction_begin(&refs->base, &err);
++ transaction = ref_store_transaction_begin(&refs->base, 0, &err);
+ if (!transaction)
+ goto cleanup;
+ ref_transaction_add_update(
+@@ -1187,7 +1187,7 @@ static int files_pack_refs(struct ref_store *ref_store, unsigned int flags)
+ struct strbuf err = STRBUF_INIT;
+ struct ref_transaction *transaction;
+
+- transaction = ref_store_transaction_begin(refs->packed_ref_store, &err);
++ transaction = ref_store_transaction_begin(refs->packed_ref_store, 0, &err);
+ if (!transaction)
+ return -1;
+
+@@ -1254,7 +1254,7 @@ static int files_delete_refs(struct ref_store *ref_store, const char *msg,
+ if (packed_refs_lock(refs->packed_ref_store, 0, &err))
+ goto error;
+
+- transaction = ref_store_transaction_begin(refs->packed_ref_store, &err);
++ transaction = ref_store_transaction_begin(refs->packed_ref_store, 0, &err);
+ if (!transaction)
+ goto error;
+
+@@ -2769,7 +2769,7 @@ static int files_transaction_prepare(struct ref_store *ref_store,
+ */
+ if (!packed_transaction) {
+ packed_transaction = ref_store_transaction_begin(
+- refs->packed_ref_store, err);
++ refs->packed_ref_store, 0, err);
+ if (!packed_transaction) {
+ ret = TRANSACTION_GENERIC_ERROR;
+ goto cleanup;
+@@ -3040,7 +3040,7 @@ static int files_initial_transaction_commit(struct ref_store *ref_store,
+ &affected_refnames))
+ BUG("initial ref transaction called with existing refs");
+
+- packed_transaction = ref_store_transaction_begin(refs->packed_ref_store, err);
++ packed_transaction = ref_store_transaction_begin(refs->packed_ref_store, 0, err);
+ if (!packed_transaction) {
+ ret = TRANSACTION_GENERIC_ERROR;
+ goto cleanup;
+diff --git a/refs/packed-backend.c b/refs/packed-backend.c
+index 960b43ff76..27dd8c3922 100644
+--- a/refs/packed-backend.c
++++ b/refs/packed-backend.c
+@@ -1534,7 +1534,7 @@ static int packed_delete_refs(struct ref_store *ref_store, const char *msg,
+ * updates into a single transaction.
+ */
+
+- transaction = ref_store_transaction_begin(ref_store, &err);
++ transaction = ref_store_transaction_begin(ref_store, 0, &err);
+ if (!transaction)
+ return -1;
+
+diff --git a/refs/refs-internal.h b/refs/refs-internal.h
+index 7ff6fba4f0..6e15db3ca4 100644
+--- a/refs/refs-internal.h
++++ b/refs/refs-internal.h
+@@ -213,6 +213,7 @@ struct ref_transaction {
+ size_t nr;
+ enum ref_transaction_state state;
+ void *backend_data;
++ unsigned int flags;
+ };
+
+ /*
+diff --git a/sequencer.c b/sequencer.c
+index 5213d16e97..5a2b609557 100644
+--- a/sequencer.c
++++ b/sequencer.c
+@@ -3588,7 +3588,7 @@ static int do_label(struct repository *r, const char *name, int len)
+ strbuf_addf(&ref_name, "refs/rewritten/%.*s", len, name);
+ strbuf_addf(&msg, "rebase (label) '%.*s'", len, name);
+
+- transaction = ref_store_transaction_begin(refs, &err);
++ transaction = ref_store_transaction_begin(refs, 0, &err);
+ if (!transaction) {
+ error("%s", err.buf);
+ ret = -1;
+--
+2.35.1
+
diff --git a/_support/git-patches/0023-refs-allow-skipping-the-reference-transaction-hook.patch b/_support/git-patches/0023-refs-allow-skipping-the-reference-transaction-hook.patch
new file mode 100644
index 000000000..cd194f8aa
--- /dev/null
+++ b/_support/git-patches/0023-refs-allow-skipping-the-reference-transaction-hook.patch
@@ -0,0 +1,60 @@
+From 4eccd16b45516df5ab02288d0c50c16e03cc44e4 Mon Sep 17 00:00:00 2001
+Message-Id: <4eccd16b45516df5ab02288d0c50c16e03cc44e4.1646206541.git.ps@pks.im>
+In-Reply-To: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+References: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+From: Patrick Steinhardt <ps@pks.im>
+Date: Mon, 17 Jan 2022 09:12:39 +0100
+Subject: [PATCH 23/34] refs: allow skipping the reference-transaction hook
+
+The reference-transaction hook is executing whenever we prepare, commit
+or abort a reference transaction. While this is mostly intentional, in
+case of the files backend we're leaking the implementation detail that
+the store is in fact a composite store with one loose and one packed
+backend to the caller. So while we want to execute the hook for all
+logical updates, executing it for such implementation details is
+unexpected.
+
+Prepare for a fix by adding a new flag which allows to skip execution of
+the hook.
+
+Signed-off-by: Patrick Steinhardt <ps@pks.im>
+Signed-off-by: Junio C Hamano <gitster@pobox.com>
+(cherry picked from commit 958fbc74e3d0fcc88b2065190e23db556a963644)
+---
+ refs.c | 3 +++
+ refs.h | 5 +++++
+ 2 files changed, 8 insertions(+)
+
+diff --git a/refs.c b/refs.c
+index f498d232e5..435b90b1ec 100644
+--- a/refs.c
++++ b/refs.c
+@@ -2084,6 +2084,9 @@ static int run_transaction_hook(struct ref_transaction *transaction,
+ const char *hook;
+ int ret = 0, i;
+
++ if (transaction->flags & REF_TRANSACTION_SKIP_HOOK)
++ return 0;
++
+ hook = find_hook("reference-transaction");
+ if (!hook)
+ return ret;
+diff --git a/refs.h b/refs.h
+index 3a141d7066..a015354fd6 100644
+--- a/refs.h
++++ b/refs.h
+@@ -568,6 +568,11 @@ enum action_on_err {
+ UPDATE_REFS_QUIET_ON_ERR
+ };
+
++/*
++ * Skip executing the reference-transaction hook.
++ */
++#define REF_TRANSACTION_SKIP_HOOK (1 << 0)
++
+ /*
+ * Begin a reference transaction. The reference transaction must
+ * be freed by calling ref_transaction_free().
+--
+2.35.1
+
diff --git a/_support/git-patches/0024-refs-demonstrate-excessive-execution-of-the-referenc.patch b/_support/git-patches/0024-refs-demonstrate-excessive-execution-of-the-referenc.patch
new file mode 100644
index 000000000..aa6d96a3e
--- /dev/null
+++ b/_support/git-patches/0024-refs-demonstrate-excessive-execution-of-the-referenc.patch
@@ -0,0 +1,97 @@
+From 079f96ed16bb70f99fbe810b1646b1709bb82871 Mon Sep 17 00:00:00 2001
+Message-Id: <079f96ed16bb70f99fbe810b1646b1709bb82871.1646206541.git.ps@pks.im>
+In-Reply-To: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+References: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+From: Patrick Steinhardt <ps@pks.im>
+Date: Mon, 17 Jan 2022 09:12:44 +0100
+Subject: [PATCH 24/34] refs: demonstrate excessive execution of the
+ reference-transaction hook
+
+Add tests which demonstate that we're executing the
+reference-transaction hook too often in some cases, which thus leaks
+implementation details about the reference store's implementation
+itself. Behaviour will be fixed in follow-up commits.
+
+Signed-off-by: Patrick Steinhardt <ps@pks.im>
+Signed-off-by: Junio C Hamano <gitster@pobox.com>
+(cherry picked from commit 2ce825436268d6409bee8ebb5f5500b7ff172b1e)
+---
+ t/t1416-ref-transaction-hooks.sh | 64 ++++++++++++++++++++++++++++++++
+ 1 file changed, 64 insertions(+)
+
+diff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh
+index 6c941027a8..0567fbdf0b 100755
+--- a/t/t1416-ref-transaction-hooks.sh
++++ b/t/t1416-ref-transaction-hooks.sh
+@@ -136,4 +136,68 @@ test_expect_success 'interleaving hook calls succeed' '
+ test_cmp expect target-repo.git/actual
+ '
+
++test_expect_success 'hook does not get called on packing refs' '
++ # Pack references first such that we are in a known state.
++ git pack-refs --all &&
++
++ write_script .git/hooks/reference-transaction <<-\EOF &&
++ echo "$@" >>actual
++ cat >>actual
++ EOF
++ rm -f actual &&
++
++ git update-ref refs/heads/unpacked-ref $POST_OID &&
++ git pack-refs --all &&
++
++ # We only expect a single hook invocation, which is the call to
++ # git-update-ref(1). But currently, packing refs will also trigger the
++ # hook.
++ cat >expect <<-EOF &&
++ prepared
++ $ZERO_OID $POST_OID refs/heads/unpacked-ref
++ committed
++ $ZERO_OID $POST_OID refs/heads/unpacked-ref
++ prepared
++ $ZERO_OID $POST_OID refs/heads/unpacked-ref
++ committed
++ $ZERO_OID $POST_OID refs/heads/unpacked-ref
++ prepared
++ $POST_OID $ZERO_OID refs/heads/unpacked-ref
++ committed
++ $POST_OID $ZERO_OID refs/heads/unpacked-ref
++ EOF
++
++ test_cmp expect actual
++'
++
++test_expect_success 'deleting packed ref calls hook once' '
++ # Create a reference and pack it.
++ git update-ref refs/heads/to-be-deleted $POST_OID &&
++ git pack-refs --all &&
++
++ write_script .git/hooks/reference-transaction <<-\EOF &&
++ echo "$@" >>actual
++ cat >>actual
++ EOF
++ rm -f actual &&
++
++ git update-ref -d refs/heads/to-be-deleted $POST_OID &&
++
++ # We only expect a single hook invocation, which is the logical
++ # deletion. But currently, we see two interleaving transactions, once
++ # for deleting the loose refs and once for deleting the packed ref.
++ cat >expect <<-EOF &&
++ prepared
++ $ZERO_OID $ZERO_OID refs/heads/to-be-deleted
++ prepared
++ $POST_OID $ZERO_OID refs/heads/to-be-deleted
++ committed
++ $ZERO_OID $ZERO_OID refs/heads/to-be-deleted
++ committed
++ $POST_OID $ZERO_OID refs/heads/to-be-deleted
++ EOF
++
++ test_cmp expect actual
++'
++
+ test_done
+--
+2.35.1
+
diff --git a/_support/git-patches/0025-refs-do-not-execute-reference-transaction-hook-on-pa.patch b/_support/git-patches/0025-refs-do-not-execute-reference-transaction-hook-on-pa.patch
new file mode 100644
index 000000000..f2e7c06ed
--- /dev/null
+++ b/_support/git-patches/0025-refs-do-not-execute-reference-transaction-hook-on-pa.patch
@@ -0,0 +1,81 @@
+From ef35ab926309bf406d3871679c576c718708a93b Mon Sep 17 00:00:00 2001
+Message-Id: <ef35ab926309bf406d3871679c576c718708a93b.1646206541.git.ps@pks.im>
+In-Reply-To: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+References: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+From: Patrick Steinhardt <ps@pks.im>
+Date: Mon, 17 Jan 2022 09:12:48 +0100
+Subject: [PATCH 25/34] refs: do not execute reference-transaction hook on
+ packing refs
+
+The reference-transaction hook is supposed to track logical changes to
+references, but it currently also gets executed when packing refs in a
+repository. This is unexpected and ultimately not all that useful:
+packing refs is not supposed to result in any user-visible change to the
+refs' state, and it ultimately is an implementation detail of how refs
+stores work.
+
+Fix this excessive execution of the hook when packing refs.
+
+Reported-by: Waleed Khan <me@waleedkhan.name>
+Signed-off-by: Patrick Steinhardt <ps@pks.im>
+Signed-off-by: Junio C Hamano <gitster@pobox.com>
+(cherry picked from commit ffad9941383465553bf26d88050f3243726f30df)
+---
+ refs/files-backend.c | 6 ++++--
+ t/t1416-ref-transaction-hooks.sh | 11 +----------
+ 2 files changed, 5 insertions(+), 12 deletions(-)
+
+diff --git a/refs/files-backend.c b/refs/files-backend.c
+index 758d12a0fa..19f43e8d29 100644
+--- a/refs/files-backend.c
++++ b/refs/files-backend.c
+@@ -1116,7 +1116,8 @@ static void prune_ref(struct files_ref_store *refs, struct ref_to_prune *r)
+ if (check_refname_format(r->name, 0))
+ return;
+
+- transaction = ref_store_transaction_begin(&refs->base, 0, &err);
++ transaction = ref_store_transaction_begin(&refs->base,
++ REF_TRANSACTION_SKIP_HOOK, &err);
+ if (!transaction)
+ goto cleanup;
+ ref_transaction_add_update(
+@@ -1187,7 +1188,8 @@ static int files_pack_refs(struct ref_store *ref_store, unsigned int flags)
+ struct strbuf err = STRBUF_INIT;
+ struct ref_transaction *transaction;
+
+- transaction = ref_store_transaction_begin(refs->packed_ref_store, 0, &err);
++ transaction = ref_store_transaction_begin(refs->packed_ref_store,
++ REF_TRANSACTION_SKIP_HOOK, &err);
+ if (!transaction)
+ return -1;
+
+diff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh
+index 0567fbdf0b..f9d3d5213f 100755
+--- a/t/t1416-ref-transaction-hooks.sh
++++ b/t/t1416-ref-transaction-hooks.sh
+@@ -150,21 +150,12 @@ test_expect_success 'hook does not get called on packing refs' '
+ git pack-refs --all &&
+
+ # We only expect a single hook invocation, which is the call to
+- # git-update-ref(1). But currently, packing refs will also trigger the
+- # hook.
++ # git-update-ref(1).
+ cat >expect <<-EOF &&
+ prepared
+ $ZERO_OID $POST_OID refs/heads/unpacked-ref
+ committed
+ $ZERO_OID $POST_OID refs/heads/unpacked-ref
+- prepared
+- $ZERO_OID $POST_OID refs/heads/unpacked-ref
+- committed
+- $ZERO_OID $POST_OID refs/heads/unpacked-ref
+- prepared
+- $POST_OID $ZERO_OID refs/heads/unpacked-ref
+- committed
+- $POST_OID $ZERO_OID refs/heads/unpacked-ref
+ EOF
+
+ test_cmp expect actual
+--
+2.35.1
+
diff --git a/_support/git-patches/0026-refs-skip-hooks-when-deleting-uncovered-packed-refs.patch b/_support/git-patches/0026-refs-skip-hooks-when-deleting-uncovered-packed-refs.patch
new file mode 100644
index 000000000..3b21bf489
--- /dev/null
+++ b/_support/git-patches/0026-refs-skip-hooks-when-deleting-uncovered-packed-refs.patch
@@ -0,0 +1,99 @@
+From d56f2a0e1eed4d0a39b99638117e0c8259e5078d Mon Sep 17 00:00:00 2001
+Message-Id: <d56f2a0e1eed4d0a39b99638117e0c8259e5078d.1646206542.git.ps@pks.im>
+In-Reply-To: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+References: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+From: Patrick Steinhardt <ps@pks.im>
+Date: Mon, 17 Jan 2022 09:12:53 +0100
+Subject: [PATCH 26/34] refs: skip hooks when deleting uncovered packed refs
+
+When deleting refs from the loose-files refs backend, then we need to be
+careful to also delete the same ref from the packed refs backend, if it
+exists. If we don't, then deleting the loose ref would "uncover" the
+packed ref. We thus always have to queue up deletions of refs for both
+the loose and the packed refs backend. This is done in two separate
+transactions, where the end result is that the reference-transaction
+hook is executed twice for the deleted refs.
+
+This behaviour is quite misleading: it's exposing implementation details
+of how the files backend works to the user, in contrast to the logical
+updates that we'd really want to expose via the hook. Worse yet, whether
+the hook gets executed once or twice depends on how well-packed the
+repository is: if the ref only exists as a loose ref, then we execute it
+once, otherwise if it is also packed then we execute it twice.
+
+Fix this behaviour and don't execute the reference-transaction hook at
+all when refs in the packed-refs backend if it's driven by the files
+backend. This works as expected even in case the refs to be deleted only
+exist in the packed-refs backend because the loose-backend always queues
+refs in its own transaction even if they don't exist such that they can
+be locked for concurrent creation. And it also does the right thing in
+case neither of the backends has the ref because that would cause the
+transaction to fail completely.
+
+Signed-off-by: Patrick Steinhardt <ps@pks.im>
+Signed-off-by: Junio C Hamano <gitster@pobox.com>
+(cherry picked from commit 2ed1b64ebdeefc7f9473ae159fb45ff0c6cf121a)
+---
+ refs/files-backend.c | 9 ++++++---
+ t/t1416-ref-transaction-hooks.sh | 7 +------
+ 2 files changed, 7 insertions(+), 9 deletions(-)
+
+diff --git a/refs/files-backend.c b/refs/files-backend.c
+index 19f43e8d29..c931dd479c 100644
+--- a/refs/files-backend.c
++++ b/refs/files-backend.c
+@@ -1256,7 +1256,8 @@ static int files_delete_refs(struct ref_store *ref_store, const char *msg,
+ if (packed_refs_lock(refs->packed_ref_store, 0, &err))
+ goto error;
+
+- transaction = ref_store_transaction_begin(refs->packed_ref_store, 0, &err);
++ transaction = ref_store_transaction_begin(refs->packed_ref_store,
++ REF_TRANSACTION_SKIP_HOOK, &err);
+ if (!transaction)
+ goto error;
+
+@@ -2771,7 +2772,8 @@ static int files_transaction_prepare(struct ref_store *ref_store,
+ */
+ if (!packed_transaction) {
+ packed_transaction = ref_store_transaction_begin(
+- refs->packed_ref_store, 0, err);
++ refs->packed_ref_store,
++ REF_TRANSACTION_SKIP_HOOK, err);
+ if (!packed_transaction) {
+ ret = TRANSACTION_GENERIC_ERROR;
+ goto cleanup;
+@@ -3042,7 +3044,8 @@ static int files_initial_transaction_commit(struct ref_store *ref_store,
+ &affected_refnames))
+ BUG("initial ref transaction called with existing refs");
+
+- packed_transaction = ref_store_transaction_begin(refs->packed_ref_store, 0, err);
++ packed_transaction = ref_store_transaction_begin(refs->packed_ref_store,
++ REF_TRANSACTION_SKIP_HOOK, err);
+ if (!packed_transaction) {
+ ret = TRANSACTION_GENERIC_ERROR;
+ goto cleanup;
+diff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh
+index f9d3d5213f..4e1e84a91f 100755
+--- a/t/t1416-ref-transaction-hooks.sh
++++ b/t/t1416-ref-transaction-hooks.sh
+@@ -175,16 +175,11 @@ test_expect_success 'deleting packed ref calls hook once' '
+ git update-ref -d refs/heads/to-be-deleted $POST_OID &&
+
+ # We only expect a single hook invocation, which is the logical
+- # deletion. But currently, we see two interleaving transactions, once
+- # for deleting the loose refs and once for deleting the packed ref.
++ # deletion.
+ cat >expect <<-EOF &&
+- prepared
+- $ZERO_OID $ZERO_OID refs/heads/to-be-deleted
+ prepared
+ $POST_OID $ZERO_OID refs/heads/to-be-deleted
+ committed
+- $ZERO_OID $ZERO_OID refs/heads/to-be-deleted
+- committed
+ $POST_OID $ZERO_OID refs/heads/to-be-deleted
+ EOF
+
+--
+2.35.1
+
diff --git a/_support/git-patches/0027-fetch-prune-exit-with-error-if-pruning-fails.patch b/_support/git-patches/0027-fetch-prune-exit-with-error-if-pruning-fails.patch
new file mode 100644
index 000000000..a6a75c054
--- /dev/null
+++ b/_support/git-patches/0027-fetch-prune-exit-with-error-if-pruning-fails.patch
@@ -0,0 +1,74 @@
+From 05bb5009a6bded848b30bb82cc02e6bcb90a8fc3 Mon Sep 17 00:00:00 2001
+Message-Id: <05bb5009a6bded848b30bb82cc02e6bcb90a8fc3.1646206542.git.ps@pks.im>
+In-Reply-To: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+References: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+From: Thomas Gummerer <t.gummerer@gmail.com>
+Date: Mon, 31 Jan 2022 13:30:47 +0000
+Subject: [PATCH 27/34] fetch --prune: exit with error if pruning fails
+
+When pruning refs fails, we print an error to stderr, but still
+exit 0 from 'git fetch'. Since this is a genuine error, fetch
+should be exiting with some non-zero exit code. Make it so.
+
+The --prune option was introduced in f360d844de ("builtin-fetch: add
+--prune option", 2009-11-10). Unfortunately it's unclear from that
+commit whether ignoring the exit code was an oversight or
+intentional, but it feels like an oversight.
+
+Helped-by: Johannes Schindelin <johannes.schindelin@gmx.de>
+Signed-off-by: Thomas Gummerer <t.gummerer@gmail.com>
+Signed-off-by: Junio C Hamano <gitster@pobox.com>
+(cherry picked from commit c9e04d905edb5487c43b03304704e8d1248f9ac0)
+---
+ builtin/fetch.c | 10 ++++++----
+ t/t5510-fetch.sh | 11 +++++++++++
+ 2 files changed, 17 insertions(+), 4 deletions(-)
+
+diff --git a/builtin/fetch.c b/builtin/fetch.c
+index ebbde5d56d..f5a64c7351 100644
+--- a/builtin/fetch.c
++++ b/builtin/fetch.c
+@@ -1613,12 +1613,14 @@ static int do_fetch(struct transport *transport,
+ * don't care whether --tags was specified.
+ */
+ if (rs->nr) {
+- prune_refs(rs, ref_map, transport->url);
++ retcode = prune_refs(rs, ref_map, transport->url);
+ } else {
+- prune_refs(&transport->remote->fetch,
+- ref_map,
+- transport->url);
++ retcode = prune_refs(&transport->remote->fetch,
++ ref_map,
++ transport->url);
+ }
++ if (retcode != 0)
++ retcode = 1;
+ }
+ if (fetch_and_consume_refs(transport, ref_map, worktrees)) {
+ free_refs(ref_map);
+diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
+index 20f7110ec1..ef0da0a63b 100755
+--- a/t/t5510-fetch.sh
++++ b/t/t5510-fetch.sh
+@@ -164,6 +164,17 @@ test_expect_success 'fetch --prune --tags with refspec prunes based on refspec'
+ git rev-parse sometag
+ '
+
++test_expect_success REFFILES 'fetch --prune fails to delete branches' '
++ cd "$D" &&
++ git clone . prune-fail &&
++ cd prune-fail &&
++ git update-ref refs/remotes/origin/extrabranch main &&
++ : this will prevent --prune from locking packed-refs for deleting refs, but adding loose refs still succeeds &&
++ >.git/packed-refs.new &&
++
++ test_must_fail git fetch --prune origin
++'
++
+ test_expect_success 'fetch --atomic works with a single branch' '
+ test_when_finished "rm -rf \"$D\"/atomic" &&
+
+--
+2.35.1
+
diff --git a/_support/git-patches/0028-fetch-increase-test-coverage-of-fetches.patch b/_support/git-patches/0028-fetch-increase-test-coverage-of-fetches.patch
new file mode 100644
index 000000000..412a908ef
--- /dev/null
+++ b/_support/git-patches/0028-fetch-increase-test-coverage-of-fetches.patch
@@ -0,0 +1,169 @@
+From e6b86c1dcd01137afe71fe2c303efcf587b01abf Mon Sep 17 00:00:00 2001
+Message-Id: <e6b86c1dcd01137afe71fe2c303efcf587b01abf.1646206542.git.ps@pks.im>
+In-Reply-To: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+References: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+From: Patrick Steinhardt <ps@pks.im>
+Date: Thu, 17 Feb 2022 14:04:16 +0100
+Subject: [PATCH 28/34] fetch: increase test coverage of fetches
+
+When using git-fetch(1) with the `--atomic` flag the expectation is that
+either all of the references are updated, or alternatively none are in
+case the fetch fails. While we already have tests for this, we do not
+have any tests which exercise atomicity either when pruning deleted refs
+or when backfilling tags. This gap in test coverage hides that we indeed
+don't handle atomicity correctly for both of these cases.
+
+Add test cases which cover these testing gaps to demonstrate the broken
+behaviour. Note that tests are not marked as `test_expect_failure`: this
+is done to explicitly demonstrate the current known-wrong behaviour, and
+they will be fixed up as soon as we fix the underlying bugs.
+
+While at it this commit also adds another test case which demonstrates
+that backfilling of tags does not return an error code in case the
+backfill fails. This bug will also be fixed by a subsequent commit.
+
+Signed-off-by: Patrick Steinhardt <ps@pks.im>
+Signed-off-by: Junio C Hamano <gitster@pobox.com>
+(cherry picked from commit 2a0cafd464709cfa22fe7249290c644a2a26c520)
+---
+ t/t5503-tagfollow.sh | 81 ++++++++++++++++++++++++++++++++++++++++++++
+ t/t5510-fetch.sh | 33 ++++++++++++++++++
+ 2 files changed, 114 insertions(+)
+
+diff --git a/t/t5503-tagfollow.sh b/t/t5503-tagfollow.sh
+index 195fc64dd4..6ffe2a5719 100755
+--- a/t/t5503-tagfollow.sh
++++ b/t/t5503-tagfollow.sh
+@@ -160,4 +160,85 @@ test_expect_success 'new clone fetch main and tags' '
+ test_cmp expect actual
+ '
+
++test_expect_success 'atomic fetch with failing backfill' '
++ git init clone3 &&
++
++ # We want to test whether a failure when backfilling tags correctly
++ # aborts the complete transaction when `--atomic` is passed: we should
++ # neither create the branch nor should we create the tag when either
++ # one of both fails to update correctly.
++ #
++ # To trigger failure we simply abort when backfilling a tag.
++ write_script clone3/.git/hooks/reference-transaction <<-\EOF &&
++ while read oldrev newrev reference
++ do
++ if test "$reference" = refs/tags/tag1
++ then
++ exit 1
++ fi
++ done
++ EOF
++
++ test_must_fail git -C clone3 fetch --atomic .. $B:refs/heads/something &&
++
++ # Creation of the tag has failed, so ideally refs/heads/something
++ # should not exist. The fact that it does demonstrates that there is
++ # a bug in the `--atomic` flag.
++ test $B = "$(git -C clone3 rev-parse --verify refs/heads/something)"
++'
++
++test_expect_success 'atomic fetch with backfill should use single transaction' '
++ git init clone4 &&
++
++ # Fetching with the `--atomic` flag should update all references in a
++ # single transaction, including backfilled tags. We thus expect to see
++ # a single reference transaction for the created branch and tags.
++ cat >expected <<-EOF &&
++ prepared
++ $ZERO_OID $B refs/heads/something
++ $ZERO_OID $S refs/tags/tag2
++ committed
++ $ZERO_OID $B refs/heads/something
++ $ZERO_OID $S refs/tags/tag2
++ prepared
++ $ZERO_OID $T refs/tags/tag1
++ committed
++ $ZERO_OID $T refs/tags/tag1
++ EOF
++
++ write_script clone4/.git/hooks/reference-transaction <<-\EOF &&
++ ( echo "$*" && cat ) >>actual
++ EOF
++
++ git -C clone4 fetch --atomic .. $B:refs/heads/something &&
++ test_cmp expected clone4/actual
++'
++
++test_expect_success 'backfill failure causes command to fail' '
++ git init clone5 &&
++
++ write_script clone5/.git/hooks/reference-transaction <<-EOF &&
++ while read oldrev newrev reference
++ do
++ if test "\$reference" = refs/tags/tag1
++ then
++ # Create a nested tag below the actual tag we
++ # wanted to write, which causes a D/F conflict
++ # later when we want to commit refs/tags/tag1.
++ # We cannot just `exit 1` here given that this
++ # would cause us to die immediately.
++ git update-ref refs/tags/tag1/nested $B
++ exit \$!
++ fi
++ done
++ EOF
++
++ # Even though we fail to create refs/tags/tag1 the below command
++ # unexpectedly succeeds.
++ git -C clone5 fetch .. $B:refs/heads/something &&
++ test $B = $(git -C clone5 rev-parse --verify refs/heads/something) &&
++ test $S = $(git -C clone5 rev-parse --verify tag2) &&
++ test_must_fail git -C clone5 rev-parse --verify tag1
++'
++
+ test_done
+diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
+index ef0da0a63b..70d51f343b 100755
+--- a/t/t5510-fetch.sh
++++ b/t/t5510-fetch.sh
+@@ -343,6 +343,39 @@ test_expect_success 'fetch --atomic --append appends to FETCH_HEAD' '
+ test_cmp expected atomic/.git/FETCH_HEAD
+ '
+
++test_expect_success 'fetch --atomic --prune executes a single reference transaction only' '
++ test_when_finished "rm -rf \"$D\"/atomic" &&
++
++ cd "$D" &&
++ git branch scheduled-for-deletion &&
++ git clone . atomic &&
++ git branch -D scheduled-for-deletion &&
++ git branch new-branch &&
++ head_oid=$(git rev-parse HEAD) &&
++
++ # Fetching with the `--atomic` flag should update all references in a
++ # single transaction. It is currently missing coverage of pruned
++ # references though, and as a result those may be committed to disk
++ # even if updating references fails later.
++ cat >expected <<-EOF &&
++ prepared
++ $ZERO_OID $ZERO_OID refs/remotes/origin/scheduled-for-deletion
++ committed
++ $ZERO_OID $ZERO_OID refs/remotes/origin/scheduled-for-deletion
++ prepared
++ $ZERO_OID $head_oid refs/remotes/origin/new-branch
++ committed
++ $ZERO_OID $head_oid refs/remotes/origin/new-branch
++ EOF
++
++ write_script atomic/.git/hooks/reference-transaction <<-\EOF &&
++ ( echo "$*" && cat ) >>actual
++ EOF
++
++ git -C atomic fetch --atomic --prune origin &&
++ test_cmp expected atomic/actual
++'
++
+ test_expect_success '--refmap="" ignores configured refspec' '
+ cd "$TRASH_DIRECTORY" &&
+ git clone "$D" remote-refs &&
+--
+2.35.1
+
diff --git a/_support/git-patches/0029-fetch-backfill-tags-before-setting-upstream.patch b/_support/git-patches/0029-fetch-backfill-tags-before-setting-upstream.patch
new file mode 100644
index 000000000..360bd10c2
--- /dev/null
+++ b/_support/git-patches/0029-fetch-backfill-tags-before-setting-upstream.patch
@@ -0,0 +1,116 @@
+From 6bb95bf1ec7cb06eca5161fe7dcf02cfabd8de83 Mon Sep 17 00:00:00 2001
+Message-Id: <6bb95bf1ec7cb06eca5161fe7dcf02cfabd8de83.1646206542.git.ps@pks.im>
+In-Reply-To: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+References: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+From: Patrick Steinhardt <ps@pks.im>
+Date: Thu, 17 Feb 2022 14:04:20 +0100
+Subject: [PATCH 29/34] fetch: backfill tags before setting upstream
+
+The fetch code flow is a bit hard to understand right now:
+
+ 1. We optionally prune all references which have vanished on the
+ remote side.
+ 2. We fetch and update all other references locally.
+ 3. We update the upstream branch in the gitconfig.
+ 4. We backfill tags pointing into the history we have just fetched.
+
+It is quite confusing that we fetch objects and update references in
+both (2) and (4), which is further stressed by the point that we use a
+`skip` goto label to jump from (3) to (4) in case we fail to update the
+gitconfig as expected.
+
+Reorder the code to first update all local references, and only after we
+have done so update the upstream branch information. This improves the
+code flow and furthermore makes it easier to refactor the way we update
+references together.
+
+Signed-off-by: Patrick Steinhardt <ps@pks.im>
+Signed-off-by: Junio C Hamano <gitster@pobox.com>
+(cherry picked from commit efbade066083eb0a8ccee5a8290cd3fc834705f3)
+---
+ builtin/fetch.c | 35 ++++++++++++++++++-----------------
+ 1 file changed, 18 insertions(+), 17 deletions(-)
+
+diff --git a/builtin/fetch.c b/builtin/fetch.c
+index f5a64c7351..4ae1afb918 100644
+--- a/builtin/fetch.c
++++ b/builtin/fetch.c
+@@ -1539,7 +1539,7 @@ static void backfill_tags(struct transport *transport, struct ref *ref_map,
+ static int do_fetch(struct transport *transport,
+ struct refspec *rs)
+ {
+- struct ref *ref_map;
++ struct ref *ref_map = NULL;
+ int autotags = (transport->remote->fetch_tags == 1);
+ int retcode = 0;
+ const struct ref *remote_refs;
+@@ -1623,11 +1623,24 @@ static int do_fetch(struct transport *transport,
+ retcode = 1;
+ }
+ if (fetch_and_consume_refs(transport, ref_map, worktrees)) {
+- free_refs(ref_map);
+ retcode = 1;
+ goto cleanup;
+ }
+
++ /*
++ * If neither --no-tags nor --tags was specified, do automated tag
++ * following.
++ */
++ if (tags == TAGS_DEFAULT && autotags) {
++ struct ref *tags_ref_map = NULL, **tail = &tags_ref_map;
++
++ find_non_local_tags(remote_refs, &tags_ref_map, &tail);
++ if (tags_ref_map)
++ backfill_tags(transport, tags_ref_map, worktrees);
++
++ free_refs(tags_ref_map);
++ }
++
+ if (set_upstream) {
+ struct branch *branch = branch_get("HEAD");
+ struct ref *rm;
+@@ -1647,7 +1660,7 @@ static int do_fetch(struct transport *transport,
+ if (!rm->peer_ref) {
+ if (source_ref) {
+ warning(_("multiple branches detected, incompatible with --set-upstream"));
+- goto skip;
++ goto cleanup;
+ } else {
+ source_ref = rm;
+ }
+@@ -1661,7 +1674,7 @@ static int do_fetch(struct transport *transport,
+ warning(_("could not set upstream of HEAD to '%s' from '%s' when "
+ "it does not point to any branch."),
+ shortname, transport->remote->name);
+- goto skip;
++ goto cleanup;
+ }
+
+ if (!strcmp(source_ref->name, "HEAD") ||
+@@ -1681,21 +1694,9 @@ static int do_fetch(struct transport *transport,
+ "you need to specify exactly one branch with the --set-upstream option"));
+ }
+ }
+-skip:
+- free_refs(ref_map);
+-
+- /* if neither --no-tags nor --tags was specified, do automated tag
+- * following ... */
+- if (tags == TAGS_DEFAULT && autotags) {
+- struct ref **tail = &ref_map;
+- ref_map = NULL;
+- find_non_local_tags(remote_refs, &ref_map, &tail);
+- if (ref_map)
+- backfill_tags(transport, ref_map, worktrees);
+- free_refs(ref_map);
+- }
+
+ cleanup:
++ free_refs(ref_map);
+ free_worktrees(worktrees);
+ return retcode;
+ }
+--
+2.35.1
+
diff --git a/_support/git-patches/0030-fetch-control-lifecycle-of-FETCH_HEAD-in-a-single-pl.patch b/_support/git-patches/0030-fetch-control-lifecycle-of-FETCH_HEAD-in-a-single-pl.patch
new file mode 100644
index 000000000..dc9d1364a
--- /dev/null
+++ b/_support/git-patches/0030-fetch-control-lifecycle-of-FETCH_HEAD-in-a-single-pl.patch
@@ -0,0 +1,170 @@
+From ded9df2b091da730b28e0aa20b667f3ee2ccaffa Mon Sep 17 00:00:00 2001
+Message-Id: <ded9df2b091da730b28e0aa20b667f3ee2ccaffa.1646206542.git.ps@pks.im>
+In-Reply-To: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+References: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+From: Patrick Steinhardt <ps@pks.im>
+Date: Thu, 17 Feb 2022 14:04:24 +0100
+Subject: [PATCH 30/34] fetch: control lifecycle of FETCH_HEAD in a single
+ place
+
+There are two different locations where we're appending to FETCH_HEAD:
+first when storing updated references, and second when backfilling tags.
+Both times we open the file, append to it and then commit it into place,
+which is essentially duplicate work.
+
+Improve the lifecycle of updating FETCH_HEAD by opening and committing
+it once in `do_fetch()`, where we pass the structure down to the code
+which wants to append to it.
+
+Signed-off-by: Patrick Steinhardt <ps@pks.im>
+Signed-off-by: Junio C Hamano <gitster@pobox.com>
+(cherry picked from commit 2983cec0f26b7409ccc2dd5710b40ff4809cd4b1)
+---
+ builtin/fetch.c | 35 +++++++++++++++++++----------------
+ 1 file changed, 19 insertions(+), 16 deletions(-)
+
+diff --git a/builtin/fetch.c b/builtin/fetch.c
+index 4ae1afb918..897fa247d9 100644
+--- a/builtin/fetch.c
++++ b/builtin/fetch.c
+@@ -1083,9 +1083,8 @@ N_("it took %.2f seconds to check forced updates; you can use\n"
+
+ static int store_updated_refs(const char *raw_url, const char *remote_name,
+ int connectivity_checked, struct ref *ref_map,
+- struct worktree **worktrees)
++ struct fetch_head *fetch_head, struct worktree **worktrees)
+ {
+- struct fetch_head fetch_head;
+ int url_len, i, rc = 0;
+ struct strbuf note = STRBUF_INIT, err = STRBUF_INIT;
+ struct ref_transaction *transaction = NULL;
+@@ -1095,10 +1094,6 @@ static int store_updated_refs(const char *raw_url, const char *remote_name,
+ int want_status;
+ int summary_width = 0;
+
+- rc = open_fetch_head(&fetch_head);
+- if (rc)
+- return -1;
+-
+ if (verbosity >= 0)
+ summary_width = transport_summary_width(ref_map);
+
+@@ -1208,7 +1203,7 @@ static int store_updated_refs(const char *raw_url, const char *remote_name,
+ strbuf_addf(&note, "'%s' of ", what);
+ }
+
+- append_fetch_head(&fetch_head, &rm->old_oid,
++ append_fetch_head(fetch_head, &rm->old_oid,
+ rm->fetch_head_status,
+ note.buf, url, url_len);
+
+@@ -1248,9 +1243,6 @@ static int store_updated_refs(const char *raw_url, const char *remote_name,
+ }
+ }
+
+- if (!rc)
+- commit_fetch_head(&fetch_head);
+-
+ if (rc & STORE_REF_ERROR_DF_CONFLICT)
+ error(_("some local refs could not be updated; try running\n"
+ " 'git remote prune %s' to remove any old, conflicting "
+@@ -1270,7 +1262,6 @@ static int store_updated_refs(const char *raw_url, const char *remote_name,
+ strbuf_release(&err);
+ ref_transaction_free(transaction);
+ free(url);
+- close_fetch_head(&fetch_head);
+ return rc;
+ }
+
+@@ -1311,6 +1302,7 @@ static int check_exist_and_connected(struct ref *ref_map)
+
+ static int fetch_and_consume_refs(struct transport *transport,
+ struct ref *ref_map,
++ struct fetch_head *fetch_head,
+ struct worktree **worktrees)
+ {
+ int connectivity_checked = 1;
+@@ -1333,7 +1325,7 @@ static int fetch_and_consume_refs(struct transport *transport,
+
+ trace2_region_enter("fetch", "consume_refs", the_repository);
+ ret = store_updated_refs(transport->url, transport->remote->name,
+- connectivity_checked, ref_map, worktrees);
++ connectivity_checked, ref_map, fetch_head, worktrees);
+ trace2_region_leave("fetch", "consume_refs", the_repository);
+
+ out:
+@@ -1506,7 +1498,9 @@ static struct transport *prepare_transport(struct remote *remote, int deepen)
+ return transport;
+ }
+
+-static void backfill_tags(struct transport *transport, struct ref *ref_map,
++static void backfill_tags(struct transport *transport,
++ struct ref *ref_map,
++ struct fetch_head *fetch_head,
+ struct worktree **worktrees)
+ {
+ int cannot_reuse;
+@@ -1528,7 +1522,7 @@ static void backfill_tags(struct transport *transport, struct ref *ref_map,
+ transport_set_option(transport, TRANS_OPT_FOLLOWTAGS, NULL);
+ transport_set_option(transport, TRANS_OPT_DEPTH, "0");
+ transport_set_option(transport, TRANS_OPT_DEEPEN_RELATIVE, NULL);
+- fetch_and_consume_refs(transport, ref_map, worktrees);
++ fetch_and_consume_refs(transport, ref_map, fetch_head, worktrees);
+
+ if (gsecondary) {
+ transport_disconnect(gsecondary);
+@@ -1547,6 +1541,7 @@ static int do_fetch(struct transport *transport,
+ TRANSPORT_LS_REFS_OPTIONS_INIT;
+ int must_list_refs = 1;
+ struct worktree **worktrees = get_worktrees();
++ struct fetch_head fetch_head = { 0 };
+
+ if (tags == TAGS_DEFAULT) {
+ if (transport->remote->fetch_tags == 2)
+@@ -1604,6 +1599,10 @@ static int do_fetch(struct transport *transport,
+ if (!update_head_ok)
+ check_not_current_branch(ref_map, worktrees);
+
++ retcode = open_fetch_head(&fetch_head);
++ if (retcode)
++ goto cleanup;
++
+ if (tags == TAGS_DEFAULT && autotags)
+ transport_set_option(transport, TRANS_OPT_FOLLOWTAGS, "1");
+ if (prune) {
+@@ -1622,7 +1621,8 @@ static int do_fetch(struct transport *transport,
+ if (retcode != 0)
+ retcode = 1;
+ }
+- if (fetch_and_consume_refs(transport, ref_map, worktrees)) {
++
++ if (fetch_and_consume_refs(transport, ref_map, &fetch_head, worktrees)) {
+ retcode = 1;
+ goto cleanup;
+ }
+@@ -1636,11 +1636,13 @@ static int do_fetch(struct transport *transport,
+
+ find_non_local_tags(remote_refs, &tags_ref_map, &tail);
+ if (tags_ref_map)
+- backfill_tags(transport, tags_ref_map, worktrees);
++ backfill_tags(transport, tags_ref_map, &fetch_head, worktrees);
+
+ free_refs(tags_ref_map);
+ }
+
++ commit_fetch_head(&fetch_head);
++
+ if (set_upstream) {
+ struct branch *branch = branch_get("HEAD");
+ struct ref *rm;
+@@ -1696,6 +1698,7 @@ static int do_fetch(struct transport *transport,
+ }
+
+ cleanup:
++ close_fetch_head(&fetch_head);
+ free_refs(ref_map);
+ free_worktrees(worktrees);
+ return retcode;
+--
+2.35.1
+
diff --git a/_support/git-patches/0031-fetch-report-errors-when-backfilling-tags-fails.patch b/_support/git-patches/0031-fetch-report-errors-when-backfilling-tags-fails.patch
new file mode 100644
index 000000000..a2cfd7b3e
--- /dev/null
+++ b/_support/git-patches/0031-fetch-report-errors-when-backfilling-tags-fails.patch
@@ -0,0 +1,100 @@
+From ab0fa78af6741c409979889d7596042eb03192b5 Mon Sep 17 00:00:00 2001
+Message-Id: <ab0fa78af6741c409979889d7596042eb03192b5.1646206542.git.ps@pks.im>
+In-Reply-To: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+References: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+From: Patrick Steinhardt <ps@pks.im>
+Date: Thu, 17 Feb 2022 14:04:28 +0100
+Subject: [PATCH 31/34] fetch: report errors when backfilling tags fails
+
+When the backfilling of tags fails we do not report this error to the
+caller, but only report it implicitly at a later point when reporting
+updated references. This leaves callers unable to act upon the
+information of whether the backfilling succeeded or not.
+
+Refactor the function to return an error code and pass it up the
+callstack. This causes us to correctly propagate the error back to the
+user of git-fetch(1).
+
+Signed-off-by: Patrick Steinhardt <ps@pks.im>
+Signed-off-by: Junio C Hamano <gitster@pobox.com>
+(cherry picked from commit 62091b4c87a199c172556f15c5662c6c3679e9cd)
+---
+ builtin/fetch.c | 26 ++++++++++++++++++--------
+ t/t5503-tagfollow.sh | 4 +---
+ 2 files changed, 19 insertions(+), 11 deletions(-)
+
+diff --git a/builtin/fetch.c b/builtin/fetch.c
+index 897fa247d9..2d0ffb9859 100644
+--- a/builtin/fetch.c
++++ b/builtin/fetch.c
+@@ -1498,12 +1498,12 @@ static struct transport *prepare_transport(struct remote *remote, int deepen)
+ return transport;
+ }
+
+-static void backfill_tags(struct transport *transport,
+- struct ref *ref_map,
+- struct fetch_head *fetch_head,
+- struct worktree **worktrees)
++static int backfill_tags(struct transport *transport,
++ struct ref *ref_map,
++ struct fetch_head *fetch_head,
++ struct worktree **worktrees)
+ {
+- int cannot_reuse;
++ int retcode, cannot_reuse;
+
+ /*
+ * Once we have set TRANS_OPT_DEEPEN_SINCE, we can't unset it
+@@ -1522,12 +1522,14 @@ static void backfill_tags(struct transport *transport,
+ transport_set_option(transport, TRANS_OPT_FOLLOWTAGS, NULL);
+ transport_set_option(transport, TRANS_OPT_DEPTH, "0");
+ transport_set_option(transport, TRANS_OPT_DEEPEN_RELATIVE, NULL);
+- fetch_and_consume_refs(transport, ref_map, fetch_head, worktrees);
++ retcode = fetch_and_consume_refs(transport, ref_map, fetch_head, worktrees);
+
+ if (gsecondary) {
+ transport_disconnect(gsecondary);
+ gsecondary = NULL;
+ }
++
++ return retcode;
+ }
+
+ static int do_fetch(struct transport *transport,
+@@ -1635,8 +1637,16 @@ static int do_fetch(struct transport *transport,
+ struct ref *tags_ref_map = NULL, **tail = &tags_ref_map;
+
+ find_non_local_tags(remote_refs, &tags_ref_map, &tail);
+- if (tags_ref_map)
+- backfill_tags(transport, tags_ref_map, &fetch_head, worktrees);
++ if (tags_ref_map) {
++ /*
++ * If backfilling of tags fails then we want to tell
++ * the user so, but we have to continue regardless to
++ * populate upstream information of the references we
++ * have already fetched above.
++ */
++ if (backfill_tags(transport, tags_ref_map, &fetch_head, worktrees))
++ retcode = 1;
++ }
+
+ free_refs(tags_ref_map);
+ }
+diff --git a/t/t5503-tagfollow.sh b/t/t5503-tagfollow.sh
+index 6ffe2a5719..c057c49e80 100755
+--- a/t/t5503-tagfollow.sh
++++ b/t/t5503-tagfollow.sh
+@@ -233,9 +233,7 @@ test_expect_success 'backfill failure causes command to fail' '
+ done
+ EOF
+
+- # Even though we fail to create refs/tags/tag1 the below command
+- # unexpectedly succeeds.
+- git -C clone5 fetch .. $B:refs/heads/something &&
++ test_must_fail git -C clone5 fetch .. $B:refs/heads/something &&
+ test $B = $(git -C clone5 rev-parse --verify refs/heads/something) &&
+ test $S = $(git -C clone5 rev-parse --verify tag2) &&
+ test_must_fail git -C clone5 rev-parse --verify tag1
+--
+2.35.1
+
diff --git a/_support/git-patches/0032-refs-add-interface-to-iterate-over-queued-transactio.patch b/_support/git-patches/0032-refs-add-interface-to-iterate-over-queued-transactio.patch
new file mode 100644
index 000000000..cc6a3e320
--- /dev/null
+++ b/_support/git-patches/0032-refs-add-interface-to-iterate-over-queued-transactio.patch
@@ -0,0 +1,98 @@
+From 7c90ce62cd5ab5f769ec789eff038b43e8f5d910 Mon Sep 17 00:00:00 2001
+Message-Id: <7c90ce62cd5ab5f769ec789eff038b43e8f5d910.1646206542.git.ps@pks.im>
+In-Reply-To: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+References: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+From: Patrick Steinhardt <ps@pks.im>
+Date: Thu, 17 Feb 2022 14:04:32 +0100
+Subject: [PATCH 32/34] refs: add interface to iterate over queued
+ transactional updates
+
+There is no way for a caller to see whether a reference update has
+already been queued up for a given reference transaction. There are
+multiple alternatives to provide this functionality:
+
+ - We may add a function that simply tells us whether a specific
+ reference has already been queued. If implemented naively then
+ this would potentially be quadratic in runtime behaviour if this
+ question is asked repeatedly because we have to iterate over all
+ references every time. The alternative would be to add a hashmap
+ of all queued reference updates to speed up the lookup, but this
+ adds overhead to all callers.
+
+ - We may add a flag to `ref_transaction_add_update()` that causes it
+ to skip duplicates, but this has the same runtime concerns as the
+ first alternative.
+
+ - We may add an interface which lets callers collect all updates
+ which have already been queued such that he can avoid re-adding
+ them. This is the most flexible approach and puts the burden on
+ the caller, but also allows us to not impact any of the existing
+ callsites which don't need this information.
+
+This commit implements the last approach: it allows us to compute the
+map of already-queued updates once up front such that we can then skip
+all subsequent references which are already part of this map.
+
+Signed-off-by: Patrick Steinhardt <ps@pks.im>
+Signed-off-by: Junio C Hamano <gitster@pobox.com>
+(cherry picked from commit 4f2ba2d06a7dd7e84e105a2779a7f07549d04231)
+---
+ refs.c | 16 ++++++++++++++++
+ refs.h | 14 ++++++++++++++
+ 2 files changed, 30 insertions(+)
+
+diff --git a/refs.c b/refs.c
+index 435b90b1ec..5d9cb306ad 100644
+--- a/refs.c
++++ b/refs.c
+@@ -2434,6 +2434,22 @@ int initial_ref_transaction_commit(struct ref_transaction *transaction,
+ return refs->be->initial_transaction_commit(refs, transaction, err);
+ }
+
++void ref_transaction_for_each_queued_update(struct ref_transaction *transaction,
++ ref_transaction_for_each_queued_update_fn cb,
++ void *cb_data)
++{
++ int i;
++
++ for (i = 0; i < transaction->nr; i++) {
++ struct ref_update *update = transaction->updates[i];
++
++ cb(update->refname,
++ (update->flags & REF_HAVE_OLD) ? &update->old_oid : NULL,
++ (update->flags & REF_HAVE_NEW) ? &update->new_oid : NULL,
++ cb_data);
++ }
++}
++
+ int refs_delete_refs(struct ref_store *refs, const char *logmsg,
+ struct string_list *refnames, unsigned int flags)
+ {
+diff --git a/refs.h b/refs.h
+index a015354fd6..be5947f111 100644
+--- a/refs.h
++++ b/refs.h
+@@ -781,6 +781,20 @@ int ref_transaction_abort(struct ref_transaction *transaction,
+ int initial_ref_transaction_commit(struct ref_transaction *transaction,
+ struct strbuf *err);
+
++/*
++ * Execute the given callback function for each of the reference updates which
++ * have been queued in the given transaction. `old_oid` and `new_oid` may be
++ * `NULL` pointers depending on whether the update has these object IDs set or
++ * not.
++ */
++typedef void ref_transaction_for_each_queued_update_fn(const char *refname,
++ const struct object_id *old_oid,
++ const struct object_id *new_oid,
++ void *cb_data);
++void ref_transaction_for_each_queued_update(struct ref_transaction *transaction,
++ ref_transaction_for_each_queued_update_fn cb,
++ void *cb_data);
++
+ /*
+ * Free `*transaction` and all associated data.
+ */
+--
+2.35.1
+
diff --git a/_support/git-patches/0033-fetch-make-atomic-flag-cover-backfilling-of-tags.patch b/_support/git-patches/0033-fetch-make-atomic-flag-cover-backfilling-of-tags.patch
new file mode 100644
index 000000000..9e136f838
--- /dev/null
+++ b/_support/git-patches/0033-fetch-make-atomic-flag-cover-backfilling-of-tags.patch
@@ -0,0 +1,310 @@
+From 3a62f32960d2df2451003f2561a4f14d459991ec Mon Sep 17 00:00:00 2001
+Message-Id: <3a62f32960d2df2451003f2561a4f14d459991ec.1646206542.git.ps@pks.im>
+In-Reply-To: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+References: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+From: Patrick Steinhardt <ps@pks.im>
+Date: Thu, 17 Feb 2022 14:04:36 +0100
+Subject: [PATCH 33/34] fetch: make `--atomic` flag cover backfilling of tags
+
+When fetching references from a remote we by default also fetch all tags
+which point into the history we have fetched. This is a separate step
+performed after updating local references because it requires us to walk
+over the history on the client-side to determine whether the remote has
+announced any tags which point to one of the fetched commits.
+
+This backfilling of tags isn't covered by the `--atomic` flag: right
+now, it only applies to the step where we update our local references.
+This is an oversight at the time the flag was introduced: its purpose is
+to either update all references or none, but right now we happily update
+local references even in the case where backfilling failed.
+
+Fix this by pulling up creation of the reference transaction such that
+we can pass the same transaction to both the code which updates local
+references and to the code which backfills tags. This allows us to only
+commit the transaction in case both actions succeed.
+
+Note that we also have to start passing the transaction into
+`find_non_local_tags()`: this function is responsible for finding all
+tags which we need to backfill. Right now, it will happily return tags
+which have already been updated with our local references. But when we
+use a single transaction for both local references and backfilling then
+it may happen that we try to queue the same reference update twice to
+the transaction, which consequently triggers a bug. We thus have to skip
+over any tags which have already been queued.
+
+Signed-off-by: Patrick Steinhardt <ps@pks.im>
+Signed-off-by: Junio C Hamano <gitster@pobox.com>
+(cherry picked from commit b3a804663c4682f6df55dd6703f8f8af9a7c6ab5)
+---
+ builtin/fetch.c | 92 +++++++++++++++++++++++++++++++-------------
+ t/t5503-tagfollow.sh | 11 ++----
+ 2 files changed, 69 insertions(+), 34 deletions(-)
+
+diff --git a/builtin/fetch.c b/builtin/fetch.c
+index 2d0ffb9859..e996a74862 100644
+--- a/builtin/fetch.c
++++ b/builtin/fetch.c
+@@ -348,7 +348,19 @@ static void clear_item(struct refname_hash_entry *item)
+ item->ignore = 1;
+ }
+
++
++static void add_already_queued_tags(const char *refname,
++ const struct object_id *old_oid,
++ const struct object_id *new_oid,
++ void *cb_data)
++{
++ struct hashmap *queued_tags = cb_data;
++ if (starts_with(refname, "refs/tags/") && new_oid)
++ (void) refname_hash_add(queued_tags, refname, new_oid);
++}
++
+ static void find_non_local_tags(const struct ref *refs,
++ struct ref_transaction *transaction,
+ struct ref **head,
+ struct ref ***tail)
+ {
+@@ -366,6 +378,16 @@ static void find_non_local_tags(const struct ref *refs,
+ create_fetch_oidset(head, &fetch_oids);
+
+ for_each_ref(add_one_refname, &existing_refs);
++
++ /*
++ * If we already have a transaction, then we need to filter out all
++ * tags which have already been queued up.
++ */
++ if (transaction)
++ ref_transaction_for_each_queued_update(transaction,
++ add_already_queued_tags,
++ &existing_refs);
++
+ for (ref = refs; ref; ref = ref->next) {
+ if (!starts_with(ref->name, "refs/tags/"))
+ continue;
+@@ -599,7 +621,7 @@ static struct ref *get_ref_map(struct remote *remote,
+ /* also fetch all tags */
+ get_fetch_map(remote_refs, tag_refspec, &tail, 0);
+ else if (tags == TAGS_DEFAULT && *autotags)
+- find_non_local_tags(remote_refs, &ref_map, &tail);
++ find_non_local_tags(remote_refs, NULL, &ref_map, &tail);
+
+ /* Now append any refs to be updated opportunistically: */
+ *tail = orefs;
+@@ -1082,12 +1104,12 @@ N_("it took %.2f seconds to check forced updates; you can use\n"
+ "to avoid this check\n");
+
+ static int store_updated_refs(const char *raw_url, const char *remote_name,
+- int connectivity_checked, struct ref *ref_map,
++ int connectivity_checked,
++ struct ref_transaction *transaction, struct ref *ref_map,
+ struct fetch_head *fetch_head, struct worktree **worktrees)
+ {
+ int url_len, i, rc = 0;
+ struct strbuf note = STRBUF_INIT, err = STRBUF_INIT;
+- struct ref_transaction *transaction = NULL;
+ const char *what, *kind;
+ struct ref *rm;
+ char *url;
+@@ -1112,14 +1134,6 @@ static int store_updated_refs(const char *raw_url, const char *remote_name,
+ }
+ }
+
+- if (atomic_fetch) {
+- transaction = ref_transaction_begin(&err);
+- if (!transaction) {
+- error("%s", err.buf);
+- goto abort;
+- }
+- }
+-
+ prepare_format_display(ref_map);
+
+ /*
+@@ -1235,14 +1249,6 @@ static int store_updated_refs(const char *raw_url, const char *remote_name,
+ }
+ }
+
+- if (!rc && transaction) {
+- rc = ref_transaction_commit(transaction, &err);
+- if (rc) {
+- error("%s", err.buf);
+- goto abort;
+- }
+- }
+-
+ if (rc & STORE_REF_ERROR_DF_CONFLICT)
+ error(_("some local refs could not be updated; try running\n"
+ " 'git remote prune %s' to remove any old, conflicting "
+@@ -1260,7 +1266,6 @@ static int store_updated_refs(const char *raw_url, const char *remote_name,
+ abort:
+ strbuf_release(&note);
+ strbuf_release(&err);
+- ref_transaction_free(transaction);
+ free(url);
+ return rc;
+ }
+@@ -1301,6 +1306,7 @@ static int check_exist_and_connected(struct ref *ref_map)
+ }
+
+ static int fetch_and_consume_refs(struct transport *transport,
++ struct ref_transaction *transaction,
+ struct ref *ref_map,
+ struct fetch_head *fetch_head,
+ struct worktree **worktrees)
+@@ -1325,7 +1331,8 @@ static int fetch_and_consume_refs(struct transport *transport,
+
+ trace2_region_enter("fetch", "consume_refs", the_repository);
+ ret = store_updated_refs(transport->url, transport->remote->name,
+- connectivity_checked, ref_map, fetch_head, worktrees);
++ connectivity_checked, transaction, ref_map,
++ fetch_head, worktrees);
+ trace2_region_leave("fetch", "consume_refs", the_repository);
+
+ out:
+@@ -1499,6 +1506,7 @@ static struct transport *prepare_transport(struct remote *remote, int deepen)
+ }
+
+ static int backfill_tags(struct transport *transport,
++ struct ref_transaction *transaction,
+ struct ref *ref_map,
+ struct fetch_head *fetch_head,
+ struct worktree **worktrees)
+@@ -1522,7 +1530,7 @@ static int backfill_tags(struct transport *transport,
+ transport_set_option(transport, TRANS_OPT_FOLLOWTAGS, NULL);
+ transport_set_option(transport, TRANS_OPT_DEPTH, "0");
+ transport_set_option(transport, TRANS_OPT_DEEPEN_RELATIVE, NULL);
+- retcode = fetch_and_consume_refs(transport, ref_map, fetch_head, worktrees);
++ retcode = fetch_and_consume_refs(transport, transaction, ref_map, fetch_head, worktrees);
+
+ if (gsecondary) {
+ transport_disconnect(gsecondary);
+@@ -1535,6 +1543,7 @@ static int backfill_tags(struct transport *transport,
+ static int do_fetch(struct transport *transport,
+ struct refspec *rs)
+ {
++ struct ref_transaction *transaction = NULL;
+ struct ref *ref_map = NULL;
+ int autotags = (transport->remote->fetch_tags == 1);
+ int retcode = 0;
+@@ -1544,6 +1553,7 @@ static int do_fetch(struct transport *transport,
+ int must_list_refs = 1;
+ struct worktree **worktrees = get_worktrees();
+ struct fetch_head fetch_head = { 0 };
++ struct strbuf err = STRBUF_INIT;
+
+ if (tags == TAGS_DEFAULT) {
+ if (transport->remote->fetch_tags == 2)
+@@ -1605,6 +1615,14 @@ static int do_fetch(struct transport *transport,
+ if (retcode)
+ goto cleanup;
+
++ if (atomic_fetch) {
++ transaction = ref_transaction_begin(&err);
++ if (!transaction) {
++ retcode = error("%s", err.buf);
++ goto cleanup;
++ }
++ }
++
+ if (tags == TAGS_DEFAULT && autotags)
+ transport_set_option(transport, TRANS_OPT_FOLLOWTAGS, "1");
+ if (prune) {
+@@ -1624,7 +1642,7 @@ static int do_fetch(struct transport *transport,
+ retcode = 1;
+ }
+
+- if (fetch_and_consume_refs(transport, ref_map, &fetch_head, worktrees)) {
++ if (fetch_and_consume_refs(transport, transaction, ref_map, &fetch_head, worktrees)) {
+ retcode = 1;
+ goto cleanup;
+ }
+@@ -1636,21 +1654,37 @@ static int do_fetch(struct transport *transport,
+ if (tags == TAGS_DEFAULT && autotags) {
+ struct ref *tags_ref_map = NULL, **tail = &tags_ref_map;
+
+- find_non_local_tags(remote_refs, &tags_ref_map, &tail);
++ find_non_local_tags(remote_refs, transaction, &tags_ref_map, &tail);
+ if (tags_ref_map) {
+ /*
+ * If backfilling of tags fails then we want to tell
+ * the user so, but we have to continue regardless to
+ * populate upstream information of the references we
+- * have already fetched above.
++ * have already fetched above. The exception though is
++ * when `--atomic` is passed: in that case we'll abort
++ * the transaction and don't commit anything.
+ */
+- if (backfill_tags(transport, tags_ref_map, &fetch_head, worktrees))
++ if (backfill_tags(transport, transaction, tags_ref_map,
++ &fetch_head, worktrees))
+ retcode = 1;
+ }
+
+ free_refs(tags_ref_map);
+ }
+
++ if (transaction) {
++ if (retcode)
++ goto cleanup;
++
++ retcode = ref_transaction_commit(transaction, &err);
++ if (retcode) {
++ error("%s", err.buf);
++ ref_transaction_free(transaction);
++ transaction = NULL;
++ goto cleanup;
++ }
++ }
++
+ commit_fetch_head(&fetch_head);
+
+ if (set_upstream) {
+@@ -1708,7 +1742,13 @@ static int do_fetch(struct transport *transport,
+ }
+
+ cleanup:
++ if (retcode && transaction) {
++ ref_transaction_abort(transaction, &err);
++ error("%s", err.buf);
++ }
++
+ close_fetch_head(&fetch_head);
++ strbuf_release(&err);
+ free_refs(ref_map);
+ free_worktrees(worktrees);
+ return retcode;
+diff --git a/t/t5503-tagfollow.sh b/t/t5503-tagfollow.sh
+index c057c49e80..e72fdc2534 100755
+--- a/t/t5503-tagfollow.sh
++++ b/t/t5503-tagfollow.sh
+@@ -180,11 +180,8 @@ test_expect_success 'atomic fetch with failing backfill' '
+ EOF
+
+ test_must_fail git -C clone3 fetch --atomic .. $B:refs/heads/something &&
+-
+- # Creation of the tag has failed, so ideally refs/heads/something
+- # should not exist. The fact that it does demonstrates that there is
+- # a bug in the `--atomic` flag.
+- test $B = "$(git -C clone3 rev-parse --verify refs/heads/something)"
++ test_must_fail git -C clone3 rev-parse --verify refs/heads/something &&
++ test_must_fail git -C clone3 rev-parse --verify refs/tags/tag2
+ '
+
+ test_expect_success 'atomic fetch with backfill should use single transaction' '
+@@ -197,12 +194,10 @@ test_expect_success 'atomic fetch with backfill should use single transaction' '
+ prepared
+ $ZERO_OID $B refs/heads/something
+ $ZERO_OID $S refs/tags/tag2
++ $ZERO_OID $T refs/tags/tag1
+ committed
+ $ZERO_OID $B refs/heads/something
+ $ZERO_OID $S refs/tags/tag2
+- prepared
+- $ZERO_OID $T refs/tags/tag1
+- committed
+ $ZERO_OID $T refs/tags/tag1
+ EOF
+
+--
+2.35.1
+
diff --git a/_support/git-patches/0034-fetch-make-atomic-flag-cover-pruning-of-refs.patch b/_support/git-patches/0034-fetch-make-atomic-flag-cover-pruning-of-refs.patch
new file mode 100644
index 000000000..e0f50a7e1
--- /dev/null
+++ b/_support/git-patches/0034-fetch-make-atomic-flag-cover-pruning-of-refs.patch
@@ -0,0 +1,149 @@
+From c1334339eccde428151dc9e544ab2ec4d0178cad Mon Sep 17 00:00:00 2001
+Message-Id: <c1334339eccde428151dc9e544ab2ec4d0178cad.1646206542.git.ps@pks.im>
+In-Reply-To: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+References: <c74f385fb46855ac0db222b6845ddb95e6a36264.1646206541.git.ps@pks.im>
+From: Patrick Steinhardt <ps@pks.im>
+Date: Thu, 17 Feb 2022 14:04:41 +0100
+Subject: [PATCH 34/34] fetch: make `--atomic` flag cover pruning of refs
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+When fetching with the `--prune` flag we will delete any local
+references matching the fetch refspec which have disappeared on the
+remote. This step is not currently covered by the `--atomic` flag: we
+delete branches even though updating of local references has failed,
+which means that the fetch is not an all-or-nothing operation.
+
+Fix this bug by passing in the global transaction into `prune_refs()`:
+if one is given, then we'll only queue up deletions and not commit them
+right away.
+
+This change also improves performance when pruning many branches in a
+repository with a big packed-refs file: every references is pruned in
+its own transaction, which means that we potentially have to rewrite
+the packed-refs files for every single reference we're about to prune.
+
+The following benchmark demonstrates this: it performs a pruning fetch
+from a repository with a single reference into a repository with 100k
+references, which causes us to prune all but one reference. This is of
+course a very artificial setup, but serves to demonstrate the impact of
+only having to write the packed-refs file once:
+
+ Benchmark 1: git fetch --prune --atomic +refs/*:refs/* (HEAD~)
+ Time (mean ± σ): 2.366 s ± 0.021 s [User: 0.858 s, System: 1.508 s]
+ Range (min … max): 2.328 s … 2.407 s 10 runs
+
+ Benchmark 2: git fetch --prune --atomic +refs/*:refs/* (HEAD)
+ Time (mean ± σ): 1.369 s ± 0.017 s [User: 0.715 s, System: 0.641 s]
+ Range (min … max): 1.346 s … 1.400 s 10 runs
+
+ Summary
+ 'git fetch --prune --atomic +refs/*:refs/* (HEAD)' ran
+ 1.73 ± 0.03 times faster than 'git fetch --prune --atomic +refs/*:refs/* (HEAD~)'
+
+Signed-off-by: Patrick Steinhardt <ps@pks.im>
+Signed-off-by: Junio C Hamano <gitster@pobox.com>
+(cherry picked from commit 583bc419235cedc6a2ba12593f058a9f812b9594)
+---
+ builtin/fetch.c | 30 ++++++++++++++++++++++--------
+ t/t5510-fetch.sh | 8 ++------
+ 2 files changed, 24 insertions(+), 14 deletions(-)
+
+diff --git a/builtin/fetch.c b/builtin/fetch.c
+index e996a74862..25cc0b906e 100644
+--- a/builtin/fetch.c
++++ b/builtin/fetch.c
+@@ -1340,11 +1340,14 @@ static int fetch_and_consume_refs(struct transport *transport,
+ return ret;
+ }
+
+-static int prune_refs(struct refspec *rs, struct ref *ref_map,
++static int prune_refs(struct refspec *rs,
++ struct ref_transaction *transaction,
++ struct ref *ref_map,
+ const char *raw_url)
+ {
+ int url_len, i, result = 0;
+ struct ref *ref, *stale_refs = get_stale_heads(rs, ref_map);
++ struct strbuf err = STRBUF_INIT;
+ char *url;
+ const char *dangling_msg = dry_run
+ ? _(" (%s will become dangling)")
+@@ -1364,13 +1367,22 @@ static int prune_refs(struct refspec *rs, struct ref *ref_map,
+ url_len = i - 3;
+
+ if (!dry_run) {
+- struct string_list refnames = STRING_LIST_INIT_NODUP;
++ if (transaction) {
++ for (ref = stale_refs; ref; ref = ref->next) {
++ result = ref_transaction_delete(transaction, ref->name, NULL, 0,
++ "fetch: prune", &err);
++ if (result)
++ goto cleanup;
++ }
++ } else {
++ struct string_list refnames = STRING_LIST_INIT_NODUP;
+
+- for (ref = stale_refs; ref; ref = ref->next)
+- string_list_append(&refnames, ref->name);
++ for (ref = stale_refs; ref; ref = ref->next)
++ string_list_append(&refnames, ref->name);
+
+- result = delete_refs("fetch: prune", &refnames, 0);
+- string_list_clear(&refnames, 0);
++ result = delete_refs("fetch: prune", &refnames, 0);
++ string_list_clear(&refnames, 0);
++ }
+ }
+
+ if (verbosity >= 0) {
+@@ -1391,6 +1403,8 @@ static int prune_refs(struct refspec *rs, struct ref *ref_map,
+ }
+ }
+
++cleanup:
++ strbuf_release(&err);
+ free(url);
+ free_refs(stale_refs);
+ return result;
+@@ -1632,10 +1646,10 @@ static int do_fetch(struct transport *transport,
+ * don't care whether --tags was specified.
+ */
+ if (rs->nr) {
+- retcode = prune_refs(rs, ref_map, transport->url);
++ retcode = prune_refs(rs, transaction, ref_map, transport->url);
+ } else {
+ retcode = prune_refs(&transport->remote->fetch,
+- ref_map,
++ transaction, ref_map,
+ transport->url);
+ }
+ if (retcode != 0)
+diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
+index 70d51f343b..48e14e2dab 100755
+--- a/t/t5510-fetch.sh
++++ b/t/t5510-fetch.sh
+@@ -354,17 +354,13 @@ test_expect_success 'fetch --atomic --prune executes a single reference transact
+ head_oid=$(git rev-parse HEAD) &&
+
+ # Fetching with the `--atomic` flag should update all references in a
+- # single transaction. It is currently missing coverage of pruned
+- # references though, and as a result those may be committed to disk
+- # even if updating references fails later.
++ # single transaction.
+ cat >expected <<-EOF &&
+ prepared
+ $ZERO_OID $ZERO_OID refs/remotes/origin/scheduled-for-deletion
+- committed
+- $ZERO_OID $ZERO_OID refs/remotes/origin/scheduled-for-deletion
+- prepared
+ $ZERO_OID $head_oid refs/remotes/origin/new-branch
+ committed
++ $ZERO_OID $ZERO_OID refs/remotes/origin/scheduled-for-deletion
+ $ZERO_OID $head_oid refs/remotes/origin/new-branch
+ EOF
+
+--
+2.35.1
+