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:
authorSami Hiltunen <shiltunen@gitlab.com>2022-03-03 13:45:04 +0300
committerSami Hiltunen <shiltunen@gitlab.com>2022-03-03 13:45:04 +0300
commit5800949732ddaf48f31cf56353a0117e50d90571 (patch)
treedda342f6d4af7a2e0761cd6972f9af6629da18cb
parentef4ac6950bbd00487178dd6b75d71892d0cc9af0 (diff)
parentb547b368c8f584e9aabe8eef9342f99440b0c248 (diff)
Merge branch 'pks-git-v2.35.1.gl0' into 'master'
git: Add support for bundled Git v2.35.1.gl0 Closes #4067 and #4068 See merge request gitlab-org/gitaly!4352
-rw-r--r--Makefile110
-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
-rw-r--r--internal/git/execution_environment.go7
-rw-r--r--internal/metadata/featureflag/ff_git_v2351_with_fetch_speedups.go5
-rw-r--r--internal/testhelper/testhelper.go3
18 files changed, 1950 insertions, 37 deletions
diff --git a/Makefile b/Makefile
index 6dda56e5e..9ab1c0cef 100644
--- a/Makefile
+++ b/Makefile
@@ -89,17 +89,6 @@ PROTOC_GEN_GO_GRPC_VERSION?= 1.1.0
GIT2GO_VERSION ?= v33
LIBGIT2_VERSION ?= v1.3.0
-# The default version is used in case the caller does not set the variable or
-# if it is either set to the empty string or "default".
-ifeq (${GIT_VERSION:default=},)
- override GIT_VERSION := v2.33.1
- GIT_APPLY_DEFAULT_PATCHES := YesPlease
-else
- # Support both vX.Y.Z and X.Y.Z version patterns, since callers across GitLab
- # use both.
- override GIT_VERSION := $(shell echo ${GIT_VERSION} | awk '/^[0-9]\.[0-9]+\.[0-9]+$$/ { printf "v" } { print $$1 }')
-endif
-
# protoc target
PROTOC_REPO_URL ?= https://github.com/protocolbuffers/protobuf
PROTOC_SOURCE_DIR ?= ${DEPENDENCY_DIR}/protobuf/source
@@ -119,7 +108,6 @@ GIT_REPO_URL ?= https://gitlab.com/gitlab-org/gitlab-git.git
# The default prefix specifies where Git will be installed to if no GIT_PREFIX
# was given. This directory will be cleaned up before we install into it.
GIT_DEFAULT_PREFIX := ${DEPENDENCY_DIR}/git/install
-GIT_SOURCE_DIR := ${DEPENDENCY_DIR}/git/source
GIT_QUIET :=
ifeq (${Q},@)
GIT_QUIET = --quiet
@@ -129,7 +117,11 @@ GIT_EXECUTABLES += git
GIT_EXECUTABLES += git-remote-http
GIT_EXECUTABLES += git-http-backend
-ifdef GIT_APPLY_DEFAULT_PATCHES
+# The default version is used in case the caller does not set the variable or
+# if it is either set to the empty string or "default".
+ifeq (${GIT_VERSION:default=},)
+ override GIT_VERSION := v2.33.1
+
# Before adding custom patches, please read doc/PROCESS.md#Patching-git
# first to make sure your patches meet our acceptance criteria. Patches
# must be put into `_support/git-patches`.
@@ -185,6 +177,10 @@ ifdef GIT_APPLY_DEFAULT_PATCHES
# then this should be undefined. Otherwise, it must be set to at least
# `gl1` given that `0` is the "default" GitLab patch level.
GIT_EXTRA_VERSION := gl3
+else
+ # Support both vX.Y.Z and X.Y.Z version patterns, since callers across GitLab
+ # use both.
+ override GIT_VERSION := $(shell echo ${GIT_VERSION} | awk '/^[0-9]\.[0-9]+\.[0-9]+$$/ { printf "v" } { print $$1 }')
endif
ifeq ($(origin GIT_BUILD_OPTIONS),undefined)
@@ -360,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
@@ -591,7 +591,7 @@ ${DEPENDENCY_DIR}: | ${BUILD_DIR}
.PHONY: dependency-version
${DEPENDENCY_DIR}/libgit2.version: dependency-version | ${DEPENDENCY_DIR}
${Q}[ x"$$(cat "$@" 2>/dev/null)" = x"${LIBGIT2_VERSION} ${LIBGIT2_BUILD_OPTIONS}" ] || >$@ echo -n "${LIBGIT2_VERSION} ${LIBGIT2_BUILD_OPTIONS}"
-${DEPENDENCY_DIR}/git.version: dependency-version | ${DEPENDENCY_DIR}
+${DEPENDENCY_DIR}/git-%.version: dependency-version | ${DEPENDENCY_DIR}
${Q}[ x"$$(cat "$@" 2>/dev/null)" = x"${GIT_VERSION}.${GIT_EXTRA_VERSION} ${GIT_BUILD_OPTIONS} ${GIT_PATCHES}" ] || >$@ echo -n "${GIT_VERSION}.${GIT_EXTRA_VERSION} ${GIT_BUILD_OPTIONS} ${GIT_PATCHES}"
${DEPENDENCY_DIR}/protoc.version: dependency-version | ${DEPENDENCY_DIR}
${Q}[ x"$$(cat "$@" 2>/dev/null)" = x"${PROTOC_VERSION} ${PROTOC_BUILD_OPTIONS}" ] || >$@ echo -n "${PROTOC_VERSION} ${PROTOC_BUILD_OPTIONS}"
@@ -616,45 +616,81 @@ ${LIBGIT2_INSTALL_DIR}/lib/libgit2.a: ${DEPENDENCY_DIR}/libgit2.version
# build binaries inside of it, we cannot depend on it directly or we'd
# otherwise try to rebuild all targets depending on it whenever we build
# something else. We thus depend on the Makefile instead.
-${GIT_SOURCE_DIR}/Makefile: ${DEPENDENCY_DIR}/git.version
- ${Q}${GIT} -c init.defaultBranch=master init ${GIT_QUIET} ${GIT_SOURCE_DIR}
- ${Q}${GIT} -C "${GIT_SOURCE_DIR}" config remote.origin.url ${GIT_REPO_URL}
- ${Q}${GIT} -C "${GIT_SOURCE_DIR}" config remote.origin.tagOpt --no-tags
- ${Q}${GIT} -C "${GIT_SOURCE_DIR}" fetch --depth 1 ${GIT_QUIET} origin ${GIT_VERSION}
- ${Q}${GIT} -C "${GIT_SOURCE_DIR}" reset --hard
- ${Q}${GIT} -C "${GIT_SOURCE_DIR}" checkout ${GIT_QUIET} --detach FETCH_HEAD
-ifdef GIT_PATCHES
- ${Q}${GIT} -C "${GIT_SOURCE_DIR}" apply $(addprefix "${SOURCE_DIR}"/_support/git-patches/,${GIT_PATCHES})
-endif
+${DEPENDENCY_DIR}/git-%/Makefile: ${DEPENDENCY_DIR}/git-%.version
+ ${Q}${GIT} -c init.defaultBranch=master init ${GIT_QUIET} "${@D}"
+ ${Q}${GIT} -C "${@D}" config remote.origin.url ${GIT_REPO_URL}
+ ${Q}${GIT} -C "${@D}" config remote.origin.tagOpt --no-tags
+ ${Q}${GIT} -C "${@D}" fetch --depth 1 ${GIT_QUIET} origin ${GIT_VERSION}
+ ${Q}${GIT} -C "${@D}" reset --hard
+ ${Q}${GIT} -C "${@D}" checkout ${GIT_QUIET} --detach FETCH_HEAD
+ ${Q}if test -n "${GIT_PATCHES}"; then ${GIT} -C "${@D}" apply $(addprefix "${SOURCE_DIR}"/_support/git-patches/,${GIT_PATCHES}); fi
@ # We're writing the version into the "version" file in Git's own source
@ # directory. If it exists, Git's Makefile will pick it up and use it as
@ # the version instead of auto-detecting via git-describe(1).
-ifdef GIT_EXTRA_VERSION
- ${Q}echo ${GIT_VERSION}.${GIT_EXTRA_VERSION} >"${GIT_SOURCE_DIR}"/version
-else
- ${Q}rm -f "${GIT_SOURCE_DIR}"/version
-endif
+ ${Q}if test -n "${GIT_EXTRA_VERSION}"; then echo ${GIT_VERSION}.${GIT_EXTRA_VERSION} >"${@D}"/version; else rm -f "${@D}"/version; fi
${Q}touch $@
-${GIT_SOURCE_DIR}/%: ${GIT_SOURCE_DIR}/Makefile
- ${Q}env -u PROFILE -u MAKEFLAGS -u GIT_VERSION ${MAKE} -C ${GIT_SOURCE_DIR} -j$(shell nproc) prefix=${GIT_PREFIX} ${GIT_BUILD_OPTIONS} $(notdir $@)
+$(patsubst %,${DEPENDENCY_DIR}/git-\%/%,${GIT_EXECUTABLES}): ${DEPENDENCY_DIR}/git-%/Makefile
+ ${Q}env -u PROFILE -u MAKEFLAGS -u GIT_VERSION ${MAKE} -C "${@D}" -j$(shell nproc) prefix=${GIT_PREFIX} ${GIT_BUILD_OPTIONS} ${GIT_EXECUTABLES}
${Q}touch $@
-${GIT_PREFIX}/bin/git: ${GIT_SOURCE_DIR}/Makefile
+${GIT_PREFIX}/bin/git: ${DEPENDENCY_DIR}/git-${GIT_VERSION}.${GIT_EXTRA_VERSION}/Makefile
@ # Remove the Git installation first in case GIT_PREFIX is the default
@ # prefix which always points into our build directory. This is done so
@ # we never end up with mixed Git installations on developer machines.
@ # We cannot ever remove GIT_PREFIX though in case they're different
@ # because it may point to a user-controlled directory.
${Q}if [ "x${GIT_DEFAULT_PREFIX}" = "x${GIT_PREFIX}" ]; then rm -rf "${GIT_DEFAULT_PREFIX}"; fi
- ${Q}env -u PROFILE -u MAKEFLAGS -u GIT_VERSION ${MAKE} -C ${GIT_SOURCE_DIR} -j$(shell nproc) prefix=${GIT_PREFIX} ${GIT_BUILD_OPTIONS} install
+ ${Q}env -u PROFILE -u MAKEFLAGS -u GIT_VERSION ${MAKE} -C "$(<D)" -j$(shell nproc) prefix=${GIT_PREFIX} ${GIT_BUILD_OPTIONS} install
${Q}touch $@
-${BUILD_DIR}/bin/gitaly-%: ${GIT_SOURCE_DIR}/% | ${BUILD_DIR}/bin
+${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}mkdir -p ${@D}
${Q}install $< $@
${PROTOC}: ${DEPENDENCY_DIR}/protoc.version | ${TOOLS_DIR}
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
+
diff --git a/internal/git/execution_environment.go b/internal/git/execution_environment.go
index d140c6f44..a13f1dfad 100644
--- a/internal/git/execution_environment.go
+++ b/internal/git/execution_environment.go
@@ -26,6 +26,13 @@ var (
// case `IsEnabled()` returns `false` though.
ExecutionEnvironmentConstructors = []ExecutionEnvironmentConstructor{
BundledGitEnvironmentConstructor{
+ Suffix: "-v2.35.1.gl1",
+ FeatureFlags: []featureflag.FeatureFlag{
+ featureflag.UseBundledGit,
+ featureflag.GitV2351WithFetchSpeedups,
+ },
+ },
+ BundledGitEnvironmentConstructor{
// This is the current default bundled Git environment, which does not yet
// have a version suffix.
Suffix: "",
diff --git a/internal/metadata/featureflag/ff_git_v2351_with_fetch_speedups.go b/internal/metadata/featureflag/ff_git_v2351_with_fetch_speedups.go
new file mode 100644
index 000000000..3c3610b9e
--- /dev/null
+++ b/internal/metadata/featureflag/ff_git_v2351_with_fetch_speedups.go
@@ -0,0 +1,5 @@
+package featureflag
+
+// GitV2351WithFetchSpeedups will enable the use of Git v2.35.1 with patches speeding up mirror
+// fetches in repositories with many references.
+var GitV2351WithFetchSpeedups = NewFeatureFlag("git_v2351_with_fetch_speedups", false)
diff --git a/internal/testhelper/testhelper.go b/internal/testhelper/testhelper.go
index a9d51ef98..496404cba 100644
--- a/internal/testhelper/testhelper.go
+++ b/internal/testhelper/testhelper.go
@@ -180,6 +180,9 @@ func ContextWithoutCancel(opts ...ContextOpt) context.Context {
// and thus it's not feasible to inject the feature flag everywhere. Instead, we just use
// one of both randomly.
ctx = featureflag.ContextWithFeatureFlag(ctx, featureflag.UseBundledGit, mrand.Int()%2 == 0)
+ // Same as with the preceding feature flag, this flag is checked whenever we execute a Git
+ // command.
+ ctx = featureflag.ContextWithFeatureFlag(ctx, featureflag.GitV2351WithFetchSpeedups, mrand.Int()%2 == 0)
for _, opt := range opts {
ctx = opt(ctx)