Thread (17 messages) flat view 17 messages, 5 authors, 7h ago
HOTtoday

[PATCH] fetch: add config to avoid fetching every branch in shallow repo

From: Harald Nordgren via GitGitGadget <hidden>
Date: 2026-09-19 14:47:28
Subsystem: documentation, the rest · Maintainers: Jonathan Corbet, Linus Torvalds

From: Harald Nordgren <redacted>

In a shallow, sparsely checked out clone of a repository with many
branches, plain git pull can take minutes or hang outright, even
though only one branch is actually being worked on.

Add fetch.shallow, off by default. When enabled, a fetch or pull for
a shallow repository that isn't already scoped to specific refs
fetches only the current branch's tracked upstream, instead of every
branch the remote has. git pull ultimately runs such a fetch under
the hood, so this fixes pull the same way. It has no effect once the
repository is no longer shallow, and no effect on a fetch of a remote
the current branch doesn't track, both fall back to the existing
behavior.

This is opt-in rather than automatic because it changes what a plain
fetch or pull leaves in refs/remotes/<name>/ for anyone who currently
relies on it syncing every branch of a shallow remote, not just the
one they are on. Scoping remote.<name>.fetch by hand already covers
this for a single remote, but that requires knowing the config exists
and applies it permanently, even to branches that are not currently
checked out.

The remote's recorded default branch (remotes/<name>/HEAD) is kept up
to date the same way it always is, only the other branches are
skipped.

Signed-off-by: Harald Nordgren <redacted>
---
    fetch: add fetch.shallow so pull doesn't fetch every branch on shallow
    repo
    
    Add fetch.shallow config for big shallow repo, so git fetch/pull doesn't
    hang by fetching every branch.

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2412%2FHaraldNordgren%2Ffetch-shallow-narrow-refspec-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2412/HaraldNordgren/fetch-shallow-narrow-refspec-v1
Pull-Request: https://github.com/git/git/pull/2412

 Documentation/config/fetch.adoc |  15 ++++
 builtin/fetch.c                 |  18 +++-
 t/t5537-fetch-shallow.sh        | 146 ++++++++++++++++++++++++++++++++
 3 files changed, 175 insertions(+), 4 deletions(-)
diff --git a/Documentation/config/fetch.adoc b/Documentation/config/fetch.adoc
index 00435e9a16..e63e54a463 100644
--- a/Documentation/config/fetch.adoc
+++ b/Documentation/config/fetch.adoc
@@ -145,3 +145,18 @@ remove the value for the `fetch.bundleCreationToken` value before fetching.
 `never`;;
 	Never create or modify the `remotes/<name>/HEAD` symbolic-ref.
 --
+
+`fetch.shallow`::
+	If true, and the repository is a shallow repository (see
+	linkgit:git-clone[1] `--depth`), a fetch or `git pull` that names no
+	explicit refspec and would otherwise fall back to the remote's
+	configured `remote.<name>.fetch` refspec instead fetches only the
+	current branch's upstream, when that upstream is on the remote being
+	fetched. This avoids negotiating history for every branch the remote
+	advertises, which can be slow on a shallow repository that tracks
+	many disjoint shallow histories. It has no effect on a fetch that
+	names an explicit remote or refspec, and no effect on a repository
+	that is not shallow. Defaults to false.
++
+`remotes/<name>/HEAD` is still kept up to date per `fetch.followRemoteHEAD`
+while this is in effect, only the other branches are skipped.
diff --git a/builtin/fetch.c b/builtin/fetch.c
index 533fdfe7d8..b22f7fe5f4 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -111,6 +111,7 @@ struct fetch_config {
 	int recurse_submodules;
 	int parallel;
 	int submodule_fetch_jobs;
+	int shallow;
 };
 
 static int git_fetch_config(const char *k, const char *v,
@@ -175,6 +176,11 @@ static int git_fetch_config(const char *k, const char *v,
 		return 0;
 	}
 
+	if (!strcmp(k, "fetch.shallow")) {
+		fetch_config->shallow = git_config_bool(k, v);
+		return 0;
+	}
+
 	if (!strcmp(k, "fetch.followremotehead")) {
 		if (!v)
 			return config_error_nonbool(k);
@@ -1958,15 +1964,19 @@ static int do_fetch(struct transport *transport,
 		refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);
 	} else {
 		struct branch *branch = branch_get(NULL);
+		int tracks_this_remote = branch && branch_has_merge_config(branch) &&
+			!strcmp(branch->remote_name, transport->remote->name);
+		int narrow_to_tracked_ref = config->shallow &&
+			is_repository_shallow(the_repository) && tracks_this_remote;
 
 		if (transport->remote->fetch.nr) {
-			refspec_ref_prefixes(&transport->remote->fetch,
-					     &transport_ls_refs_options.ref_prefixes);
+			if (!narrow_to_tracked_ref)
+				refspec_ref_prefixes(&transport->remote->fetch,
+						     &transport_ls_refs_options.ref_prefixes);
 			if (follow_remote_head != FOLLOW_REMOTE_NEVER)
 				do_set_head = 1;
 		}
-		if (branch && branch_has_merge_config(branch) &&
-		    !strcmp(branch->remote_name, transport->remote->name)) {
+		if (tracks_this_remote) {
 			int i;
 			for (i = 0; i < branch->merge_nr; i++) {
 				strvec_push(&transport_ls_refs_options.ref_prefixes,
diff --git a/t/t5537-fetch-shallow.sh b/t/t5537-fetch-shallow.sh
index f323ceebd2..8143af9fc3 100755
--- a/t/t5537-fetch-shallow.sh
+++ b/t/t5537-fetch-shallow.sh
@@ -13,6 +13,24 @@ commit() {
 	git commit -m "$1"
 }
 
+check_upstream_refs () {
+	git for-each-ref --format="%(refname)" refs/remotes/upstream/ >actual &&
+	cat >expect &&
+	test_cmp expect actual
+}
+
+check_upstream_head () {
+	git symbolic-ref refs/remotes/upstream/HEAD >actual &&
+	echo "refs/remotes/upstream/$1" >expect &&
+	test_cmp expect actual
+}
+
+check_same_tip () {
+	git log --oneline -1 "$1" >expect &&
+	git -C "$2" log --oneline -1 "$3" >actual &&
+	test_cmp expect actual
+}
+
 test_expect_success 'setup' '
 	commit 1 &&
 	commit 2 &&
@@ -261,6 +279,134 @@ test_expect_success 'fetch --deepen does not truncate' '
 	test_cmp expect actual
 '
 
+test_expect_success 'fetch.shallow setup' '
+	git branch narrow-side &&
+	git clone --no-local --depth=1 --branch main --single-branch \
+		.git narrow-default &&
+	git clone --no-local --depth=1 --branch main --single-branch \
+		.git narrow-enabled &&
+	(
+		cd narrow-default &&
+		git remote add upstream ../.git &&
+		git fetch --depth=1 upstream main:refs/remotes/upstream/main &&
+		git branch --set-upstream-to=upstream/main main
+	) &&
+	(
+		cd narrow-enabled &&
+		git remote add upstream ../.git &&
+		git fetch --depth=1 upstream main:refs/remotes/upstream/main &&
+		git branch --set-upstream-to=upstream/main main &&
+		git config fetch.shallow true
+	)
+'
+
+test_expect_success 'a refspec-less fetch expands to the configured refspec by default' '
+	(
+		cd narrow-default &&
+		git fetch upstream &&
+		check_upstream_refs <<-\EOF
+		refs/remotes/upstream/HEAD
+		refs/remotes/upstream/main
+		refs/remotes/upstream/narrow-side
+		EOF
+	)
+'
+
+test_expect_success 'fetch.shallow=true limits a refspec-less fetch to the tracked branch' '
+	(
+		cd narrow-enabled &&
+		git fetch upstream &&
+		check_upstream_refs <<-\EOF
+		refs/remotes/upstream/HEAD
+		refs/remotes/upstream/main
+		EOF
+	)
+'
+
+test_expect_success 'fetch.shallow=true still creates refs/remotes/<remote>/HEAD' '
+	(
+		cd narrow-enabled &&
+		git symbolic-ref -d refs/remotes/upstream/HEAD &&
+		git fetch upstream &&
+		check_upstream_head main
+	)
+'
+
+test_expect_success 'fetch.shallow=true with followRemoteHEAD=always corrects a stale HEAD' '
+	test_when_finished \
+		"git -C narrow-enabled update-ref -d refs/remotes/upstream/stale-branch" &&
+	(
+		cd narrow-enabled &&
+		git update-ref refs/remotes/upstream/stale-branch refs/remotes/upstream/main &&
+		git symbolic-ref refs/remotes/upstream/HEAD refs/remotes/upstream/stale-branch &&
+		git -c fetch.followRemoteHEAD=always fetch upstream &&
+		check_upstream_head main
+	)
+'
+
+test_expect_success 'fetch.shallow=true still updates the tracked branch' '
+	commit 5 &&
+	git -C narrow-enabled fetch upstream &&
+	check_same_tip main narrow-enabled refs/remotes/upstream/main
+'
+
+test_expect_success 'fetch.shallow=true keeps git pull narrowed too' '
+	test_when_finished "git branch -D narrow-side" &&
+	commit 6 &&
+	(
+		cd narrow-enabled &&
+		git pull &&
+		check_upstream_refs <<-\EOF
+		refs/remotes/upstream/HEAD
+		refs/remotes/upstream/main
+		EOF
+	) &&
+	check_same_tip main narrow-enabled HEAD
+'
+
+test_expect_success 'fetch.shallow=true has no effect on a non-shallow repository' '
+	git clone --no-local --branch main --single-branch .git narrow-full &&
+	(
+		cd narrow-full &&
+		git rev-parse --is-shallow-repository >actual &&
+		echo false >expect &&
+		test_cmp expect actual &&
+		git remote add upstream ../.git &&
+		git fetch upstream &&
+		git branch --set-upstream-to=upstream/main main &&
+		git config fetch.shallow true
+	) &&
+	test_when_finished "git branch -D narrow-full-side" &&
+	git branch narrow-full-side &&
+	(
+		cd narrow-full &&
+		git fetch upstream &&
+		check_upstream_refs <<-\EOF
+		refs/remotes/upstream/HEAD
+		refs/remotes/upstream/main
+		refs/remotes/upstream/narrow-full-side
+		EOF
+	)
+'
+
+test_expect_success 'fetch.shallow=true only narrows a fetch of the tracked remote' '
+	test_when_finished "git branch -D other-side" &&
+	git branch other-side &&
+	git clone --no-local --depth=1 --branch main --single-branch \
+		.git narrow-other-remote &&
+	(
+		cd narrow-other-remote &&
+		git remote add upstream ../.git &&
+		git config fetch.shallow true &&
+		git fetch upstream &&
+		check_upstream_refs <<-\EOF
+		refs/remotes/upstream/HEAD
+		refs/remotes/upstream/main
+		refs/remotes/upstream/other-side
+		EOF
+	)
+'
+
 . "$TEST_DIRECTORY"/lib-httpd.sh
 start_httpd
 
base-commit: d38352cd43ab9745686d697872408bc3249a153f
-- 
gitgitgadget
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help