{"thread":{"id":"66353","subject":"[PATCH] fetch: add config to avoid fetching every branch in shallow repo","startedAt":"2026-09-19T14:47:28Z","lastAt":"2026-10-05T18:09:30Z","messageCount":47,"participants":["Harald Nordgren via GitGitGadget","Phillip Wood","Harald Nordgren","Junio C Hamano","D. Ben Knoble"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"552881","messageId":"pull.2412.git.git.1789829246437.gitgitgadget@gmail.com","threadId":"66353","inReplyTo":null,"subject":"[PATCH] fetch: add config to avoid fetching every branch in shallow repo","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-19T14:47:26Z","receivedAt":"2026-09-19T14:47:28Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nIn a shallow, sparsely checked out clone of a repository with many\nbranches, plain git pull can take minutes or hang outright, even\nthough only one branch is actually being worked on.\n\nAdd fetch.shallow, off by default. When enabled, a fetch or pull for\na shallow repository that isn't already scoped to specific refs\nfetches only the current branch's tracked upstream, instead of every\nbranch the remote has. git pull ultimately runs such a fetch under\nthe hood, so this fixes pull the same way. It has no effect once the\nrepository is no longer shallow, and no effect on a fetch of a remote\nthe current branch doesn't track, both fall back to the existing\nbehavior.\n\nThis is opt-in rather than automatic because it changes what a plain\nfetch or pull leaves in refs/remotes/<name>/ for anyone who currently\nrelies on it syncing every branch of a shallow remote, not just the\none they are on. Scoping remote.<name>.fetch by hand already covers\nthis for a single remote, but that requires knowing the config exists\nand applies it permanently, even to branches that are not currently\nchecked out.\n\nThe remote's recorded default branch (remotes/<name>/HEAD) is kept up\nto date the same way it always is, only the other branches are\nskipped.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n    fetch: add fetch.shallow so pull doesn't fetch every branch on shallow\n    repo\n    \n    Add fetch.shallow config for big shallow repo, so git fetch/pull doesn't\n    hang by fetching every branch.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2412%2FHaraldNordgren%2Ffetch-shallow-narrow-refspec-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2412/HaraldNordgren/fetch-shallow-narrow-refspec-v1\nPull-Request: https://github.com/git/git/pull/2412\n\n Documentation/config/fetch.adoc |  15 ++++\n builtin/fetch.c                 |  18 +++-\n t/t5537-fetch-shallow.sh        | 146 ++++++++++++++++++++++++++++++++\n 3 files changed, 175 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/config/fetch.adoc b/Documentation/config/fetch.adoc\nindex 00435e9a16..e63e54a463 100644\n--- a/Documentation/config/fetch.adoc\n+++ b/Documentation/config/fetch.adoc\n@@ -145,3 +145,18 @@ remove the value for the `fetch.bundleCreationToken` value before fetching.\n `never`;;\n \tNever create or modify the `remotes/<name>/HEAD` symbolic-ref.\n --\n+\n+`fetch.shallow`::\n+\tIf true, and the repository is a shallow repository (see\n+\tlinkgit:git-clone[1] `--depth`), a fetch or `git pull` that names no\n+\texplicit refspec and would otherwise fall back to the remote's\n+\tconfigured `remote.<name>.fetch` refspec instead fetches only the\n+\tcurrent branch's upstream, when that upstream is on the remote being\n+\tfetched. This avoids negotiating history for every branch the remote\n+\tadvertises, which can be slow on a shallow repository that tracks\n+\tmany disjoint shallow histories. It has no effect on a fetch that\n+\tnames an explicit remote or refspec, and no effect on a repository\n+\tthat is not shallow. Defaults to false.\n++\n+`remotes/<name>/HEAD` is still kept up to date per `fetch.followRemoteHEAD`\n+while this is in effect, only the other branches are skipped.\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 533fdfe7d8..b22f7fe5f4 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -111,6 +111,7 @@ struct fetch_config {\n \tint recurse_submodules;\n \tint parallel;\n \tint submodule_fetch_jobs;\n+\tint shallow;\n };\n \n static int git_fetch_config(const char *k, const char *v,\n@@ -175,6 +176,11 @@ static int git_fetch_config(const char *k, const char *v,\n \t\treturn 0;\n \t}\n \n+\tif (!strcmp(k, \"fetch.shallow\")) {\n+\t\tfetch_config->shallow = git_config_bool(k, v);\n+\t\treturn 0;\n+\t}\n+\n \tif (!strcmp(k, \"fetch.followremotehead\")) {\n \t\tif (!v)\n \t\t\treturn config_error_nonbool(k);\n@@ -1958,15 +1964,19 @@ static int do_fetch(struct transport *transport,\n \t\trefspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);\n \t} else {\n \t\tstruct branch *branch = branch_get(NULL);\n+\t\tint tracks_this_remote = branch && branch_has_merge_config(branch) &&\n+\t\t\t!strcmp(branch->remote_name, transport->remote->name);\n+\t\tint narrow_to_tracked_ref = config->shallow &&\n+\t\t\tis_repository_shallow(the_repository) && tracks_this_remote;\n \n \t\tif (transport->remote->fetch.nr) {\n-\t\t\trefspec_ref_prefixes(&transport->remote->fetch,\n-\t\t\t\t\t     &transport_ls_refs_options.ref_prefixes);\n+\t\t\tif (!narrow_to_tracked_ref)\n+\t\t\t\trefspec_ref_prefixes(&transport->remote->fetch,\n+\t\t\t\t\t\t     &transport_ls_refs_options.ref_prefixes);\n \t\t\tif (follow_remote_head != FOLLOW_REMOTE_NEVER)\n \t\t\t\tdo_set_head = 1;\n \t\t}\n-\t\tif (branch && branch_has_merge_config(branch) &&\n-\t\t    !strcmp(branch->remote_name, transport->remote->name)) {\n+\t\tif (tracks_this_remote) {\n \t\t\tint i;\n \t\t\tfor (i = 0; i < branch->merge_nr; i++) {\n \t\t\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\ndiff --git a/t/t5537-fetch-shallow.sh b/t/t5537-fetch-shallow.sh\nindex f323ceebd2..8143af9fc3 100755\n--- a/t/t5537-fetch-shallow.sh\n+++ b/t/t5537-fetch-shallow.sh\n@@ -13,6 +13,24 @@ commit() {\n \tgit commit -m \"$1\"\n }\n \n+check_upstream_refs () {\n+\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream/ >actual &&\n+\tcat >expect &&\n+\ttest_cmp expect actual\n+}\n+\n+check_upstream_head () {\n+\tgit symbolic-ref refs/remotes/upstream/HEAD >actual &&\n+\techo \"refs/remotes/upstream/$1\" >expect &&\n+\ttest_cmp expect actual\n+}\n+\n+check_same_tip () {\n+\tgit log --oneline -1 \"$1\" >expect &&\n+\tgit -C \"$2\" log --oneline -1 \"$3\" >actual &&\n+\ttest_cmp expect actual\n+}\n+\n test_expect_success 'setup' '\n \tcommit 1 &&\n \tcommit 2 &&\n@@ -261,6 +279,134 @@ test_expect_success 'fetch --deepen does not truncate' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'fetch.shallow setup' '\n+\tgit branch narrow-side &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\t.git narrow-default &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\t.git narrow-enabled &&\n+\t(\n+\t\tcd narrow-default &&\n+\t\tgit remote add upstream ../.git &&\n+\t\tgit fetch --depth=1 upstream main:refs/remotes/upstream/main &&\n+\t\tgit branch --set-upstream-to=upstream/main main\n+\t) &&\n+\t(\n+\t\tcd narrow-enabled &&\n+\t\tgit remote add upstream ../.git &&\n+\t\tgit fetch --depth=1 upstream main:refs/remotes/upstream/main &&\n+\t\tgit branch --set-upstream-to=upstream/main main &&\n+\t\tgit config fetch.shallow true\n+\t)\n+'\n+\n+test_expect_success 'a refspec-less fetch expands to the configured refspec by default' '\n+\t(\n+\t\tcd narrow-default &&\n+\t\tgit fetch upstream &&\n+\t\tcheck_upstream_refs <<-\\EOF\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\trefs/remotes/upstream/narrow-side\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'fetch.shallow=true limits a refspec-less fetch to the tracked branch' '\n+\t(\n+\t\tcd narrow-enabled &&\n+\t\tgit fetch upstream &&\n+\t\tcheck_upstream_refs <<-\\EOF\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'fetch.shallow=true still creates refs/remotes/<remote>/HEAD' '\n+\t(\n+\t\tcd narrow-enabled &&\n+\t\tgit symbolic-ref -d refs/remotes/upstream/HEAD &&\n+\t\tgit fetch upstream &&\n+\t\tcheck_upstream_head main\n+\t)\n+'\n+\n+test_expect_success 'fetch.shallow=true with followRemoteHEAD=always corrects a stale HEAD' '\n+\ttest_when_finished \\\n+\t\t\"git -C narrow-enabled update-ref -d refs/remotes/upstream/stale-branch\" &&\n+\t(\n+\t\tcd narrow-enabled &&\n+\t\tgit update-ref refs/remotes/upstream/stale-branch refs/remotes/upstream/main &&\n+\t\tgit symbolic-ref refs/remotes/upstream/HEAD refs/remotes/upstream/stale-branch &&\n+\t\tgit -c fetch.followRemoteHEAD=always fetch upstream &&\n+\t\tcheck_upstream_head main\n+\t)\n+'\n+\n+test_expect_success 'fetch.shallow=true still updates the tracked branch' '\n+\tcommit 5 &&\n+\tgit -C narrow-enabled fetch upstream &&\n+\tcheck_same_tip main narrow-enabled refs/remotes/upstream/main\n+'\n+\n+test_expect_success 'fetch.shallow=true keeps git pull narrowed too' '\n+\ttest_when_finished \"git branch -D narrow-side\" &&\n+\tcommit 6 &&\n+\t(\n+\t\tcd narrow-enabled &&\n+\t\tgit pull &&\n+\t\tcheck_upstream_refs <<-\\EOF\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\tEOF\n+\t) &&\n+\tcheck_same_tip main narrow-enabled HEAD\n+'\n+\n+test_expect_success 'fetch.shallow=true has no effect on a non-shallow repository' '\n+\tgit clone --no-local --branch main --single-branch .git narrow-full &&\n+\t(\n+\t\tcd narrow-full &&\n+\t\tgit rev-parse --is-shallow-repository >actual &&\n+\t\techo false >expect &&\n+\t\ttest_cmp expect actual &&\n+\t\tgit remote add upstream ../.git &&\n+\t\tgit fetch upstream &&\n+\t\tgit branch --set-upstream-to=upstream/main main &&\n+\t\tgit config fetch.shallow true\n+\t) &&\n+\ttest_when_finished \"git branch -D narrow-full-side\" &&\n+\tgit branch narrow-full-side &&\n+\t(\n+\t\tcd narrow-full &&\n+\t\tgit fetch upstream &&\n+\t\tcheck_upstream_refs <<-\\EOF\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\trefs/remotes/upstream/narrow-full-side\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'fetch.shallow=true only narrows a fetch of the tracked remote' '\n+\ttest_when_finished \"git branch -D other-side\" &&\n+\tgit branch other-side &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\t.git narrow-other-remote &&\n+\t(\n+\t\tcd narrow-other-remote &&\n+\t\tgit remote add upstream ../.git &&\n+\t\tgit config fetch.shallow true &&\n+\t\tgit fetch upstream &&\n+\t\tcheck_upstream_refs <<-\\EOF\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\trefs/remotes/upstream/other-side\n+\t\tEOF\n+\t)\n+'\n+\n . \"$TEST_DIRECTORY\"/lib-httpd.sh\n start_httpd\n \n\nbase-commit: d38352cd43ab9745686d697872408bc3249a153f\n-- \ngitgitgadget\n"},{"id":"552930","messageId":"7f084e4d-f738-4bd4-9b4d-cad995f04be8@gmail.com","threadId":"66353","inReplyTo":"pull.2412.git.git.1789829246437.gitgitgadget@gmail.com","subject":"Re: [PATCH] fetch: add config to avoid fetching every branch in shallow repo","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-21T13:28:08Z","receivedAt":"2026-09-21T13:28:13Z","isPatch":true,"body":"Hi Harald\n\nOn 19/09/2026 15:47, Harald Nordgren via GitGitGadget wrote:\n> From: Harald Nordgren <haraldnordgren@gmail.com>\n> \n> In a shallow, sparsely checked out clone of a repository with many\n> branches, plain git pull can take minutes or hang outright, even\n> though only one branch is actually being worked on.\n\nI think the sparse checkout is irrelevant? It is unclear to me if this \nis talking about a case where there are many branches in the remote \nrepository and only one of them was cloned, then adding a second remote \ncreated a wildcard fetch refspec; or if there are intentionally lots of \nremote tracking branches in the local repository and you don't want to \nwait for them all to update. If it is the former then we should think \nhow we can improve the behavior of \"git remote add\" in a sparse \nrepository to prevent it adding a wildcard fetch refspec and instead \nsetup the new remote to fetch only the branch(es) we're interested in.\n\nThanks\n\nPhillip\n\n> Add fetch.shallow, off by default. When enabled, a fetch or pull for\n> a shallow repository that isn't already scoped to specific refs\n> fetches only the current branch's tracked upstream, instead of every\n> branch the remote has. git pull ultimately runs such a fetch under\n> the hood, so this fixes pull the same way. It has no effect once the\n> repository is no longer shallow, and no effect on a fetch of a remote\n> the current branch doesn't track, both fall back to the existing\n> behavior.\n> \n> This is opt-in rather than automatic because it changes what a plain\n> fetch or pull leaves in refs/remotes/<name>/ for anyone who currently\n> relies on it syncing every branch of a shallow remote, not just the\n> one they are on. Scoping remote.<name>.fetch by hand already covers\n> this for a single remote, but that requires knowing the config exists\n> and applies it permanently, even to branches that are not currently\n> checked out.\n> \n> The remote's recorded default branch (remotes/<name>/HEAD) is kept up\n> to date the same way it always is, only the other branches are\n> skipped.\n> \n> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n> ---\n>      fetch: add fetch.shallow so pull doesn't fetch every branch on shallow\n>      repo\n>      \n>      Add fetch.shallow config for big shallow repo, so git fetch/pull doesn't\n>      hang by fetching every branch.\n> \n> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2412%2FHaraldNordgren%2Ffetch-shallow-narrow-refspec-v1\n> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2412/HaraldNordgren/fetch-shallow-narrow-refspec-v1\n> Pull-Request: https://github.com/git/git/pull/2412\n> \n>   Documentation/config/fetch.adoc |  15 ++++\n>   builtin/fetch.c                 |  18 +++-\n>   t/t5537-fetch-shallow.sh        | 146 ++++++++++++++++++++++++++++++++\n>   3 files changed, 175 insertions(+), 4 deletions(-)\n> \n> diff --git a/Documentation/config/fetch.adoc b/Documentation/config/fetch.adoc\n> index 00435e9a16..e63e54a463 100644\n> --- a/Documentation/config/fetch.adoc\n> +++ b/Documentation/config/fetch.adoc\n> @@ -145,3 +145,18 @@ remove the value for the `fetch.bundleCreationToken` value before fetching.\n>   `never`;;\n>   \tNever create or modify the `remotes/<name>/HEAD` symbolic-ref.\n>   --\n> +\n> +`fetch.shallow`::\n> +\tIf true, and the repository is a shallow repository (see\n> +\tlinkgit:git-clone[1] `--depth`), a fetch or `git pull` that names no\n> +\texplicit refspec and would otherwise fall back to the remote's\n> +\tconfigured `remote.<name>.fetch` refspec instead fetches only the\n> +\tcurrent branch's upstream, when that upstream is on the remote being\n> +\tfetched. This avoids negotiating history for every branch the remote\n> +\tadvertises, which can be slow on a shallow repository that tracks\n> +\tmany disjoint shallow histories. It has no effect on a fetch that\n> +\tnames an explicit remote or refspec, and no effect on a repository\n> +\tthat is not shallow. Defaults to false.\n> ++\n> +`remotes/<name>/HEAD` is still kept up to date per `fetch.followRemoteHEAD`\n> +while this is in effect, only the other branches are skipped.\n> diff --git a/builtin/fetch.c b/builtin/fetch.c\n> index 533fdfe7d8..b22f7fe5f4 100644\n> --- a/builtin/fetch.c\n> +++ b/builtin/fetch.c\n> @@ -111,6 +111,7 @@ struct fetch_config {\n>   \tint recurse_submodules;\n>   \tint parallel;\n>   \tint submodule_fetch_jobs;\n> +\tint shallow;\n>   };\n>   \n>   static int git_fetch_config(const char *k, const char *v,\n> @@ -175,6 +176,11 @@ static int git_fetch_config(const char *k, const char *v,\n>   \t\treturn 0;\n>   \t}\n>   \n> +\tif (!strcmp(k, \"fetch.shallow\")) {\n> +\t\tfetch_config->shallow = git_config_bool(k, v);\n> +\t\treturn 0;\n> +\t}\n> +\n>   \tif (!strcmp(k, \"fetch.followremotehead\")) {\n>   \t\tif (!v)\n>   \t\t\treturn config_error_nonbool(k);\n> @@ -1958,15 +1964,19 @@ static int do_fetch(struct transport *transport,\n>   \t\trefspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);\n>   \t} else {\n>   \t\tstruct branch *branch = branch_get(NULL);\n> +\t\tint tracks_this_remote = branch && branch_has_merge_config(branch) &&\n> +\t\t\t!strcmp(branch->remote_name, transport->remote->name);\n> +\t\tint narrow_to_tracked_ref = config->shallow &&\n> +\t\t\tis_repository_shallow(the_repository) && tracks_this_remote;\n>   \n>   \t\tif (transport->remote->fetch.nr) {\n> -\t\t\trefspec_ref_prefixes(&transport->remote->fetch,\n> -\t\t\t\t\t     &transport_ls_refs_options.ref_prefixes);\n> +\t\t\tif (!narrow_to_tracked_ref)\n> +\t\t\t\trefspec_ref_prefixes(&transport->remote->fetch,\n> +\t\t\t\t\t\t     &transport_ls_refs_options.ref_prefixes);\n>   \t\t\tif (follow_remote_head != FOLLOW_REMOTE_NEVER)\n>   \t\t\t\tdo_set_head = 1;\n>   \t\t}\n> -\t\tif (branch && branch_has_merge_config(branch) &&\n> -\t\t    !strcmp(branch->remote_name, transport->remote->name)) {\n> +\t\tif (tracks_this_remote) {\n>   \t\t\tint i;\n>   \t\t\tfor (i = 0; i < branch->merge_nr; i++) {\n>   \t\t\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n> diff --git a/t/t5537-fetch-shallow.sh b/t/t5537-fetch-shallow.sh\n> index f323ceebd2..8143af9fc3 100755\n> --- a/t/t5537-fetch-shallow.sh\n> +++ b/t/t5537-fetch-shallow.sh\n> @@ -13,6 +13,24 @@ commit() {\n>   \tgit commit -m \"$1\"\n>   }\n>   \n> +check_upstream_refs () {\n> +\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream/ >actual &&\n> +\tcat >expect &&\n> +\ttest_cmp expect actual\n> +}\n> +\n> +check_upstream_head () {\n> +\tgit symbolic-ref refs/remotes/upstream/HEAD >actual &&\n> +\techo \"refs/remotes/upstream/$1\" >expect &&\n> +\ttest_cmp expect actual\n> +}\n> +\n> +check_same_tip () {\n> +\tgit log --oneline -1 \"$1\" >expect &&\n> +\tgit -C \"$2\" log --oneline -1 \"$3\" >actual &&\n> +\ttest_cmp expect actual\n> +}\n> +\n>   test_expect_success 'setup' '\n>   \tcommit 1 &&\n>   \tcommit 2 &&\n> @@ -261,6 +279,134 @@ test_expect_success 'fetch --deepen does not truncate' '\n>   \ttest_cmp expect actual\n>   '\n>   \n> +test_expect_success 'fetch.shallow setup' '\n> +\tgit branch narrow-side &&\n> +\tgit clone --no-local --depth=1 --branch main --single-branch \\\n> +\t\t.git narrow-default &&\n> +\tgit clone --no-local --depth=1 --branch main --single-branch \\\n> +\t\t.git narrow-enabled &&\n> +\t(\n> +\t\tcd narrow-default &&\n> +\t\tgit remote add upstream ../.git &&\n> +\t\tgit fetch --depth=1 upstream main:refs/remotes/upstream/main &&\n> +\t\tgit branch --set-upstream-to=upstream/main main\n> +\t) &&\n> +\t(\n> +\t\tcd narrow-enabled &&\n> +\t\tgit remote add upstream ../.git &&\n> +\t\tgit fetch --depth=1 upstream main:refs/remotes/upstream/main &&\n> +\t\tgit branch --set-upstream-to=upstream/main main &&\n> +\t\tgit config fetch.shallow true\n> +\t)\n> +'\n> +\n> +test_expect_success 'a refspec-less fetch expands to the configured refspec by default' '\n> +\t(\n> +\t\tcd narrow-default &&\n> +\t\tgit fetch upstream &&\n> +\t\tcheck_upstream_refs <<-\\EOF\n> +\t\trefs/remotes/upstream/HEAD\n> +\t\trefs/remotes/upstream/main\n> +\t\trefs/remotes/upstream/narrow-side\n> +\t\tEOF\n> +\t)\n> +'\n> +\n> +test_expect_success 'fetch.shallow=true limits a refspec-less fetch to the tracked branch' '\n> +\t(\n> +\t\tcd narrow-enabled &&\n> +\t\tgit fetch upstream &&\n> +\t\tcheck_upstream_refs <<-\\EOF\n> +\t\trefs/remotes/upstream/HEAD\n> +\t\trefs/remotes/upstream/main\n> +\t\tEOF\n> +\t)\n> +'\n> +\n> +test_expect_success 'fetch.shallow=true still creates refs/remotes/<remote>/HEAD' '\n> +\t(\n> +\t\tcd narrow-enabled &&\n> +\t\tgit symbolic-ref -d refs/remotes/upstream/HEAD &&\n> +\t\tgit fetch upstream &&\n> +\t\tcheck_upstream_head main\n> +\t)\n> +'\n> +\n> +test_expect_success 'fetch.shallow=true with followRemoteHEAD=always corrects a stale HEAD' '\n> +\ttest_when_finished \\\n> +\t\t\"git -C narrow-enabled update-ref -d refs/remotes/upstream/stale-branch\" &&\n> +\t(\n> +\t\tcd narrow-enabled &&\n> +\t\tgit update-ref refs/remotes/upstream/stale-branch refs/remotes/upstream/main &&\n> +\t\tgit symbolic-ref refs/remotes/upstream/HEAD refs/remotes/upstream/stale-branch &&\n> +\t\tgit -c fetch.followRemoteHEAD=always fetch upstream &&\n> +\t\tcheck_upstream_head main\n> +\t)\n> +'\n> +\n> +test_expect_success 'fetch.shallow=true still updates the tracked branch' '\n> +\tcommit 5 &&\n> +\tgit -C narrow-enabled fetch upstream &&\n> +\tcheck_same_tip main narrow-enabled refs/remotes/upstream/main\n> +'\n> +\n> +test_expect_success 'fetch.shallow=true keeps git pull narrowed too' '\n> +\ttest_when_finished \"git branch -D narrow-side\" &&\n> +\tcommit 6 &&\n> +\t(\n> +\t\tcd narrow-enabled &&\n> +\t\tgit pull &&\n> +\t\tcheck_upstream_refs <<-\\EOF\n> +\t\trefs/remotes/upstream/HEAD\n> +\t\trefs/remotes/upstream/main\n> +\t\tEOF\n> +\t) &&\n> +\tcheck_same_tip main narrow-enabled HEAD\n> +'\n> +\n> +test_expect_success 'fetch.shallow=true has no effect on a non-shallow repository' '\n> +\tgit clone --no-local --branch main --single-branch .git narrow-full &&\n> +\t(\n> +\t\tcd narrow-full &&\n> +\t\tgit rev-parse --is-shallow-repository >actual &&\n> +\t\techo false >expect &&\n> +\t\ttest_cmp expect actual &&\n> +\t\tgit remote add upstream ../.git &&\n> +\t\tgit fetch upstream &&\n> +\t\tgit branch --set-upstream-to=upstream/main main &&\n> +\t\tgit config fetch.shallow true\n> +\t) &&\n> +\ttest_when_finished \"git branch -D narrow-full-side\" &&\n> +\tgit branch narrow-full-side &&\n> +\t(\n> +\t\tcd narrow-full &&\n> +\t\tgit fetch upstream &&\n> +\t\tcheck_upstream_refs <<-\\EOF\n> +\t\trefs/remotes/upstream/HEAD\n> +\t\trefs/remotes/upstream/main\n> +\t\trefs/remotes/upstream/narrow-full-side\n> +\t\tEOF\n> +\t)\n> +'\n> +\n> +test_expect_success 'fetch.shallow=true only narrows a fetch of the tracked remote' '\n> +\ttest_when_finished \"git branch -D other-side\" &&\n> +\tgit branch other-side &&\n> +\tgit clone --no-local --depth=1 --branch main --single-branch \\\n> +\t\t.git narrow-other-remote &&\n> +\t(\n> +\t\tcd narrow-other-remote &&\n> +\t\tgit remote add upstream ../.git &&\n> +\t\tgit config fetch.shallow true &&\n> +\t\tgit fetch upstream &&\n> +\t\tcheck_upstream_refs <<-\\EOF\n> +\t\trefs/remotes/upstream/HEAD\n> +\t\trefs/remotes/upstream/main\n> +\t\trefs/remotes/upstream/other-side\n> +\t\tEOF\n> +\t)\n> +'\n> +\n>   . \"$TEST_DIRECTORY\"/lib-httpd.sh\n>   start_httpd\n>   \n> \n> base-commit: d38352cd43ab9745686d697872408bc3249a153f\n\n"},{"id":"552953","messageId":"CAHwyqnWbbCK40qAU1vhCFmN86J-dCY3tv_N_nBYgK2Bxf3o1fg@mail.gmail.com","threadId":"66353","inReplyTo":"7f084e4d-f738-4bd4-9b4d-cad995f04be8@gmail.com","subject":"Re: [PATCH] fetch: add config to avoid fetching every branch in shallow repo","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-09-21T21:45:45Z","receivedAt":"2026-09-21T21:46:24Z","isPatch":true,"body":"On Mon, Sep 21, 2026 at 3:28 PM Phillip Wood <phillip.wood123@gmail.com> wrote:\n>\n> Hi Harald\n>\n> On 19/09/2026 15:47, Harald Nordgren via GitGitGadget wrote:\n> > From: Harald Nordgren <haraldnordgren@gmail.com>\n> >\n> > In a shallow, sparsely checked out clone of a repository with many\n> > branches, plain git pull can take minutes or hang outright, even\n> > though only one branch is actually being worked on.\n>\n> I think the sparse checkout is irrelevant? It is unclear to me if this\n> is talking about a case where there are many branches in the remote\n> repository and only one of them was cloned, then adding a second remote\n> created a wildcard fetch refspec\n\nYeah likely. I cloned my fork of homebrew-core with `--depth=1` and\nthen added real upstream as a remote. Then I made my work on a feature\nbranch.\n\nThen tried to set my branch to track upstream, but it didn't work.\n\nThen 'git pull' and it hung.\n\n> If it is the former then we should think\n> how we can improve the behavior of \"git remote add\" in a sparse\n> repository to prevent it adding a wildcard fetch refspec and instead\n> setup the new remote to fetch only the branch(es) we're interested in.\n\nI didn't even consider that, that sounds like an interesting idea!\n\n\nHarald\n"},{"id":"552982","messageId":"CAHwyqnUVkVUjKCiN7YT3tkvHEVCQwb3jDGM0OTojPxptFKrsLQ@mail.gmail.com","threadId":"66353","inReplyTo":"CAHwyqnWbbCK40qAU1vhCFmN86J-dCY3tv_N_nBYgK2Bxf3o1fg@mail.gmail.com","subject":"Re: [PATCH] fetch: add config to avoid fetching every branch in shallow repo","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-09-22T13:00:11Z","receivedAt":"2026-09-22T13:00:51Z","isPatch":true,"body":"> > If it is the former then we should think\n> > how we can improve the behavior of \"git remote add\" in a sparse\n> > repository to prevent it adding a wildcard fetch refspec and instead\n> > setup the new remote to fetch only the branch(es) we're interested in.\n>\n> I didn't even consider that, that sounds like an interesting idea!\n\nWould you suggest we do that instead of this, or in addition to this\nfix?\n\nSeems maybe we only need the fix on \"git remote add\".\n\n\n\n\nHarald\n"},{"id":"552995","messageId":"15c85d2d-4fb0-427d-b52e-8e4dd958dcb9@gmail.com","threadId":"66353","inReplyTo":"CAHwyqnUVkVUjKCiN7YT3tkvHEVCQwb3jDGM0OTojPxptFKrsLQ@mail.gmail.com","subject":"Re: [PATCH] fetch: add config to avoid fetching every branch in shallow repo","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-22T14:53:36Z","receivedAt":"2026-09-22T14:53:44Z","isPatch":true,"body":"On 22/09/2026 14:00, Harald Nordgren wrote:\n>>> If it is the former then we should think\n>>> how we can improve the behavior of \"git remote add\" in a sparse\n>>> repository to prevent it adding a wildcard fetch refspec and instead\n>>> setup the new remote to fetch only the branch(es) we're interested in.\n>>\n>> I didn't even consider that, that sounds like an interesting idea!\n> \n> Would you suggest we do that instead of this, or in addition to this\n> fix?\n\nI'd say instead - if we can fix \"git remote add\" then the problem \nshouldn't arise in the first place which is better than having an opt-in \nconfig option that most users are probably unaware of.\n\nThanks\n\nPhillip\n\n> Seems maybe we only need the fix on \"git remote add\".\n> \n> \n> \n> \n> Harald\n\n"},{"id":"552997","messageId":"CAHwyqnUjDJNjER2AxFXOR6G3sOJKpsXj-agzmm7410_QAFu8PQ@mail.gmail.com","threadId":"66353","inReplyTo":"15c85d2d-4fb0-427d-b52e-8e4dd958dcb9@gmail.com","subject":"Re: [PATCH] fetch: add config to avoid fetching every branch in shallow repo","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-09-22T15:37:17Z","receivedAt":"2026-09-22T15:37:56Z","isPatch":true,"body":"On Tue, Sep 22, 2026 at 4:53 PM Phillip Wood <phillip.wood123@gmail.com> wrote:\n>\n> On 22/09/2026 14:00, Harald Nordgren wrote:\n> >>> If it is the former then we should think\n> >>> how we can improve the behavior of \"git remote add\" in a sparse\n> >>> repository to prevent it adding a wildcard fetch refspec and instead\n> >>> setup the new remote to fetch only the branch(es) we're interested in.\n> >>\n> >> I didn't even consider that, that sounds like an interesting idea!\n> >\n> > Would you suggest we do that instead of this, or in addition to this\n> > fix?\n>\n> I'd say instead - if we can fix \"git remote add\" then the problem\n> shouldn't arise in the first place which is better than having an opt-in\n> config option that most users are probably unaware of.\n\nThat makes a lot of sense.\n\nHowever, I think there is info missing on the local side that we can't\nget without making a network call to the newly added remote, like its\nbranches (and default branch). How do we handle that?\n\n\nHarald\n"},{"id":"553003","messageId":"xmqqh5jhfbyw.fsf@gitster.g","threadId":"66353","inReplyTo":"7f084e4d-f738-4bd4-9b4d-cad995f04be8@gmail.com","subject":"Re: [PATCH] fetch: add config to avoid fetching every branch in shallow repo","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-22T17:11:03Z","receivedAt":"2026-09-22T17:11:05Z","isPatch":true,"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> I think the sparse checkout is irrelevant? It is unclear to me if this \n> is talking about a case where there are many branches in the remote \n> repository and only one of them was cloned, then adding a second remote \n> created a wildcard fetch refspec; or if there are intentionally lots of \n> remote tracking branches in the local repository and you don't want to \n> wait for them all to update. If it is the former then we should think \n> how we can improve the behavior of \"git remote add\" in a sparse \n> repository to prevent it adding a wildcard fetch refspec and instead \n> setup the new remote to fetch only the branch(es) we're interested in.\n\nVery good suggestions.  \"Avoid wildcards\" is easy, but designing a\nsuitable alternative (\"only the ones we are interested in\") may be\nharder.\n\nPerhaps we want to have something similar in spirit to the\n\"matching\" mode 'git push' has, where the set of local branches we\nhave defines the set of branches we are interested in?  That is,\nwhen 'remote.*.fetch' is configured to signal that special mode,\n'git fetch' would:\n\n - Find each local branch that has its '@{upstream}' set to a branch\n   at the remote we are fetching from.\n\n - Fetch these branches at the remote that our local branches care\n   about.\n\nI said \"in spirit\" above, and I find it tempting to use ':' and '+:'\nas the special '<refspec>' to trigger this mode, to mimic what 'git\npush' does when using the local branches we have as the set of\nbranches we care about.  But there are important differences:\n\n (1) The correspondence between local and remote-tracking branches\n     is not one-to-one, as you can fork multiple local topics out\n     of the same upstream branch.  Maybe our 7 local branches build\n     on top of only 2 branches we fetch from the remote, for\n     example.\n\n (2) Corollary.  Unlike the matching mode in 'git push' where local\n     branch 'B' is used to update branch 'B' at the remote (if it\n     exists), this new mode in 'git fetch' only uses local branches\n     as a guide to determine which branches to fetch from the\n     remote.  If our local branch 'B' builds on top of branch 'U' at\n     the remote, it is branch 'U' we fetch and store as the\n     'refs/remotes/R/U' remote-tracking branch, where 'R' is the\n     remote, and 'B' as the name does not get anywhere in the\n     picture.\n\nIn other words, this is not \"matching\" at all, even though it takes\ninspiration from it.  I do not know what it should be called, but I\nthink it would be a useful addition.\n\nThanks.\n"},{"id":"553021","messageId":"CAHwyqnVwQRv3TKDtkZ2iAO3hNtOeC464qdv6GCxFYE_v-rLq6Q@mail.gmail.com","threadId":"66353","inReplyTo":"xmqqh5jhfbyw.fsf@gitster.g","subject":"Re: [PATCH] fetch: add config to avoid fetching every branch in shallow repo","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-09-22T21:40:09Z","receivedAt":"2026-09-22T21:40:51Z","isPatch":true,"body":"On Tue, Sep 22, 2026 at 7:11 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Phillip Wood <phillip.wood123@gmail.com> writes:\n>\n> > I think the sparse checkout is irrelevant? It is unclear to me if this\n> > is talking about a case where there are many branches in the remote\n> > repository and only one of them was cloned, then adding a second remote\n> > created a wildcard fetch refspec; or if there are intentionally lots of\n> > remote tracking branches in the local repository and you don't want to\n> > wait for them all to update. If it is the former then we should think\n> > how we can improve the behavior of \"git remote add\" in a sparse\n> > repository to prevent it adding a wildcard fetch refspec and instead\n> > setup the new remote to fetch only the branch(es) we're interested in.\n>\n> Very good suggestions.  \"Avoid wildcards\" is easy, but designing a\n> suitable alternative (\"only the ones we are interested in\") may be\n> harder.\n>\n> Perhaps we want to have something similar in spirit to the\n> \"matching\" mode 'git push' has, where the set of local branches we\n> have defines the set of branches we are interested in?  That is,\n> when 'remote.*.fetch' is configured to signal that special mode,\n> 'git fetch' would:\n>\n>  - Find each local branch that has its '@{upstream}' set to a branch\n>    at the remote we are fetching from.\n>\n>  - Fetch these branches at the remote that our local branches care\n>    about.\n>\n> I said \"in spirit\" above, and I find it tempting to use ':' and '+:'\n> as the special '<refspec>' to trigger this mode, to mimic what 'git\n> push' does when using the local branches we have as the set of\n> branches we care about.  But there are important differences:\n>\n>  (1) The correspondence between local and remote-tracking branches\n>      is not one-to-one, as you can fork multiple local topics out\n>      of the same upstream branch.  Maybe our 7 local branches build\n>      on top of only 2 branches we fetch from the remote, for\n>      example.\n>\n>  (2) Corollary.  Unlike the matching mode in 'git push' where local\n>      branch 'B' is used to update branch 'B' at the remote (if it\n>      exists), this new mode in 'git fetch' only uses local branches\n>      as a guide to determine which branches to fetch from the\n>      remote.  If our local branch 'B' builds on top of branch 'U' at\n>      the remote, it is branch 'U' we fetch and store as the\n>      'refs/remotes/R/U' remote-tracking branch, where 'R' is the\n>      remote, and 'B' as the name does not get anywhere in the\n>      picture.\n>\n> In other words, this is not \"matching\" at all, even though it takes\n> inspiration from it.  I do not know what it should be called, but I\n> think it would be a useful addition.\n\nVery interesting idea! I would like to suggest that the default branch\nof the upstream is always included as well, even if it's not the\nupstream of any branch yet.\n\nI run this on every repo I work with\n\n    git branch --set-upstream-to=upstream # upstream's default branch\n\nand noticed on the shallow repo that it didn't work unless I first ran this\n\n    git remote set-head upstream --auto\n\nwhich was very annoying.\n\n\n\nHarald\n"},{"id":"553066","messageId":"c5710264-d901-4b23-81f9-fc69f8268826@gmail.com","threadId":"66353","inReplyTo":"CAHwyqnUjDJNjER2AxFXOR6G3sOJKpsXj-agzmm7410_QAFu8PQ@mail.gmail.com","subject":"Re: [PATCH] fetch: add config to avoid fetching every branch in shallow repo","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-23T15:14:32Z","receivedAt":"2026-09-23T15:14:38Z","isPatch":true,"body":"Hi Harald\n\nOn 22/09/2026 16:37, Harald Nordgren wrote:\n> On Tue, Sep 22, 2026 at 4:53 PM Phillip Wood <phillip.wood123@gmail.com> wrote:\n>>\n>> On 22/09/2026 14:00, Harald Nordgren wrote:\n>>>>> If it is the former then we should think\n>>>>> how we can improve the behavior of \"git remote add\" in a sparse\n>>>>> repository to prevent it adding a wildcard fetch refspec and instead\n>>>>> setup the new remote to fetch only the branch(es) we're interested in.\n>>>>\n>>>> I didn't even consider that, that sounds like an interesting idea!\n>>>\n>>> Would you suggest we do that instead of this, or in addition to this\n>>> fix?\n>>\n>> I'd say instead - if we can fix \"git remote add\" then the problem\n>> shouldn't arise in the first place which is better than having an opt-in\n>> config option that most users are probably unaware of.\n> \n> That makes a lot of sense.\n> \n> However, I think there is info missing on the local side that we can't\n> get without making a network call to the newly added remote, like its\n> branches (and default branch). How do we handle that?\n\nThere is a \"--track\" option for \"git remote add\" to set up the branches \nthat are fetched. Without \"--fetch\" we could refuse to add a new remote \nin a shallow repository unless the user tells us which branches to \ntrack. That is a bit of a pain, but I think it is better than adding a \nwildcard refspec that makes \"git fetch\" unusably slow. We could also \nperhaps look at the refspecs for the existing remotes and, if they are \nall fetching the same branch(es?) copy those.\n\nWith \"--fetch\" we could lookup the default branch on the remote and \nfetch that if \"--track\" isn't given.\n\nThanks\n\nPhillip\n"},{"id":"553068","messageId":"05d0e6e0-e156-4a2a-95a0-4986ab18ef49@gmail.com","threadId":"66353","inReplyTo":"xmqqh5jhfbyw.fsf@gitster.g","subject":"Re: [PATCH] fetch: add config to avoid fetching every branch in shallow repo","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-09-23T15:19:06Z","receivedAt":"2026-09-23T15:19:12Z","isPatch":true,"body":"On 22/09/2026 18:11, Junio C Hamano wrote:\n> Phillip Wood <phillip.wood123@gmail.com> writes:\n> \n>> I think the sparse checkout is irrelevant? It is unclear to me if this\n>> is talking about a case where there are many branches in the remote\n>> repository and only one of them was cloned, then adding a second remote\n>> created a wildcard fetch refspec; or if there are intentionally lots of\n>> remote tracking branches in the local repository and you don't want to\n>> wait for them all to update. If it is the former then we should think\n>> how we can improve the behavior of \"git remote add\" in a sparse\n>> repository to prevent it adding a wildcard fetch refspec and instead\n>> setup the new remote to fetch only the branch(es) we're interested in.\n> \n> Very good suggestions.  \"Avoid wildcards\" is easy, but designing a\n> suitable alternative (\"only the ones we are interested in\") may be\n> harder.\n> \n> Perhaps we want to have something similar in spirit to the\n> \"matching\" mode 'git push' has, where the set of local branches we\n> have defines the set of branches we are interested in?  That is,\n> when 'remote.*.fetch' is configured to signal that special mode,\n> 'git fetch' would:\n> \n>   - Find each local branch that has its '@{upstream}' set to a branch\n>     at the remote we are fetching from.\n> \n>   - Fetch these branches at the remote that our local branches care\n>     about.\n\nI can see that being useful fetch mode for a remote that we've already \nfetched from, but for a newly added remote there will be no local \nbranches with their upstream set to it because \"git branch \n--set-upstream-to\" fails if the upstream does not already exist. So I \nlike the idea for fetching from existing remotes, but it leaves us with \na chicken-and-egg problem when adding new remotes, so I'm not sure how \nit would work in practice.\n\nThanks\n\nPhillip\n  > I said \"in spirit\" above, and I find it tempting to use ':' and '+:'\n> as the special '<refspec>' to trigger this mode, to mimic what 'git\n> push' does when using the local branches we have as the set of\n> branches we care about.  But there are important differences:\n> \n>   (1) The correspondence between local and remote-tracking branches\n>       is not one-to-one, as you can fork multiple local topics out\n>       of the same upstream branch.  Maybe our 7 local branches build\n>       on top of only 2 branches we fetch from the remote, for\n>       example.\n> \n>   (2) Corollary.  Unlike the matching mode in 'git push' where local\n>       branch 'B' is used to update branch 'B' at the remote (if it\n>       exists), this new mode in 'git fetch' only uses local branches\n>       as a guide to determine which branches to fetch from the\n>       remote.  If our local branch 'B' builds on top of branch 'U' at\n>       the remote, it is branch 'U' we fetch and store as the\n>       'refs/remotes/R/U' remote-tracking branch, where 'R' is the\n>       remote, and 'B' as the name does not get anywhere in the\n>       picture.\n> \n> In other words, this is not \"matching\" at all, even though it takes\n> inspiration from it.  I do not know what it should be called, but I\n> think it would be a useful addition.\n> \n> Thanks.\n\n"},{"id":"553069","messageId":"xmqq5wzwc76w.fsf@gitster.g","threadId":"66353","inReplyTo":"05d0e6e0-e156-4a2a-95a0-4986ab18ef49@gmail.com","subject":"Re: [PATCH] fetch: add config to avoid fetching every branch in shallow repo","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-23T15:34:47Z","receivedAt":"2026-09-23T15:34:50Z","isPatch":true,"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n>> when 'remote.*.fetch' is configured to signal that special mode,\n>> 'git fetch' would:\n>> \n>>   - Find each local branch that has its '@{upstream}' set to a branch\n>>     at the remote we are fetching from.\n>> \n>>   - Fetch these branches at the remote that our local branches care\n>>     about.\n>\n> I can see that being useful fetch mode for a remote that we've already \n> fetched from, but for a newly added remote there will be no local \n> branches with their upstream set to it because \"git branch \n> --set-upstream-to\" fails if the upstream does not already exist. So I \n> like the idea for fetching from existing remotes, but it leaves us with \n> a chicken-and-egg problem when adding new remotes, so I'm not sure how \n> it would work in practice.\n\nJust like with \"git push there :\", you prime the pump by explicitly\ndoing something (for \"push\", you do \"git push there mine\" to express\nyour preference to work with branch 'mine' and share it with the\nremote).\n\nSo if we are to allow customizing the refspec used for fetch with\n\"git remote add\", you might do:\n\n    $ git remote add --fetch=: second https://ho.st/git/second\n\nwhich creates:\n\n        [remote \"second\"]\n                url = https://ho.st/git/second\n                fetch = :\n\n(Note: I am not sure if \":\" is a good special token to express this\nmode of fetching, as I said earlier).\n\nYour initial \"git fetch second\" without any other arguments will be\na no-op.  You may decide to work on top of their 'main' branch by\nrunning:\n\n    $ git fetch second main:refs/remotes/second/main\n    $ git checkout -b topic -t second/main\n\nAt that point, the local branch 'topic' is built on top of their\n'main' branch by having its @{upstream} set to that remote-tracking\nbranch.  After that, running \"git fetch\" will update 'second/main' and\nno other remote-tracking branch.\n"},{"id":"553073","messageId":"CALnO6CA2DXvyOO+fu04sozg2=E0JoymAqyhs_heHzExgRSEzVw@mail.gmail.com","threadId":"66353","inReplyTo":"xmqq5wzwc76w.fsf@gitster.g","subject":"Re: [PATCH] fetch: add config to avoid fetching every branch in shallow repo","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-09-23T16:55:07Z","receivedAt":"2026-09-23T16:55:19Z","isPatch":true,"body":"On Wed, Sep 23, 2026 at 11:49 AM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Phillip Wood <phillip.wood123@gmail.com> writes:\n>\n> >> when 'remote.*.fetch' is configured to signal that special mode,\n> >> 'git fetch' would:\n> >>\n> >>   - Find each local branch that has its '@{upstream}' set to a branch\n> >>     at the remote we are fetching from.\n> >>\n> >>   - Fetch these branches at the remote that our local branches care\n> >>     about.\n> >\n> > I can see that being useful fetch mode for a remote that we've already\n> > fetched from, but for a newly added remote there will be no local\n> > branches with their upstream set to it because \"git branch\n> > --set-upstream-to\" fails if the upstream does not already exist. So I\n> > like the idea for fetching from existing remotes, but it leaves us with\n> > a chicken-and-egg problem when adding new remotes, so I'm not sure how\n> > it would work in practice.\n>\n> Just like with \"git push there :\", you prime the pump by explicitly\n> doing something (for \"push\", you do \"git push there mine\" to express\n> your preference to work with branch 'mine' and share it with the\n> remote).\n>\n> So if we are to allow customizing the refspec used for fetch with\n> \"git remote add\", you might do:\n>\n>     $ git remote add --fetch=: second https://ho.st/git/second\n>\n> which creates:\n>\n>         [remote \"second\"]\n>                 url = https://ho.st/git/second\n>                 fetch = :\n>\n> (Note: I am not sure if \":\" is a good special token to express this\n> mode of fetching, as I said earlier).\n>\n> Your initial \"git fetch second\" without any other arguments will be\n> a no-op.  You may decide to work on top of their 'main' branch by\n> running:\n>\n>     $ git fetch second main:refs/remotes/second/main\n\nI've wanted something similar for notes, so allow me to interject from\nthe sidelines: it would be even nicer to still have the ability to map\nfetches (so that \"git fetch second main\" did the right thing, creating\na useful remote tracking branch with the usual hierarchy;\nconfigurable, of course) on top of this. So, the first \"git fetch\nsecond\" does nothing, but \"git fetch second main\" + creating the\nbranch does as you describe.\n\n>     $ git checkout -b topic -t second/main\n>\n> At that point, the local branch 'topic' is built on top of their\n> 'main' branch by having its @{upstream} set to that remote-tracking\n> branch.  After that, running \"git fetch\" will update 'second/main' and\n> no other remote-tracking branch.\n\n\n-- \nD. Ben Knoble\n"},{"id":"553105","messageId":"xmqqbj9nagt2.fsf@gitster.g","threadId":"66353","inReplyTo":"CALnO6CA2DXvyOO+fu04sozg2=E0JoymAqyhs_heHzExgRSEzVw@mail.gmail.com","subject":"Re: [PATCH] fetch: add config to avoid fetching every branch in shallow repo","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-23T19:50:01Z","receivedAt":"2026-09-23T19:50:06Z","isPatch":true,"body":"\"D. Ben Knoble\" <ben.knoble@gmail.com> writes:\n\n>>     $ git fetch second main:refs/remotes/second/main\n>\n> I've wanted something similar for notes, so allow me to interject from\n> the sidelines: it would be even nicer to still have the ability to map\n> fetches (so that \"git fetch second main\" did the right thing, creating\n\nThe thing is, the command line\n\n    $ git fetch second main\n\nhas been used for the past 20 years as a \"single-shot fetch\" syntax\nthat expresses that the user does not intend to keep interacting\nwith the same 'main' branch or even the same 'second' repository,\nand for the \"single-shot fetch\", it is absolutely the wrong thing to\ncreate a remote-tracking branch.\n\nIt would be even worse if we created a remote 'second' and\nremote-tracking branch 'refs/remotes/second/main' when you ran\n\n    $ git fetch https://ho.st/second main\n\nHaving said that, I suspect that the fact that you have the\nshorthand 'second' (i.e., you have \"[remote \"second\"] url = ...\"\ndefined) may be a good enough sign that you expect to keep\ninteracting with that repository, and some people might appreciate\nit if\n\n    $ git fetch second main\n\ncreated a remote-tracking branch \"refs/remotes/second/main\"\nautomatically.\n\nBut we cannot suddenly start doing so without breaking people's\nexpectations, and without a good transition plan.  We need at least\nan escape hatch for users to say \"No, this is a single-shot fetch;\ndo not write the object anywhere other than FETCH_HEAD as we have\nalways done\".\n"},{"id":"553109","messageId":"pull.2412.v2.git.git.1790195720941.gitgitgadget@gmail.com","threadId":"66353","inReplyTo":"pull.2412.git.git.1789829246437.gitgitgadget@gmail.com","subject":"[PATCH v2] fetch: avoid fetching every branch of a new remote in a shallow repo","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-23T20:35:20Z","receivedAt":"2026-09-23T20:35:25Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\ngit remote add sets a new remote up to fetch every branch by default.\nIn an already shallow repository, that turns the next plain fetch or\npull into a slow or hanging one, even when only one or two branches\nare ever used.\n\nAdd a special fetch refspec, \"+:\", that fetches only the branches our\nlocal branches are built on, plus the remote's default branch so a\nbrand new remote is usable right away, without needing to first set\nanything up to track it. git remote add now uses it instead of the\nusual wildcard refspec whenever the repository is already shallow.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n    fetch: avoid fetching every branch of a new remote in a shallow repo\n    \n    Avoid fetching every branch of a new remote in a shallow repo.\n    \n    Changes in v2:\n    \n     * Replaced the opt-in fetch.shallow config entirely with a new special\n       fetch refspec (+:) that git remote add now defaults new remotes to in\n       a shallow repository. The new refspec fetches whichever branches any\n       local branch tracks at that remote, plus the remote's default branch.\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2412%2FHaraldNordgren%2Ffetch-shallow-narrow-refspec-v2\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2412/HaraldNordgren/fetch-shallow-narrow-refspec-v2\nPull-Request: https://github.com/git/git/pull/2412\n\nRange-diff vs v1:\n\n 1:  ec05d3e2d6 ! 1:  f04f5ce219 fetch: add config to avoid fetching every branch in shallow repo\n     @@ Metadata\n      Author: Harald Nordgren <haraldnordgren@gmail.com>\n      \n       ## Commit message ##\n     -    fetch: add config to avoid fetching every branch in shallow repo\n     +    fetch: avoid fetching every branch of a new remote in a shallow repo\n      \n     -    In a shallow, sparsely checked out clone of a repository with many\n     -    branches, plain git pull can take minutes or hang outright, even\n     -    though only one branch is actually being worked on.\n     +    git remote add sets a new remote up to fetch every branch by default.\n     +    In an already shallow repository, that turns the next plain fetch or\n     +    pull into a slow or hanging one, even when only one or two branches\n     +    are ever used.\n      \n     -    Add fetch.shallow, off by default. When enabled, a fetch or pull for\n     -    a shallow repository that isn't already scoped to specific refs\n     -    fetches only the current branch's tracked upstream, instead of every\n     -    branch the remote has. git pull ultimately runs such a fetch under\n     -    the hood, so this fixes pull the same way. It has no effect once the\n     -    repository is no longer shallow, and no effect on a fetch of a remote\n     -    the current branch doesn't track, both fall back to the existing\n     -    behavior.\n     -\n     -    This is opt-in rather than automatic because it changes what a plain\n     -    fetch or pull leaves in refs/remotes/<name>/ for anyone who currently\n     -    relies on it syncing every branch of a shallow remote, not just the\n     -    one they are on. Scoping remote.<name>.fetch by hand already covers\n     -    this for a single remote, but that requires knowing the config exists\n     -    and applies it permanently, even to branches that are not currently\n     -    checked out.\n     -\n     -    The remote's recorded default branch (remotes/<name>/HEAD) is kept up\n     -    to date the same way it always is, only the other branches are\n     -    skipped.\n     +    Add a special fetch refspec, \"+:\", that fetches only the branches our\n     +    local branches are built on, plus the remote's default branch so a\n     +    brand new remote is usable right away, without needing to first set\n     +    anything up to track it. git remote add now uses it instead of the\n     +    usual wildcard refspec whenever the repository is already shallow.\n      \n          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n      \n     - ## Documentation/config/fetch.adoc ##\n     -@@ Documentation/config/fetch.adoc: remove the value for the `fetch.bundleCreationToken` value before fetching.\n     - `never`;;\n     - \tNever create or modify the `remotes/<name>/HEAD` symbolic-ref.\n     - --\n     -+\n     -+`fetch.shallow`::\n     -+\tIf true, and the repository is a shallow repository (see\n     -+\tlinkgit:git-clone[1] `--depth`), a fetch or `git pull` that names no\n     -+\texplicit refspec and would otherwise fall back to the remote's\n     -+\tconfigured `remote.<name>.fetch` refspec instead fetches only the\n     -+\tcurrent branch's upstream, when that upstream is on the remote being\n     -+\tfetched. This avoids negotiating history for every branch the remote\n     -+\tadvertises, which can be slow on a shallow repository that tracks\n     -+\tmany disjoint shallow histories. It has no effect on a fetch that\n     -+\tnames an explicit remote or refspec, and no effect on a repository\n     -+\tthat is not shallow. Defaults to false.\n     + ## Documentation/git-remote.adoc ##\n     +@@ Documentation/git-remote.adoc: Add a remote named _<name>_ for the repository at\n     + _<URL>_.  The command `git fetch <name>` can then be used to create and\n     + update remote-tracking branches `<name>/<branch>`.\n     + +\n     ++If the repository is already a shallow repository (see linkgit:git-clone[1]\n     ++`--depth`) and neither `-t` nor `--mirror` is given, the remote is set up\n     ++with the special tracking refspec (see the _<refspec>_ entry in\n     ++linkgit:git-fetch[1]) instead of the default glob refspec, so that\n     ++`git fetch <name>` does not need to negotiate history for every branch\n     ++the remote has.\n     +++\n     + With `-f` option, `git fetch <name>` is run immediately after\n     + the remote information is set up.\n     + +\n     +\n     + ## Documentation/pull-fetch-param.adoc ##\n     +@@ Documentation/pull-fetch-param.adoc: Negative refspecs can themselves be pattern refspecs. However, they may only\n     + contain a _<src>_ and do not specify a _<dst>_. Fully spelled out hex object\n     + names are also not supported.\n     + +\n     ++The special refspec `:` (or `+:` to allow non-fast-forward updates)\n     ++directs Git to fetch whichever branches at the remote our local branches\n     ++have as their `@{upstream}`, plus the remote's default branch, which is\n     ++always included so that it is available even before anything is set up\n     ++to track it. Unlike the same syntax when pushing, the local branch name\n     ++plays no role here beyond identifying which remote branch to fetch: the\n     ++result is written to `refs/remotes/<remote>/<branch>` using the remote's\n     ++own branch name, and several local branches built on the same remote\n     ++branch only cause it to be fetched once.\n      ++\n     -+`remotes/<name>/HEAD` is still kept up to date per `fetch.followRemoteHEAD`\n     -+while this is in effect, only the other branches are skipped.\n     + `tag <tag>` means the same as `refs/tags/<tag>:refs/tags/<tag>`;\n     + it requests fetching everything up to the given tag.\n     + +\n      \n       ## builtin/fetch.c ##\n     -@@ builtin/fetch.c: struct fetch_config {\n     - \tint recurse_submodules;\n     - \tint parallel;\n     - \tint submodule_fetch_jobs;\n     -+\tint shallow;\n     - };\n     - \n     - static int git_fetch_config(const char *k, const char *v,\n     -@@ builtin/fetch.c: static int git_fetch_config(const char *k, const char *v,\n     - \t\treturn 0;\n     +@@ builtin/fetch.c: static void filter_prefetch_refspec(struct refspec *rs)\n       \t}\n     + }\n       \n     -+\tif (!strcmp(k, \"fetch.shallow\")) {\n     -+\t\tfetch_config->shallow = git_config_bool(k, v);\n     -+\t\treturn 0;\n     ++static void fetch_tracked_branches(struct remote *remote,\n     ++\t\t\t\t   const struct ref *remote_refs,\n     ++\t\t\t\t   struct ref ***tail,\n     ++\t\t\t\t   char **default_branch_out,\n     ++\t\t\t\t   int force)\n     ++{\n     ++\tstruct string_list tracked = STRING_LIST_INIT_DUP;\n     ++\tstruct string_list_item *item;\n     ++\tstruct ref *head;\n     ++\tchar *default_branch = NULL;\n     ++\n     ++\tbranches_tracking_remote(remote, &tracked);\n     ++\n     ++\thead = get_remote_ref(remote_refs, \"HEAD\");\n     ++\tif (head && head->symref && *head->symref) {\n     ++\t\tdefault_branch = xstrdup(head->symref);\n     ++\t\tstring_list_insert(&tracked, default_branch);\n     ++\t}\n     ++\tfree_one_ref(head);\n     ++\n     ++\tfor_each_string_list_item(item, &tracked) {\n     ++\t\tstruct refspec_item ref = { .force = force };\n     ++\t\tconst char *branch_name = item->string;\n     ++\t\tint is_default_branch = default_branch &&\n     ++\t\t\t!strcmp(item->string, default_branch);\n     ++\n     ++\t\tskip_prefix(branch_name, \"refs/heads/\", &branch_name);\n     ++\t\tref.src = xstrdup(is_default_branch ? \"HEAD\" : item->string);\n     ++\t\tref.dst = xstrfmt(\"refs/remotes/%s/%s\", remote->name, branch_name);\n     ++\t\tget_fetch_map(remote_refs, &ref, tail, 1);\n     ++\t\tfree(ref.src);\n     ++\t\tfree(ref.dst);\n     ++\t}\n     ++\n     ++\tif (default_branch && default_branch_out) {\n     ++\t\tconst char *branch_name = default_branch;\n     ++\t\tskip_prefix(branch_name, \"refs/heads/\", &branch_name);\n     ++\t\t*default_branch_out = xstrdup(branch_name);\n      +\t}\n      +\n     - \tif (!strcmp(k, \"fetch.followremotehead\")) {\n     - \t\tif (!v)\n     - \t\t\treturn config_error_nonbool(k);\n     ++\tfree(default_branch);\n     ++\tstring_list_clear(&tracked, 0);\n     ++}\n     ++\n     + static struct ref *get_ref_map(struct remote *remote,\n     + \t\t\t       const struct ref *remote_refs,\n     + \t\t\t       struct refspec *rs,\n     +-\t\t\t       int tags, int *autotags)\n     ++\t\t\t       int tags, int *autotags,\n     ++\t\t\t       char **bootstrap_head_branch)\n     + {\n     + \tint i;\n     + \tstruct ref *rm;\n     +@@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,\n     + \t\t     /* Note: has_merge implies non-NULL branch->remote_name */\n     + \t\t     (has_merge && !strcmp(branch->remote_name, remote->name)))) {\n     + \t\t\tfor (i = 0; i < remote->fetch.nr; i++) {\n     ++\t\t\t\tif (remote->fetch.items[i].tracking) {\n     ++\t\t\t\t\tfetch_tracked_branches(remote, remote_refs, &tail,\n     ++\t\t\t\t\t\t\t\tbootstrap_head_branch,\n     ++\t\t\t\t\t\t\t\tremote->fetch.items[i].force);\n     ++\t\t\t\t\tcontinue;\n     ++\t\t\t\t}\n     + \t\t\t\tget_fetch_map(remote_refs, &remote->fetch.items[i], &tail, 0);\n     + \t\t\t\tif (remote->fetch.items[i].dst &&\n     + \t\t\t\t    remote->fetch.items[i].dst[0])\n     +@@ builtin/fetch.c: static void warn_set_head(const char *remote, const char *head_name,\n     + }\n     + \n     + static int set_head(const struct ref *remote_refs, struct remote *remote,\n     +-\t\t\tint follow_remote_head)\n     ++\t\t\tint follow_remote_head, const char *known_head_branch)\n     + {\n     + \tint result = 0, create_only, baremirror, was_detached;\n     + \tstruct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,\n     + \t\t      b_local_head = STRBUF_INIT;\n     + \tconst char *no_warn_branch = remote->no_warn_branch;\n     + \tchar *head_name = NULL;\n     +-\tstruct ref *ref, *matches;\n     ++\tstruct ref *ref, *matches = NULL;\n     + \tstruct ref *fetch_map = NULL, **fetch_map_tail = &fetch_map;\n     + \tstruct refspec_item refspec = {\n     + \t\t.force = 0,\n     +@@ builtin/fetch.c: static int set_head(const struct ref *remote_refs, struct remote *remote,\n     + \tstruct string_list heads = STRING_LIST_INIT_DUP;\n     + \tstruct ref_store *refs = get_main_ref_store(the_repository);\n     + \n     +-\tget_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);\n     +-\tmatches = guess_remote_head(find_ref_by_name(remote_refs, \"HEAD\"),\n     +-\t\t\t\t    fetch_map, REMOTE_GUESS_HEAD_ALL);\n     +-\tfor (ref = matches; ref; ref = ref->next) {\n     +-\t\tstring_list_append(&heads, strip_refshead(ref->name));\n     +-\t}\n     ++\tif (known_head_branch) {\n     ++\t\thead_name = xstrdup(known_head_branch);\n     ++\t} else {\n     ++\t\tget_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);\n     ++\t\tmatches = guess_remote_head(find_ref_by_name(remote_refs, \"HEAD\"),\n     ++\t\t\t\t\t    fetch_map, REMOTE_GUESS_HEAD_ALL);\n     ++\t\tfor (ref = matches; ref; ref = ref->next) {\n     ++\t\t\tstring_list_append(&heads, strip_refshead(ref->name));\n     ++\t\t}\n     + \n     +-\tif (!heads.nr)\n     +-\t\tresult = 1;\n     +-\telse if (heads.nr > 1)\n     +-\t\tresult = 1;\n     +-\telse\n     +-\t\thead_name = xstrdup(heads.items[0].string);\n     ++\t\tif (!heads.nr)\n     ++\t\t\tresult = 1;\n     ++\t\telse if (heads.nr > 1)\n     ++\t\t\tresult = 1;\n     ++\t\telse\n     ++\t\t\thead_name = xstrdup(heads.items[0].string);\n     ++\t}\n     + \n     + \tif (!head_name)\n     + \t\tgoto cleanup;\n     +@@ builtin/fetch.c: static int do_fetch(struct transport *transport,\n     + \tstruct strmap rejected_refs = STRMAP_INIT;\n     + \tint summary_width = 0;\n     + \tint follow_remote_head;\n     ++\tchar *bootstrap_head_branch = NULL;\n     + \n     + \tif (tags == TAGS_DEFAULT) {\n     + \t\tif (transport->remote->fetch_tags == 2)\n      @@ builtin/fetch.c: static int do_fetch(struct transport *transport,\n       \t\trefspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);\n       \t} else {\n       \t\tstruct branch *branch = branch_get(NULL);\n      +\t\tint tracks_this_remote = branch && branch_has_merge_config(branch) &&\n      +\t\t\t!strcmp(branch->remote_name, transport->remote->name);\n     -+\t\tint narrow_to_tracked_ref = config->shallow &&\n     -+\t\t\tis_repository_shallow(the_repository) && tracks_this_remote;\n     ++\t\tint tracking_refspec = transport->remote->fetch.nr == 1 &&\n     ++\t\t\ttransport->remote->fetch.items[0].tracking;\n       \n     - \t\tif (transport->remote->fetch.nr) {\n     --\t\t\trefspec_ref_prefixes(&transport->remote->fetch,\n     --\t\t\t\t\t     &transport_ls_refs_options.ref_prefixes);\n     -+\t\t\tif (!narrow_to_tracked_ref)\n     -+\t\t\t\trefspec_ref_prefixes(&transport->remote->fetch,\n     -+\t\t\t\t\t\t     &transport_ls_refs_options.ref_prefixes);\n     - \t\t\tif (follow_remote_head != FOLLOW_REMOTE_NEVER)\n     - \t\t\t\tdo_set_head = 1;\n     +-\t\tif (transport->remote->fetch.nr) {\n     ++\t\tif (tracking_refspec) {\n     ++\t\t\tstruct string_list tracked = STRING_LIST_INIT_DUP;\n     ++\t\t\tstruct string_list_item *item;\n     ++\n     ++\t\t\tbranches_tracking_remote(transport->remote, &tracked);\n     ++\t\t\tfor_each_string_list_item(item, &tracked)\n     ++\t\t\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n     ++\t\t\t\t\t    item->string);\n     ++\t\t\tstring_list_clear(&tracked, 0);\n     ++\t\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n     ++\t\t} else if (transport->remote->fetch.nr) {\n     + \t\t\trefspec_ref_prefixes(&transport->remote->fetch,\n     + \t\t\t\t\t     &transport_ls_refs_options.ref_prefixes);\n     +-\t\t\tif (follow_remote_head != FOLLOW_REMOTE_NEVER)\n     +-\t\t\t\tdo_set_head = 1;\n       \t\t}\n      -\t\tif (branch && branch_has_merge_config(branch) &&\n      -\t\t    !strcmp(branch->remote_name, transport->remote->name)) {\n     ++\t\tif (transport->remote->fetch.nr &&\n     ++\t\t    follow_remote_head != FOLLOW_REMOTE_NEVER)\n     ++\t\t\tdo_set_head = 1;\n      +\t\tif (tracks_this_remote) {\n       \t\t\tint i;\n       \t\t\tfor (i = 0; i < branch->merge_nr; i++) {\n       \t\t\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n     +@@ builtin/fetch.c: static int do_fetch(struct transport *transport,\n     + \ttransport_ls_refs_options_release(&transport_ls_refs_options);\n     + \n     + \tref_map = get_ref_map(transport->remote, remote_refs, rs,\n     +-\t\t\t      tags, &autotags);\n     ++\t\t\t      tags, &autotags, &bootstrap_head_branch);\n     ++\n     + \tif (!update_head_ok)\n     + \t\tcheck_not_current_branch(ref_map);\n     + \n     +@@ builtin/fetch.c: static int do_fetch(struct transport *transport,\n     + \t\t * Way too many cases where this can go wrong so let's just\n     + \t\t * ignore errors and fail silently for now.\n     + \t\t */\n     +-\t\tset_head(remote_refs, transport->remote, follow_remote_head);\n     ++\t\tset_head(remote_refs, transport->remote, follow_remote_head,\n     ++\t\t\t bootstrap_head_branch);\n     + \t}\n     + \n     + cleanup:\n     ++\tfree(bootstrap_head_branch);\n     + \t/*\n     + \t * When using batched updates, we want to commit the non-rejected\n     + \t * updates and also handle the rejections.\n      \n     - ## t/t5537-fetch-shallow.sh ##\n     -@@ t/t5537-fetch-shallow.sh: commit() {\n     - \tgit commit -m \"$1\"\n     + ## builtin/remote.c ##\n     +@@\n     + #include \"rebase.h\"\n     + #include \"refs.h\"\n     + #include \"refspec.h\"\n     ++#include \"shallow.h\"\n     + #include \"odb.h\"\n     + #include \"strvec.h\"\n     + #include \"commit-reach.h\"\n     +@@ builtin/remote.c: static void add_branch(const char *key, const char *branchname,\n     + \t\t       const char *remotename, int mirror, struct strbuf *tmp)\n     + {\n     + \tstrbuf_reset(tmp);\n     +-\tstrbuf_addch(tmp, '+');\n     +-\tif (mirror)\n     +-\t\tstrbuf_addf(tmp, \"refs/%s:refs/%s\",\n     +-\t\t\t\tbranchname, branchname);\n     +-\telse\n     +-\t\tstrbuf_addf(tmp, \"refs/heads/%s:refs/remotes/%s/%s\",\n     +-\t\t\t\tbranchname, remotename, branchname);\n     ++\tif (!strcmp(branchname, \":\")) {\n     ++\t\tstrbuf_addstr(tmp, \"+:\");\n     ++\t} else {\n     ++\t\tstrbuf_addch(tmp, '+');\n     ++\t\tif (mirror)\n     ++\t\t\tstrbuf_addf(tmp, \"refs/%s:refs/%s\",\n     ++\t\t\t\t\tbranchname, branchname);\n     ++\t\telse\n     ++\t\t\tstrbuf_addf(tmp, \"refs/heads/%s:refs/remotes/%s/%s\",\n     ++\t\t\t\t\tbranchname, remotename, branchname);\n     ++\t}\n     + \trepo_config_set_multivar(the_repository, key, tmp->buf, \"^$\", 0);\n       }\n       \n     -+check_upstream_refs () {\n     -+\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream/ >actual &&\n     -+\tcat >expect &&\n     -+\ttest_cmp expect actual\n     -+}\n     +@@ builtin/remote.c: static int add(int argc, const char **argv, const char *prefix,\n     + \t\tstrbuf_reset(&buf);\n     + \t\tstrbuf_addf(&buf, \"remote.%s.fetch\", name);\n     + \t\tif (track.nr == 0)\n     +-\t\t\tstring_list_append(&track, \"*\");\n     +-\t\tfor (size_t i = 0; i < track.nr; i++) {\n     ++\t\t\tstring_list_append(&track,\n     ++\t\t\t\tmirror == MIRROR_NONE &&\n     ++\t\t\t\tis_repository_shallow(the_repository) ?\n     ++\t\t\t\t\":\" : \"*\");\n     ++\t\tfor (size_t i = 0; i < track.nr; i++)\n     + \t\t\tadd_branch(buf.buf, track.items[i].string,\n     + \t\t\t\t   name, mirror, &buf2);\n     +-\t\t}\n     + \t}\n     + \n     + \tif (mirror & MIRROR_PUSH) {\n     +\n     + ## refspec.c ##\n     +@@ refspec.c: static int parse_refspec(struct refspec_item *item, const char *refspec,\n     + \t\treturn 1;\n     + \t}\n     + \n     ++\tif (fetch && rhs == lhs && rhs[1] == '\\0') {\n     ++\t\titem->tracking = 1;\n     ++\t\treturn 1;\n     ++\t}\n     ++\n     + \tif (rhs) {\n     + \t\tsize_t rlen = strlen(++rhs);\n     + \t\tis_glob = (1 <= rlen && strchr(rhs, '*'));\n     +@@ refspec.c: void refspec_item_clear(struct refspec_item *item)\n     + \titem->force = 0;\n     + \titem->pattern = 0;\n     + \titem->matching = 0;\n     ++\titem->tracking = 0;\n     + \titem->exact_sha1 = 0;\n     + }\n     + \n     +@@ refspec.c: static int refspec_find_negative_match(struct refspec *rs, struct refspec_item *\n     + \t\tstruct refspec_item *refspec = &rs->items[i];\n     + \t\tchar *expn_name;\n     + \n     +-\t\tif (refspec->negative)\n     ++\t\tif (refspec->negative || refspec->tracking)\n     + \t\t\tcontinue;\n     + \n     + \t\t/* Note the reversal of src and dst */\n     +\n     + ## refspec.h ##\n     +@@ refspec.h: struct refspec_item {\n     + \tunsigned matching : 1;\n     + \tunsigned exact_sha1 : 1;\n     + \tunsigned negative : 1;\n     ++\tunsigned tracking : 1;\n     + \n     + \tchar *src;\n     + \tchar *dst;\n     +\n     + ## remote.c ##\n     +@@ remote.c: void ref_push_report_free(struct ref_push_report *report)\n     + \n     + int remote_find_tracking(struct remote *remote, struct refspec_item *refspec)\n     + {\n     ++\tif (remote->fetch.nr == 1 && remote->fetch.items[0].tracking) {\n     ++\t\tstruct strbuf prefix = STRBUF_INIT;\n     ++\t\tconst char *branch_name;\n     ++\t\tint ret = -1;\n     ++\n     ++\t\tstrbuf_addf(&prefix, \"refs/remotes/%s/\", remote->name);\n     ++\t\tif (!refspec->src) {\n     ++\t\t\tif (refspec->dst &&\n     ++\t\t\t    skip_prefix(refspec->dst, prefix.buf, &branch_name)) {\n     ++\t\t\t\trefspec->src = xstrfmt(\"refs/heads/%s\", branch_name);\n     ++\t\t\t\trefspec->force = 1;\n     ++\t\t\t\tret = 0;\n     ++\t\t\t}\n     ++\t\t} else if (skip_prefix(refspec->src, \"refs/heads/\", &branch_name)) {\n     ++\t\t\tfree(refspec->dst);\n     ++\t\t\trefspec->dst = xstrfmt(\"%s%s\", prefix.buf, branch_name);\n     ++\t\t\trefspec->force = 1;\n     ++\t\t\tret = 0;\n     ++\t\t}\n     ++\t\tstrbuf_release(&prefix);\n     ++\t\treturn ret;\n     ++\t}\n     ++\n     + \treturn refspec_find_match(&remote->fetch, refspec);\n     + }\n     + \n     +@@ remote.c: int branch_merge_matches(struct branch *branch,\n     + \treturn refname_match(branch->merge[i]->src, refname);\n     + }\n     + \n     ++struct branches_tracking_remote_cb_data {\n     ++\tstruct remote *remote;\n     ++\tstruct string_list *tracked;\n     ++};\n     ++\n     ++static int add_if_tracking_remote(const struct reference *ref, void *cb_data)\n     ++{\n     ++\tstruct branches_tracking_remote_cb_data *data = cb_data;\n     ++\tstruct branch *branch;\n     ++\n     ++\tbranch = branch_get(ref->name);\n     ++\tif (!branch_has_merge_config(branch) ||\n     ++\t    strcmp(branch->remote_name, data->remote->name))\n     ++\t\treturn 0;\n      +\n     -+check_upstream_head () {\n     -+\tgit symbolic-ref refs/remotes/upstream/HEAD >actual &&\n     -+\techo \"refs/remotes/upstream/$1\" >expect &&\n     -+\ttest_cmp expect actual\n     ++\tfor (int i = 0; i < branch->merge_nr; i++)\n     ++\t\tstring_list_insert(data->tracked, branch->merge[i]->src);\n     ++\n     ++\treturn 0;\n      +}\n      +\n     -+check_same_tip () {\n     -+\tgit log --oneline -1 \"$1\" >expect &&\n     -+\tgit -C \"$2\" log --oneline -1 \"$3\" >actual &&\n     -+\ttest_cmp expect actual\n     ++void branches_tracking_remote(struct remote *remote, struct string_list *tracked)\n     ++{\n     ++\tstruct branches_tracking_remote_cb_data data = { remote, tracked };\n     ++\n     ++\trefs_for_each_branch_ref(get_main_ref_store(the_repository),\n     ++\t\t\t\t  add_if_tracking_remote, &data);\n      +}\n      +\n     - test_expect_success 'setup' '\n     - \tcommit 1 &&\n     - \tcommit 2 &&\n     -@@ t/t5537-fetch-shallow.sh: test_expect_success 'fetch --deepen does not truncate' '\n     - \ttest_cmp expect actual\n     + __attribute__((format (printf,2,3)))\n     + static char *error_buf(struct strbuf *err, const char *fmt, ...)\n     + {\n     +@@ remote.c: int get_fetch_map(const struct ref *remote_refs,\n     + {\n     + \tstruct ref *ref_map, **rmp;\n     + \n     +-\tif (refspec->negative)\n     ++\tif (refspec->negative || refspec->tracking)\n     + \t\treturn 0;\n     + \n     + \tif (refspec->pattern) {\n     +\n     + ## remote.h ##\n     +@@ remote.h: int branch_has_merge_config(struct branch *branch);\n     + \n     + int branch_merge_matches(struct branch *, int n, const char *);\n     + \n     ++/* fills tracked with the refname of every local branch's upstream on remote */\n     ++void branches_tracking_remote(struct remote *remote, struct string_list *tracked);\n     ++\n     + /* list of the remote in a group as configured */\n     + struct remote_group_data {\n     + \tconst char *name;\n     +\n     + ## t/meson.build ##\n     +@@ t/meson.build: integration_tests = [\n     +   't5582-fetch-negative-refspec.sh',\n     +   't5583-push-branches.sh',\n     +   't5584-http-429-retry.sh',\n     ++  't5585-fetch-tracking-refspec.sh',\n     +   't5600-clone-fail-cleanup.sh',\n     +   't5601-clone.sh',\n     +   't5602-clone-remote-exec.sh',\n     +\n     + ## t/t5505-remote.sh ##\n     +@@ t/t5505-remote.sh: test_expect_success 'filters are listed by git remote -v only' '\n     + \ttest_grep ! \"\\[blob:none\\]\" out\n       '\n       \n     -+test_expect_success 'fetch.shallow setup' '\n     -+\tgit branch narrow-side &&\n     -+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n     -+\t\t.git narrow-default &&\n     ++test_expect_success 'add remote -t keeps an explicit refspec in a shallow repository' '\n     ++\ttest_when_finished \"rm -rf shallow-add\" &&\n      +\tgit clone --no-local --depth=1 --branch main --single-branch \\\n     -+\t\t.git narrow-enabled &&\n     -+\t(\n     -+\t\tcd narrow-default &&\n     -+\t\tgit remote add upstream ../.git &&\n     -+\t\tgit fetch --depth=1 upstream main:refs/remotes/upstream/main &&\n     -+\t\tgit branch --set-upstream-to=upstream/main main\n     -+\t) &&\n     ++\t\tone shallow-add &&\n      +\t(\n     -+\t\tcd narrow-enabled &&\n     -+\t\tgit remote add upstream ../.git &&\n     -+\t\tgit fetch --depth=1 upstream main:refs/remotes/upstream/main &&\n     -+\t\tgit branch --set-upstream-to=upstream/main main &&\n     -+\t\tgit config fetch.shallow true\n     ++\t\tcd shallow-add &&\n     ++\t\tgit remote add -t main upstream ../two &&\n     ++\t\ttest_cmp_config \"+refs/heads/main:refs/remotes/upstream/main\" \\\n     ++\t\t\tremote.upstream.fetch\n      +\t)\n      +'\n      +\n     -+test_expect_success 'a refspec-less fetch expands to the configured refspec by default' '\n     ++test_expect_success 'add remote keeps the wildcard refspec in a full repository' '\n     ++\ttest_when_finished \"rm -rf full-add\" &&\n     ++\tgit clone --no-local one full-add &&\n      +\t(\n     -+\t\tcd narrow-default &&\n     -+\t\tgit fetch upstream &&\n     -+\t\tcheck_upstream_refs <<-\\EOF\n     -+\t\trefs/remotes/upstream/HEAD\n     -+\t\trefs/remotes/upstream/main\n     -+\t\trefs/remotes/upstream/narrow-side\n     -+\t\tEOF\n     ++\t\tcd full-add &&\n     ++\t\tgit remote add upstream ../two &&\n     ++\t\ttest_cmp_config \"+refs/heads/*:refs/remotes/upstream/*\" \\\n     ++\t\t\tremote.upstream.fetch\n      +\t)\n      +'\n      +\n     -+test_expect_success 'fetch.shallow=true limits a refspec-less fetch to the tracked branch' '\n     ++test_expect_success 'a remote added in a shallow repository defaults to the tracking refspec' '\n     ++\ttest_when_finished \"rm -rf shallow-add\" &&\n     ++\tgit clone --no-local --depth=1 --branch main --single-branch \\\n     ++\t\tone shallow-add &&\n      +\t(\n     -+\t\tcd narrow-enabled &&\n     ++\t\tcd shallow-add &&\n     ++\t\tgit remote add upstream ../two &&\n     ++\t\ttest_cmp_config \"+:\" remote.upstream.fetch &&\n      +\t\tgit fetch upstream &&\n     -+\t\tcheck_upstream_refs <<-\\EOF\n     ++\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n     ++\t\tcat >expect <<-\\EOF &&\n      +\t\trefs/remotes/upstream/HEAD\n      +\t\trefs/remotes/upstream/main\n      +\t\tEOF\n     ++\t\ttest_cmp expect actual &&\n     ++\t\tgit branch --set-upstream-to=upstream &&\n     ++\t\ttest_cmp_config upstream branch.main.remote &&\n     ++\t\ttest_cmp_config refs/heads/main branch.main.merge\n      +\t)\n      +'\n      +\n     -+test_expect_success 'fetch.shallow=true still creates refs/remotes/<remote>/HEAD' '\n     + test_expect_success 'check remote-tracking' '\n     + \t(\n     + \t\tcd test &&\n     +\n     + ## t/t5585-fetch-tracking-refspec.sh (new) ##\n     +@@\n     ++#!/bin/sh\n     ++\n     ++test_description='\"git fetch\" with the tracking refspec \":\" (or \"+:\")\n     ++\n     ++The special refspec fetches whichever branches at the remote our local\n     ++branches have as their @{upstream}, plus the default branch of that\n     ++remote, which is always included so that it is available even before\n     ++anything is set up to track it. git remote add uses it by default for\n     ++a new remote in an already shallow repository, so that adding a\n     ++second remote there does not turn the next plain fetch or pull into\n     ++one that negotiates history for every branch the remote has.\n     ++'\n     ++\n     ++GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n     ++export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n     ++\n     ++. ./test-lib.sh\n     ++\n     ++test_expect_success 'setup' '\n     ++\ttest_commit main-1 &&\n     ++\ttest_commit main-2 &&\n     ++\tgit checkout -b side main-1 &&\n     ++\ttest_commit side-1 &&\n     ++\tgit checkout -b next main-1 &&\n     ++\ttest_commit next-1 &&\n     ++\tgit checkout main\n     ++'\n     ++\n     ++test_expect_success 'clone shallow and single-branch, then add a second remote' '\n     ++\tgit clone --no-local --depth=1 --branch main --single-branch . client &&\n      +\t(\n     -+\t\tcd narrow-enabled &&\n     -+\t\tgit symbolic-ref -d refs/remotes/upstream/HEAD &&\n     -+\t\tgit fetch upstream &&\n     -+\t\tcheck_upstream_head main\n     ++\t\tcd client &&\n     ++\t\tgit remote add upstream .. &&\n     ++\t\ttest_cmp_config \"+:\" remote.upstream.fetch\n      +\t)\n      +'\n      +\n     -+test_expect_success 'fetch.shallow=true with followRemoteHEAD=always corrects a stale HEAD' '\n     -+\ttest_when_finished \\\n     -+\t\t\"git -C narrow-enabled update-ref -d refs/remotes/upstream/stale-branch\" &&\n     ++test_expect_success 'fetching the new remote does not need every branch it has' '\n      +\t(\n     -+\t\tcd narrow-enabled &&\n     -+\t\tgit update-ref refs/remotes/upstream/stale-branch refs/remotes/upstream/main &&\n     -+\t\tgit symbolic-ref refs/remotes/upstream/HEAD refs/remotes/upstream/stale-branch &&\n     -+\t\tgit -c fetch.followRemoteHEAD=always fetch upstream &&\n     -+\t\tcheck_upstream_head main\n     ++\t\tcd client &&\n     ++\t\tgit fetch upstream &&\n     ++\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n     ++\t\tcat >expect <<-\\EOF &&\n     ++\t\trefs/remotes/upstream/HEAD\n     ++\t\trefs/remotes/upstream/main\n     ++\t\tEOF\n     ++\t\ttest_cmp expect actual\n      +\t)\n      +'\n      +\n     -+test_expect_success 'fetch.shallow=true still updates the tracked branch' '\n     -+\tcommit 5 &&\n     -+\tgit -C narrow-enabled fetch upstream &&\n     -+\tcheck_same_tip main narrow-enabled refs/remotes/upstream/main\n     ++test_expect_success 'set-upstream-to now resolves right after that first fetch' '\n     ++\t(\n     ++\t\tcd client &&\n     ++\t\tgit branch --set-upstream-to=upstream &&\n     ++\t\ttest_cmp_config upstream branch.main.remote &&\n     ++\t\ttest_cmp_config refs/heads/main branch.main.merge\n     ++\t)\n      +'\n      +\n     -+test_expect_success 'fetch.shallow=true keeps git pull narrowed too' '\n     -+\ttest_when_finished \"git branch -D narrow-side\" &&\n     -+\tcommit 6 &&\n     ++test_expect_success 'a branch checked out from a one-time fetch is kept updated by later plain fetches' '\n     ++\t(\n     ++\t\tcd client &&\n     ++\t\tgit fetch upstream side:refs/remotes/upstream/side &&\n     ++\t\tgit branch side-topic upstream/side\n     ++\t) &&\n     ++\tgit checkout side &&\n     ++\ttest_commit side-2 &&\n     ++\tgit checkout main &&\n      +\t(\n     -+\t\tcd narrow-enabled &&\n     -+\t\tgit pull &&\n     -+\t\tcheck_upstream_refs <<-\\EOF\n     ++\t\tcd client &&\n     ++\t\tgit fetch upstream &&\n     ++\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n     ++\t\tcat >expect <<-\\EOF &&\n      +\t\trefs/remotes/upstream/HEAD\n      +\t\trefs/remotes/upstream/main\n     ++\t\trefs/remotes/upstream/side\n      +\t\tEOF\n     -+\t) &&\n     -+\tcheck_same_tip main narrow-enabled HEAD\n     ++\t\ttest_cmp expect actual &&\n     ++\t\tgit rev-parse refs/remotes/upstream/side >actual-oid &&\n     ++\t\tgit -C .. rev-parse side >expect-oid &&\n     ++\t\ttest_cmp expect-oid actual-oid\n     ++\t)\n      +'\n      +\n     -+test_expect_success 'fetch.shallow=true has no effect on a non-shallow repository' '\n     -+\tgit clone --no-local --branch main --single-branch .git narrow-full &&\n     -+\t(\n     -+\t\tcd narrow-full &&\n     -+\t\tgit rev-parse --is-shallow-repository >actual &&\n     -+\t\techo false >expect &&\n     -+\t\ttest_cmp expect actual &&\n     -+\t\tgit remote add upstream ../.git &&\n     -+\t\tgit fetch upstream &&\n     -+\t\tgit branch --set-upstream-to=upstream/main main &&\n     -+\t\tgit config fetch.shallow true\n     -+\t) &&\n     -+\ttest_when_finished \"git branch -D narrow-full-side\" &&\n     -+\tgit branch narrow-full-side &&\n     ++test_expect_success 'a second branch tracking the same upstream branch does not fetch it twice' '\n      +\t(\n     -+\t\tcd narrow-full &&\n     ++\t\tcd client &&\n     ++\t\tgit branch side-topic-2 upstream/side &&\n      +\t\tgit fetch upstream &&\n     -+\t\tcheck_upstream_refs <<-\\EOF\n     ++\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n     ++\t\tcat >expect <<-\\EOF &&\n      +\t\trefs/remotes/upstream/HEAD\n      +\t\trefs/remotes/upstream/main\n     -+\t\trefs/remotes/upstream/narrow-full-side\n     ++\t\trefs/remotes/upstream/side\n      +\t\tEOF\n     ++\t\ttest_cmp expect actual\n      +\t)\n      +'\n      +\n     -+test_expect_success 'fetch.shallow=true only narrows a fetch of the tracked remote' '\n     -+\ttest_when_finished \"git branch -D other-side\" &&\n     -+\tgit branch other-side &&\n     -+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n     -+\t\t.git narrow-other-remote &&\n     ++test_expect_success 'a branch tracking a different remote is not fetched from upstream' '\n      +\t(\n     -+\t\tcd narrow-other-remote &&\n     -+\t\tgit remote add upstream ../.git &&\n     -+\t\tgit config fetch.shallow true &&\n     ++\t\tcd client &&\n     ++\t\tgit remote add other .. &&\n     ++\t\tgit fetch other next:refs/remotes/other/next &&\n     ++\t\tgit branch next-topic other/next &&\n      +\t\tgit fetch upstream &&\n     -+\t\tcheck_upstream_refs <<-\\EOF\n     ++\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n     ++\t\tcat >expect <<-\\EOF &&\n      +\t\trefs/remotes/upstream/HEAD\n      +\t\trefs/remotes/upstream/main\n     -+\t\trefs/remotes/upstream/other-side\n     ++\t\trefs/remotes/upstream/side\n      +\t\tEOF\n     ++\t\ttest_cmp expect actual\n      +\t)\n      +'\n      +\n     - . \"$TEST_DIRECTORY\"/lib-httpd.sh\n     - start_httpd\n     - \n     ++test_expect_success 'git remote show does not choke on the tracking refspec' '\n     ++\t(\n     ++\t\tcd client &&\n     ++\t\tgit remote show upstream\n     ++\t)\n     ++'\n     ++\n     ++test_done\n\n\n Documentation/git-remote.adoc       |   7 ++\n Documentation/pull-fetch-param.adoc |  10 +++\n builtin/fetch.c                     | 117 +++++++++++++++++++++-----\n builtin/remote.c                    |  27 +++---\n refspec.c                           |   8 +-\n refspec.h                           |   1 +\n remote.c                            |  54 +++++++++++-\n remote.h                            |   3 +\n t/meson.build                       |   1 +\n t/t5505-remote.sh                   |  44 ++++++++++\n t/t5585-fetch-tracking-refspec.sh   | 124 ++++++++++++++++++++++++++++\n 11 files changed, 362 insertions(+), 34 deletions(-)\n create mode 100755 t/t5585-fetch-tracking-refspec.sh\n\ndiff --git a/Documentation/git-remote.adoc b/Documentation/git-remote.adoc\nindex eaae30aa88..7856ccf54f 100644\n--- a/Documentation/git-remote.adoc\n+++ b/Documentation/git-remote.adoc\n@@ -52,6 +52,13 @@ Add a remote named _<name>_ for the repository at\n _<URL>_.  The command `git fetch <name>` can then be used to create and\n update remote-tracking branches `<name>/<branch>`.\n +\n+If the repository is already a shallow repository (see linkgit:git-clone[1]\n+`--depth`) and neither `-t` nor `--mirror` is given, the remote is set up\n+with the special tracking refspec (see the _<refspec>_ entry in\n+linkgit:git-fetch[1]) instead of the default glob refspec, so that\n+`git fetch <name>` does not need to negotiate history for every branch\n+the remote has.\n++\n With `-f` option, `git fetch <name>` is run immediately after\n the remote information is set up.\n +\ndiff --git a/Documentation/pull-fetch-param.adoc b/Documentation/pull-fetch-param.adoc\nindex d903dc8900..e1a42152d9 100644\n--- a/Documentation/pull-fetch-param.adoc\n+++ b/Documentation/pull-fetch-param.adoc\n@@ -48,6 +48,16 @@ Negative refspecs can themselves be pattern refspecs. However, they may only\n contain a _<src>_ and do not specify a _<dst>_. Fully spelled out hex object\n names are also not supported.\n +\n+The special refspec `:` (or `+:` to allow non-fast-forward updates)\n+directs Git to fetch whichever branches at the remote our local branches\n+have as their `@{upstream}`, plus the remote's default branch, which is\n+always included so that it is available even before anything is set up\n+to track it. Unlike the same syntax when pushing, the local branch name\n+plays no role here beyond identifying which remote branch to fetch: the\n+result is written to `refs/remotes/<remote>/<branch>` using the remote's\n+own branch name, and several local branches built on the same remote\n+branch only cause it to be fetched once.\n++\n `tag <tag>` means the same as `refs/tags/<tag>:refs/tags/<tag>`;\n it requests fetching everything up to the given tag.\n +\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 533fdfe7d8..7a3387d733 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -500,10 +500,55 @@ static void filter_prefetch_refspec(struct refspec *rs)\n \t}\n }\n \n+static void fetch_tracked_branches(struct remote *remote,\n+\t\t\t\t   const struct ref *remote_refs,\n+\t\t\t\t   struct ref ***tail,\n+\t\t\t\t   char **default_branch_out,\n+\t\t\t\t   int force)\n+{\n+\tstruct string_list tracked = STRING_LIST_INIT_DUP;\n+\tstruct string_list_item *item;\n+\tstruct ref *head;\n+\tchar *default_branch = NULL;\n+\n+\tbranches_tracking_remote(remote, &tracked);\n+\n+\thead = get_remote_ref(remote_refs, \"HEAD\");\n+\tif (head && head->symref && *head->symref) {\n+\t\tdefault_branch = xstrdup(head->symref);\n+\t\tstring_list_insert(&tracked, default_branch);\n+\t}\n+\tfree_one_ref(head);\n+\n+\tfor_each_string_list_item(item, &tracked) {\n+\t\tstruct refspec_item ref = { .force = force };\n+\t\tconst char *branch_name = item->string;\n+\t\tint is_default_branch = default_branch &&\n+\t\t\t!strcmp(item->string, default_branch);\n+\n+\t\tskip_prefix(branch_name, \"refs/heads/\", &branch_name);\n+\t\tref.src = xstrdup(is_default_branch ? \"HEAD\" : item->string);\n+\t\tref.dst = xstrfmt(\"refs/remotes/%s/%s\", remote->name, branch_name);\n+\t\tget_fetch_map(remote_refs, &ref, tail, 1);\n+\t\tfree(ref.src);\n+\t\tfree(ref.dst);\n+\t}\n+\n+\tif (default_branch && default_branch_out) {\n+\t\tconst char *branch_name = default_branch;\n+\t\tskip_prefix(branch_name, \"refs/heads/\", &branch_name);\n+\t\t*default_branch_out = xstrdup(branch_name);\n+\t}\n+\n+\tfree(default_branch);\n+\tstring_list_clear(&tracked, 0);\n+}\n+\n static struct ref *get_ref_map(struct remote *remote,\n \t\t\t       const struct ref *remote_refs,\n \t\t\t       struct refspec *rs,\n-\t\t\t       int tags, int *autotags)\n+\t\t\t       int tags, int *autotags,\n+\t\t\t       char **bootstrap_head_branch)\n {\n \tint i;\n \tstruct ref *rm;\n@@ -570,6 +615,12 @@ static struct ref *get_ref_map(struct remote *remote,\n \t\t     /* Note: has_merge implies non-NULL branch->remote_name */\n \t\t     (has_merge && !strcmp(branch->remote_name, remote->name)))) {\n \t\t\tfor (i = 0; i < remote->fetch.nr; i++) {\n+\t\t\t\tif (remote->fetch.items[i].tracking) {\n+\t\t\t\t\tfetch_tracked_branches(remote, remote_refs, &tail,\n+\t\t\t\t\t\t\t\tbootstrap_head_branch,\n+\t\t\t\t\t\t\t\tremote->fetch.items[i].force);\n+\t\t\t\t\tcontinue;\n+\t\t\t\t}\n \t\t\t\tget_fetch_map(remote_refs, &remote->fetch.items[i], &tail, 0);\n \t\t\t\tif (remote->fetch.items[i].dst &&\n \t\t\t\t    remote->fetch.items[i].dst[0])\n@@ -1751,14 +1802,14 @@ static void warn_set_head(const char *remote, const char *head_name,\n }\n \n static int set_head(const struct ref *remote_refs, struct remote *remote,\n-\t\t\tint follow_remote_head)\n+\t\t\tint follow_remote_head, const char *known_head_branch)\n {\n \tint result = 0, create_only, baremirror, was_detached;\n \tstruct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,\n \t\t      b_local_head = STRBUF_INIT;\n \tconst char *no_warn_branch = remote->no_warn_branch;\n \tchar *head_name = NULL;\n-\tstruct ref *ref, *matches;\n+\tstruct ref *ref, *matches = NULL;\n \tstruct ref *fetch_map = NULL, **fetch_map_tail = &fetch_map;\n \tstruct refspec_item refspec = {\n \t\t.force = 0,\n@@ -1769,19 +1820,23 @@ static int set_head(const struct ref *remote_refs, struct remote *remote,\n \tstruct string_list heads = STRING_LIST_INIT_DUP;\n \tstruct ref_store *refs = get_main_ref_store(the_repository);\n \n-\tget_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);\n-\tmatches = guess_remote_head(find_ref_by_name(remote_refs, \"HEAD\"),\n-\t\t\t\t    fetch_map, REMOTE_GUESS_HEAD_ALL);\n-\tfor (ref = matches; ref; ref = ref->next) {\n-\t\tstring_list_append(&heads, strip_refshead(ref->name));\n-\t}\n+\tif (known_head_branch) {\n+\t\thead_name = xstrdup(known_head_branch);\n+\t} else {\n+\t\tget_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);\n+\t\tmatches = guess_remote_head(find_ref_by_name(remote_refs, \"HEAD\"),\n+\t\t\t\t\t    fetch_map, REMOTE_GUESS_HEAD_ALL);\n+\t\tfor (ref = matches; ref; ref = ref->next) {\n+\t\t\tstring_list_append(&heads, strip_refshead(ref->name));\n+\t\t}\n \n-\tif (!heads.nr)\n-\t\tresult = 1;\n-\telse if (heads.nr > 1)\n-\t\tresult = 1;\n-\telse\n-\t\thead_name = xstrdup(heads.items[0].string);\n+\t\tif (!heads.nr)\n+\t\t\tresult = 1;\n+\t\telse if (heads.nr > 1)\n+\t\t\tresult = 1;\n+\t\telse\n+\t\t\thead_name = xstrdup(heads.items[0].string);\n+\t}\n \n \tif (!head_name)\n \t\tgoto cleanup;\n@@ -1923,6 +1978,7 @@ static int do_fetch(struct transport *transport,\n \tstruct strmap rejected_refs = STRMAP_INIT;\n \tint summary_width = 0;\n \tint follow_remote_head;\n+\tchar *bootstrap_head_branch = NULL;\n \n \tif (tags == TAGS_DEFAULT) {\n \t\tif (transport->remote->fetch_tags == 2)\n@@ -1958,15 +2014,29 @@ static int do_fetch(struct transport *transport,\n \t\trefspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);\n \t} else {\n \t\tstruct branch *branch = branch_get(NULL);\n+\t\tint tracks_this_remote = branch && branch_has_merge_config(branch) &&\n+\t\t\t!strcmp(branch->remote_name, transport->remote->name);\n+\t\tint tracking_refspec = transport->remote->fetch.nr == 1 &&\n+\t\t\ttransport->remote->fetch.items[0].tracking;\n \n-\t\tif (transport->remote->fetch.nr) {\n+\t\tif (tracking_refspec) {\n+\t\t\tstruct string_list tracked = STRING_LIST_INIT_DUP;\n+\t\t\tstruct string_list_item *item;\n+\n+\t\t\tbranches_tracking_remote(transport->remote, &tracked);\n+\t\t\tfor_each_string_list_item(item, &tracked)\n+\t\t\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n+\t\t\t\t\t    item->string);\n+\t\t\tstring_list_clear(&tracked, 0);\n+\t\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n+\t\t} else if (transport->remote->fetch.nr) {\n \t\t\trefspec_ref_prefixes(&transport->remote->fetch,\n \t\t\t\t\t     &transport_ls_refs_options.ref_prefixes);\n-\t\t\tif (follow_remote_head != FOLLOW_REMOTE_NEVER)\n-\t\t\t\tdo_set_head = 1;\n \t\t}\n-\t\tif (branch && branch_has_merge_config(branch) &&\n-\t\t    !strcmp(branch->remote_name, transport->remote->name)) {\n+\t\tif (transport->remote->fetch.nr &&\n+\t\t    follow_remote_head != FOLLOW_REMOTE_NEVER)\n+\t\t\tdo_set_head = 1;\n+\t\tif (tracks_this_remote) {\n \t\t\tint i;\n \t\t\tfor (i = 0; i < branch->merge_nr; i++) {\n \t\t\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n@@ -2006,7 +2076,8 @@ static int do_fetch(struct transport *transport,\n \ttransport_ls_refs_options_release(&transport_ls_refs_options);\n \n \tref_map = get_ref_map(transport->remote, remote_refs, rs,\n-\t\t\t      tags, &autotags);\n+\t\t\t      tags, &autotags, &bootstrap_head_branch);\n+\n \tif (!update_head_ok)\n \t\tcheck_not_current_branch(ref_map);\n \n@@ -2169,10 +2240,12 @@ static int do_fetch(struct transport *transport,\n \t\t * Way too many cases where this can go wrong so let's just\n \t\t * ignore errors and fail silently for now.\n \t\t */\n-\t\tset_head(remote_refs, transport->remote, follow_remote_head);\n+\t\tset_head(remote_refs, transport->remote, follow_remote_head,\n+\t\t\t bootstrap_head_branch);\n \t}\n \n cleanup:\n+\tfree(bootstrap_head_branch);\n \t/*\n \t * When using batched updates, we want to commit the non-rejected\n \t * updates and also handle the rejections.\ndiff --git a/builtin/remote.c b/builtin/remote.c\nindex de989ea3ba..e407d67037 100644\n--- a/builtin/remote.c\n+++ b/builtin/remote.c\n@@ -16,6 +16,7 @@\n #include \"rebase.h\"\n #include \"refs.h\"\n #include \"refspec.h\"\n+#include \"shallow.h\"\n #include \"odb.h\"\n #include \"strvec.h\"\n #include \"commit-reach.h\"\n@@ -127,13 +128,17 @@ static void add_branch(const char *key, const char *branchname,\n \t\t       const char *remotename, int mirror, struct strbuf *tmp)\n {\n \tstrbuf_reset(tmp);\n-\tstrbuf_addch(tmp, '+');\n-\tif (mirror)\n-\t\tstrbuf_addf(tmp, \"refs/%s:refs/%s\",\n-\t\t\t\tbranchname, branchname);\n-\telse\n-\t\tstrbuf_addf(tmp, \"refs/heads/%s:refs/remotes/%s/%s\",\n-\t\t\t\tbranchname, remotename, branchname);\n+\tif (!strcmp(branchname, \":\")) {\n+\t\tstrbuf_addstr(tmp, \"+:\");\n+\t} else {\n+\t\tstrbuf_addch(tmp, '+');\n+\t\tif (mirror)\n+\t\t\tstrbuf_addf(tmp, \"refs/%s:refs/%s\",\n+\t\t\t\t\tbranchname, branchname);\n+\t\telse\n+\t\t\tstrbuf_addf(tmp, \"refs/heads/%s:refs/remotes/%s/%s\",\n+\t\t\t\t\tbranchname, remotename, branchname);\n+\t}\n \trepo_config_set_multivar(the_repository, key, tmp->buf, \"^$\", 0);\n }\n \n@@ -233,11 +238,13 @@ static int add(int argc, const char **argv, const char *prefix,\n \t\tstrbuf_reset(&buf);\n \t\tstrbuf_addf(&buf, \"remote.%s.fetch\", name);\n \t\tif (track.nr == 0)\n-\t\t\tstring_list_append(&track, \"*\");\n-\t\tfor (size_t i = 0; i < track.nr; i++) {\n+\t\t\tstring_list_append(&track,\n+\t\t\t\tmirror == MIRROR_NONE &&\n+\t\t\t\tis_repository_shallow(the_repository) ?\n+\t\t\t\t\":\" : \"*\");\n+\t\tfor (size_t i = 0; i < track.nr; i++)\n \t\t\tadd_branch(buf.buf, track.items[i].string,\n \t\t\t\t   name, mirror, &buf2);\n-\t\t}\n \t}\n \n \tif (mirror & MIRROR_PUSH) {\ndiff --git a/refspec.c b/refspec.c\nindex 7cb479983b..84a19b5b5c 100644\n--- a/refspec.c\n+++ b/refspec.c\n@@ -49,6 +49,11 @@ static int parse_refspec(struct refspec_item *item, const char *refspec,\n \t\treturn 1;\n \t}\n \n+\tif (fetch && rhs == lhs && rhs[1] == '\\0') {\n+\t\titem->tracking = 1;\n+\t\treturn 1;\n+\t}\n+\n \tif (rhs) {\n \t\tsize_t rlen = strlen(++rhs);\n \t\tis_glob = (1 <= rlen && strchr(rhs, '*'));\n@@ -181,6 +186,7 @@ void refspec_item_clear(struct refspec_item *item)\n \titem->force = 0;\n \titem->pattern = 0;\n \titem->matching = 0;\n+\titem->tracking = 0;\n \titem->exact_sha1 = 0;\n }\n \n@@ -373,7 +379,7 @@ static int refspec_find_negative_match(struct refspec *rs, struct refspec_item *\n \t\tstruct refspec_item *refspec = &rs->items[i];\n \t\tchar *expn_name;\n \n-\t\tif (refspec->negative)\n+\t\tif (refspec->negative || refspec->tracking)\n \t\t\tcontinue;\n \n \t\t/* Note the reversal of src and dst */\ndiff --git a/refspec.h b/refspec.h\nindex fadef67933..97bd6ffef5 100644\n--- a/refspec.h\n+++ b/refspec.h\n@@ -27,6 +27,7 @@ struct refspec_item {\n \tunsigned matching : 1;\n \tunsigned exact_sha1 : 1;\n \tunsigned negative : 1;\n+\tunsigned tracking : 1;\n \n \tchar *src;\n \tchar *dst;\ndiff --git a/remote.c b/remote.c\nindex fe62068463..53b410f7e7 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -988,6 +988,29 @@ void ref_push_report_free(struct ref_push_report *report)\n \n int remote_find_tracking(struct remote *remote, struct refspec_item *refspec)\n {\n+\tif (remote->fetch.nr == 1 && remote->fetch.items[0].tracking) {\n+\t\tstruct strbuf prefix = STRBUF_INIT;\n+\t\tconst char *branch_name;\n+\t\tint ret = -1;\n+\n+\t\tstrbuf_addf(&prefix, \"refs/remotes/%s/\", remote->name);\n+\t\tif (!refspec->src) {\n+\t\t\tif (refspec->dst &&\n+\t\t\t    skip_prefix(refspec->dst, prefix.buf, &branch_name)) {\n+\t\t\t\trefspec->src = xstrfmt(\"refs/heads/%s\", branch_name);\n+\t\t\t\trefspec->force = 1;\n+\t\t\t\tret = 0;\n+\t\t\t}\n+\t\t} else if (skip_prefix(refspec->src, \"refs/heads/\", &branch_name)) {\n+\t\t\tfree(refspec->dst);\n+\t\t\trefspec->dst = xstrfmt(\"%s%s\", prefix.buf, branch_name);\n+\t\t\trefspec->force = 1;\n+\t\t\tret = 0;\n+\t\t}\n+\t\tstrbuf_release(&prefix);\n+\t\treturn ret;\n+\t}\n+\n \treturn refspec_find_match(&remote->fetch, refspec);\n }\n \n@@ -1874,6 +1897,35 @@ int branch_merge_matches(struct branch *branch,\n \treturn refname_match(branch->merge[i]->src, refname);\n }\n \n+struct branches_tracking_remote_cb_data {\n+\tstruct remote *remote;\n+\tstruct string_list *tracked;\n+};\n+\n+static int add_if_tracking_remote(const struct reference *ref, void *cb_data)\n+{\n+\tstruct branches_tracking_remote_cb_data *data = cb_data;\n+\tstruct branch *branch;\n+\n+\tbranch = branch_get(ref->name);\n+\tif (!branch_has_merge_config(branch) ||\n+\t    strcmp(branch->remote_name, data->remote->name))\n+\t\treturn 0;\n+\n+\tfor (int i = 0; i < branch->merge_nr; i++)\n+\t\tstring_list_insert(data->tracked, branch->merge[i]->src);\n+\n+\treturn 0;\n+}\n+\n+void branches_tracking_remote(struct remote *remote, struct string_list *tracked)\n+{\n+\tstruct branches_tracking_remote_cb_data data = { remote, tracked };\n+\n+\trefs_for_each_branch_ref(get_main_ref_store(the_repository),\n+\t\t\t\t  add_if_tracking_remote, &data);\n+}\n+\n __attribute__((format (printf,2,3)))\n static char *error_buf(struct strbuf *err, const char *fmt, ...)\n {\n@@ -2138,7 +2190,7 @@ int get_fetch_map(const struct ref *remote_refs,\n {\n \tstruct ref *ref_map, **rmp;\n \n-\tif (refspec->negative)\n+\tif (refspec->negative || refspec->tracking)\n \t\treturn 0;\n \n \tif (refspec->pattern) {\ndiff --git a/remote.h b/remote.h\nindex cca02033b9..5ab187e00a 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -353,6 +353,9 @@ int branch_has_merge_config(struct branch *branch);\n \n int branch_merge_matches(struct branch *, int n, const char *);\n \n+/* fills tracked with the refname of every local branch's upstream on remote */\n+void branches_tracking_remote(struct remote *remote, struct string_list *tracked);\n+\n /* list of the remote in a group as configured */\n struct remote_group_data {\n \tconst char *name;\ndiff --git a/t/meson.build b/t/meson.build\nindex 3ca7b27104..4c13048d46 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -728,6 +728,7 @@ integration_tests = [\n   't5582-fetch-negative-refspec.sh',\n   't5583-push-branches.sh',\n   't5584-http-429-retry.sh',\n+  't5585-fetch-tracking-refspec.sh',\n   't5600-clone-fail-cleanup.sh',\n   't5601-clone.sh',\n   't5602-clone-remote-exec.sh',\ndiff --git a/t/t5505-remote.sh b/t/t5505-remote.sh\nindex 5fbfcb0848..f7c3857098 100755\n--- a/t/t5505-remote.sh\n+++ b/t/t5505-remote.sh\n@@ -137,6 +137,50 @@ test_expect_success 'filters are listed by git remote -v only' '\n \ttest_grep ! \"\\[blob:none\\]\" out\n '\n \n+test_expect_success 'add remote -t keeps an explicit refspec in a shallow repository' '\n+\ttest_when_finished \"rm -rf shallow-add\" &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\tone shallow-add &&\n+\t(\n+\t\tcd shallow-add &&\n+\t\tgit remote add -t main upstream ../two &&\n+\t\ttest_cmp_config \"+refs/heads/main:refs/remotes/upstream/main\" \\\n+\t\t\tremote.upstream.fetch\n+\t)\n+'\n+\n+test_expect_success 'add remote keeps the wildcard refspec in a full repository' '\n+\ttest_when_finished \"rm -rf full-add\" &&\n+\tgit clone --no-local one full-add &&\n+\t(\n+\t\tcd full-add &&\n+\t\tgit remote add upstream ../two &&\n+\t\ttest_cmp_config \"+refs/heads/*:refs/remotes/upstream/*\" \\\n+\t\t\tremote.upstream.fetch\n+\t)\n+'\n+\n+test_expect_success 'a remote added in a shallow repository defaults to the tracking refspec' '\n+\ttest_when_finished \"rm -rf shallow-add\" &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\tone shallow-add &&\n+\t(\n+\t\tcd shallow-add &&\n+\t\tgit remote add upstream ../two &&\n+\t\ttest_cmp_config \"+:\" remote.upstream.fetch &&\n+\t\tgit fetch upstream &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\tEOF\n+\t\ttest_cmp expect actual &&\n+\t\tgit branch --set-upstream-to=upstream &&\n+\t\ttest_cmp_config upstream branch.main.remote &&\n+\t\ttest_cmp_config refs/heads/main branch.main.merge\n+\t)\n+'\n+\n test_expect_success 'check remote-tracking' '\n \t(\n \t\tcd test &&\ndiff --git a/t/t5585-fetch-tracking-refspec.sh b/t/t5585-fetch-tracking-refspec.sh\nnew file mode 100755\nindex 0000000000..ebc3c5787f\n--- /dev/null\n+++ b/t/t5585-fetch-tracking-refspec.sh\n@@ -0,0 +1,124 @@\n+#!/bin/sh\n+\n+test_description='\"git fetch\" with the tracking refspec \":\" (or \"+:\")\n+\n+The special refspec fetches whichever branches at the remote our local\n+branches have as their @{upstream}, plus the default branch of that\n+remote, which is always included so that it is available even before\n+anything is set up to track it. git remote add uses it by default for\n+a new remote in an already shallow repository, so that adding a\n+second remote there does not turn the next plain fetch or pull into\n+one that negotiates history for every branch the remote has.\n+'\n+\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup' '\n+\ttest_commit main-1 &&\n+\ttest_commit main-2 &&\n+\tgit checkout -b side main-1 &&\n+\ttest_commit side-1 &&\n+\tgit checkout -b next main-1 &&\n+\ttest_commit next-1 &&\n+\tgit checkout main\n+'\n+\n+test_expect_success 'clone shallow and single-branch, then add a second remote' '\n+\tgit clone --no-local --depth=1 --branch main --single-branch . client &&\n+\t(\n+\t\tcd client &&\n+\t\tgit remote add upstream .. &&\n+\t\ttest_cmp_config \"+:\" remote.upstream.fetch\n+\t)\n+'\n+\n+test_expect_success 'fetching the new remote does not need every branch it has' '\n+\t(\n+\t\tcd client &&\n+\t\tgit fetch upstream &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\tEOF\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'set-upstream-to now resolves right after that first fetch' '\n+\t(\n+\t\tcd client &&\n+\t\tgit branch --set-upstream-to=upstream &&\n+\t\ttest_cmp_config upstream branch.main.remote &&\n+\t\ttest_cmp_config refs/heads/main branch.main.merge\n+\t)\n+'\n+\n+test_expect_success 'a branch checked out from a one-time fetch is kept updated by later plain fetches' '\n+\t(\n+\t\tcd client &&\n+\t\tgit fetch upstream side:refs/remotes/upstream/side &&\n+\t\tgit branch side-topic upstream/side\n+\t) &&\n+\tgit checkout side &&\n+\ttest_commit side-2 &&\n+\tgit checkout main &&\n+\t(\n+\t\tcd client &&\n+\t\tgit fetch upstream &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\trefs/remotes/upstream/side\n+\t\tEOF\n+\t\ttest_cmp expect actual &&\n+\t\tgit rev-parse refs/remotes/upstream/side >actual-oid &&\n+\t\tgit -C .. rev-parse side >expect-oid &&\n+\t\ttest_cmp expect-oid actual-oid\n+\t)\n+'\n+\n+test_expect_success 'a second branch tracking the same upstream branch does not fetch it twice' '\n+\t(\n+\t\tcd client &&\n+\t\tgit branch side-topic-2 upstream/side &&\n+\t\tgit fetch upstream &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\trefs/remotes/upstream/side\n+\t\tEOF\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'a branch tracking a different remote is not fetched from upstream' '\n+\t(\n+\t\tcd client &&\n+\t\tgit remote add other .. &&\n+\t\tgit fetch other next:refs/remotes/other/next &&\n+\t\tgit branch next-topic other/next &&\n+\t\tgit fetch upstream &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\trefs/remotes/upstream/side\n+\t\tEOF\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'git remote show does not choke on the tracking refspec' '\n+\t(\n+\t\tcd client &&\n+\t\tgit remote show upstream\n+\t)\n+'\n+\n+test_done\n\nbase-commit: 3bc0341126508f78f5869cbfc0005e987efdf0c7\n-- \ngitgitgadget\n"},{"id":"553119","messageId":"xmqq7bkb7in5.fsf@gitster.g","threadId":"66353","inReplyTo":"pull.2412.v2.git.git.1790195720941.gitgitgadget@gmail.com","subject":"Re: [PATCH v2] fetch: avoid fetching every branch of a new remote in a shallow repo","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-23T21:38:38Z","receivedAt":"2026-09-23T21:38:40Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Harald Nordgren <haraldnordgren@gmail.com>\n>\n> git remote add sets a new remote up to fetch every branch by default.\n> In an already shallow repository, that turns the next plain fetch or\n> pull into a slow or hanging one, even when only one or two branches\n> are ever used.\n>\n> Add a special fetch refspec, \"+:\", that fetches only the branches our\n> local branches are built on, plus the remote's default branch so a\n> brand new remote is usable right away, without needing to first set\n> anything up to track it. git remote add now uses it instead of the\n> usual wildcard refspec whenever the repository is already shallow.\n\nThat's way too much for a single patch.  It needs to be split into\ndigestible chunks, but I offhand do not know how many pieces are\nappropriate, so let's think aloud together and try to refine the\ndesign while we do so.\n\nThe outline of our design so far should give something like this in\nour configuration file.\n\n\t[remote \"second\"]\n\t\tfetch = :+\n\n\t[branch \"topic1\"]\n\t\tremote = second\n\t\tmerge = refs/heads/main\n\n\t[branch \"topic2\"]\n\t\tremote = second\n\t\tmerge = refs/heads/next\n\nIn the \"fetch only what we build on\" mode, we would collect local\nbranches $X where branch.$X.remote == second and then collect\nbranch.$X.merge for these local branches.  In this case, we would\ndecide to fetch 'main' and 'next' branches in the end.\n\nBut notice that this does not give us sufficient information.  There\nis no explicit clue that tells that the remote-tracking branches for\nthis remote 'second' should be stored under refs/remotes/second/\nhierarchy.  A normal remote that is defined like so:\n\n\t[remote \"origin\"]\n\t\tfetch = +refs/heads/*:refs/remotes/origin/*\n\ndoes not have such a problem, as it makes it crystal clear that\ntheir branches go under refs/remotes/origin/ hierarchy.\n\nSo using \"fetch = :+\" is *not* a good idea, as I said.  Let's scrap\nthat syntax.\n\nOne thing we could do is probably to introduce\n\n\t[remote \"second\"]\n\t\trefmap = +refs/heads/*:refs/remotes/second/*\n\ninstead to give this clue (see \"git fetch --help\" for what a refmap\nis; it looks similar to refspec but only defines how their refs are\nmapped to our namespace without specifying what to be fetched, which\nis exactly what we need here).  We do not use remote.second.fetch at\nall.\n\nIt would be an easy first step to teach that an explicit\n\n\t$ git fetch second main next\n\nwith such a remote.second.refmap should behave the same way as\n\n\t$ git fetch --refmap='+refs/heads/*:refs/remotes/second/*' second \\\n\t\tmain next\n\nin a repository without the refmote.second.refmap configuration.  As\n\"git fetch --refmap=... second main next\" should already work, it\nwould be only the matter of supplementing the command line argument\nwith configured default.\n\nThen teach \"git fetch\" to further treat\n\n\t$ git fetch --refmap='+refs/heads/*:refs/remotes/second/*' second\n\ni.e., fetch with refmap but without specifying what exactly to\nfetch, as a request to fetch their branches we build on (and nothing\nelse), using the refmap, in other words, the lack of \"what to fetch\"\nin the above command line signals \"git fetch\" to rewrite the above to\n\n\t$ git fetch --refmap='+refs/heads/*:refs/remotes/second/*' second \\\n\t\tmain next\n\ninternally.  Since we have the previous remote.X.refmap step already,\nit means that with remote.second.refmap configured properly, the\nuser can only say\n\n\t$ git fetch second\n\nand it would do the right thing in our scenario.\n\nAnother and final step would be to teach \"git remote add\" to add\n\n        [remote \"second\"]\n                refmap = +refs/heads/*:refs/remotes/second/*\n\nwhen you want to fetch only what you build on.  I am not sure what\nshould trigger the decision.  Your initial message said something\nabout shallow and sparse and an earlier review refuted one of them\n(I do not recall which offhand, but probably sparse).  It probably\nis a good idea to start with an explicit command line option to \"git\nremote add --limited-fetch\" in a single commit.\n\nAnd then add heuristics (like \"in a shallow clone, this mode is\nturned on by default, but an explicit '--no-limited-fetch' can\ncountermand it\") in another commit.\n\nSo far, we identified four distinct commits, each bite sized.\n\n - remote.X.refmap configuration acts as if --refmap=... command\n   line argument was passed.\n\n - passing refmap without saying what to fetch enumerates what their\n   branches we build on, and pretend as if the user listed these\n   branches on the command line to fetch.\n\n - \"git remote add --limited-fetch\" creates remote.X.refmap instead\n   of remote.X.fetch as necessary.\n\n - \"git remote add\" without explicit \"--[no-]limited-fetch\" uses\n   heuristics to enable it.\n\nOr something like that, perhaps?\n"},{"id":"553211","messageId":"CALnO6CA5b7mpia9tkiOENdOKcOHF4errc31wuF5n5-=rrbHxVw@mail.gmail.com","threadId":"66353","inReplyTo":"xmqqbj9nagt2.fsf@gitster.g","subject":"Re: [PATCH] fetch: add config to avoid fetching every branch in shallow repo","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-09-24T17:10:47Z","receivedAt":"2026-09-24T17:10:59Z","isPatch":true,"body":"On Wed, Sep 23, 2026 at 3:50 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> \"D. Ben Knoble\" <ben.knoble@gmail.com> writes:\n>\n> >>     $ git fetch second main:refs/remotes/second/main\n> >\n> > I've wanted something similar for notes, so allow me to interject from\n> > the sidelines: it would be even nicer to still have the ability to map\n> > fetches (so that \"git fetch second main\" did the right thing, creating\n>\n> The thing is, the command line\n>\n>     $ git fetch second main\n>\n> has been used for the past 20 years as a \"single-shot fetch\" syntax\n> that expresses that the user does not intend to keep interacting\n> with the same 'main' branch or even the same 'second' repository,\n> and for the \"single-shot fetch\", it is absolutely the wrong thing to\n> create a remote-tracking branch.\n\nYes, perhaps I was a bit cavalier about that particular case; but in\nyour other message you suggested allowing configurable refmaps, and\nthat's what I was getting at previously without having the vocabulary\nfor it. Thanks!\n\n> It would be even worse if we created a remote 'second' and\n> remote-tracking branch 'refs/remotes/second/main' when you ran\n>\n>     $ git fetch https://ho.st/second main\n>\n> Having said that, I suspect that the fact that you have the\n> shorthand 'second' (i.e., you have \"[remote \"second\"] url = ...\"\n> defined) may be a good enough sign that you expect to keep\n> interacting with that repository, and some people might appreciate\n> it if\n>\n>     $ git fetch second main\n>\n> created a remote-tracking branch \"refs/remotes/second/main\"\n> automatically.\n>\n> But we cannot suddenly start doing so without breaking people's\n> expectations, and without a good transition plan.  We need at least\n> an escape hatch for users to say \"No, this is a single-shot fetch;\n> do not write the object anywhere other than FETCH_HEAD as we have\n> always done\".\n\nSo anyway, I think fetch.<remote>.refmap is the right thing here, thanks!\n\n-- \nD. Ben Knoble\n"},{"id":"553218","messageId":"xmqqa4p634tx.fsf@gitster.g","threadId":"66353","inReplyTo":"CALnO6CA5b7mpia9tkiOENdOKcOHF4errc31wuF5n5-=rrbHxVw@mail.gmail.com","subject":"Re: [PATCH] fetch: add config to avoid fetching every branch in shallow repo","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-24T18:02:50Z","receivedAt":"2026-09-24T18:02:54Z","isPatch":true,"body":"\"D. Ben Knoble\" <ben.knoble@gmail.com> writes:\n\n>> Having said that, I suspect that the fact that you have the\n>> shorthand 'second' (i.e., you have \"[remote \"second\"] url = ...\"\n>> defined) may be a good enough sign that you expect to keep\n>> interacting with that repository, and some people might appreciate\n>> it if\n>>\n>>     $ git fetch second main\n>>\n>> created a remote-tracking branch \"refs/remotes/second/main\"\n>> automatically.\n>>\n>> But we cannot suddenly start doing so without breaking people's\n>> expectations, and without a good transition plan.  We need at least\n>> an escape hatch for users to say \"No, this is a single-shot fetch;\n>> do not write the object anywhere other than FETCH_HEAD as we have\n>> always done\".\n>\n> So anyway, I think fetch.<remote>.refmap is the right thing here, thanks!\n\nAhh, yes, if we introduce fetch.second.refmap that makes it pretend\nas if you gave a --refmap argument on the command line, your\n\n\t$ git fetch second main\n\nwill automatically do what you want it to do.  I didn't realize that\nit was the implication of what I was outlining while preparing the\nresponse to Harald ;-)\n\nThanks.\n"},{"id":"553273","messageId":"pull.2412.v3.git.git.1790333402.gitgitgadget@gmail.com","threadId":"66353","inReplyTo":"pull.2412.git.git.1789829246437.gitgitgadget@gmail.com","subject":"[PATCH v3 0/4] fetch: avoid fetching every branch of a new remote in a shallow repo","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-25T10:49:58Z","receivedAt":"2026-09-25T10:50:05Z","isPatch":true,"body":"Avoid fetching every branch of a new remote in a shallow repo.\n\nChanges in v3:\n\n * Replace the special \":\"/\"+:\" fetch refspec with remote.<name>.refmap,\n   reusing git's existing --refmap mechanism instead of inventing new\n   refspec syntax.\n * Split the change into 4 commits.\n\nChanges in v2:\n\n * Replaced the opt-in fetch.shallow config entirely with a new special\n   fetch refspec (+:) that git remote add now defaults new remotes to in a\n   shallow repository. The new refspec fetches whichever branches any local\n   branch tracks at that remote, plus the remote's default branch.\n\nHarald Nordgren (4):\n  fetch: add remote.<name>.refmap\n  fetch: infer branches to fetch from a refmap-only remote\n  remote: add \"git remote add --limited-fetch\"\n  remote: default to --limited-fetch in a shallow repository\n\n Documentation/config/remote.adoc |   7 ++\n Documentation/fetch-options.adoc |   7 ++\n Documentation/git-remote.adoc    |  14 ++-\n builtin/fetch.c                  | 142 +++++++++++++++++++++++++------\n builtin/remote.c                 |  31 +++++--\n remote.c                         |  41 ++++++++-\n remote.h                         |   9 ++\n t/meson.build                    |   1 +\n t/t5505-remote.sh                |  75 ++++++++++++++++\n t/t5510-fetch.sh                 |  17 ++++\n t/t5585-fetch-refmap.sh          | 124 +++++++++++++++++++++++++++\n 11 files changed, 433 insertions(+), 35 deletions(-)\n create mode 100755 t/t5585-fetch-refmap.sh\n\n\nbase-commit: 0f8e75abebff0877cae681a3d5ff31ac47f54220\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2412%2FHaraldNordgren%2Ffetch-shallow-narrow-refspec-v3\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2412/HaraldNordgren/fetch-shallow-narrow-refspec-v3\nPull-Request: https://github.com/git/git/pull/2412\n\nRange-diff vs v2:\n\n -:  ---------- > 1:  b04c00b974 fetch: add remote.<name>.refmap\n 1:  f04f5ce219 ! 2:  4ec508a223 fetch: avoid fetching every branch of a new remote in a shallow repo\n     @@ Metadata\n      Author: Harald Nordgren <haraldnordgren@gmail.com>\n      \n       ## Commit message ##\n     -    fetch: avoid fetching every branch of a new remote in a shallow repo\n     +    fetch: infer branches to fetch from a refmap-only remote\n      \n     -    git remote add sets a new remote up to fetch every branch by default.\n     -    In an already shallow repository, that turns the next plain fetch or\n     -    pull into a slow or hanging one, even when only one or two branches\n     -    are ever used.\n     +    Configuring remote.<name>.refmap without remote.<name>.fetch used to\n     +    make a refspec-less \"git fetch <name>\" fail with \"--refmap option is\n     +    only meaningful with command-line refspec(s)\", since a refmap only\n     +    says where to put fetched refs, not what to fetch.\n      \n     -    Add a special fetch refspec, \"+:\", that fetches only the branches our\n     -    local branches are built on, plus the remote's default branch so a\n     -    brand new remote is usable right away, without needing to first set\n     -    anything up to track it. git remote add now uses it instead of the\n     -    usual wildcard refspec whenever the repository is already shallow.\n     +    Make that case infer what to fetch: the local branches whose\n     +    @{upstream} is already on that remote, plus the remote's default\n     +    branch, which is always included so it is available even before\n     +    anything is set up to track it. This lets a remote be configured to\n     +    fetch only the branches actually in use, without listing them by\n     +    hand in remote.<name>.fetch, and without needing to touch the\n     +    command line every time.\n      \n          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n      \n     - ## Documentation/git-remote.adoc ##\n     -@@ Documentation/git-remote.adoc: Add a remote named _<name>_ for the repository at\n     - _<URL>_.  The command `git fetch <name>` can then be used to create and\n     - update remote-tracking branches `<name>/<branch>`.\n     - +\n     -+If the repository is already a shallow repository (see linkgit:git-clone[1]\n     -+`--depth`) and neither `-t` nor `--mirror` is given, the remote is set up\n     -+with the special tracking refspec (see the _<refspec>_ entry in\n     -+linkgit:git-fetch[1]) instead of the default glob refspec, so that\n     -+`git fetch <name>` does not need to negotiate history for every branch\n     -+the remote has.\n     -++\n     - With `-f` option, `git fetch <name>` is run immediately after\n     - the remote information is set up.\n     - +\n     + ## Documentation/config/remote.adoc ##\n     +@@ Documentation/config/remote.adoc: remote.<name>.fetch::\n     + \n     + remote.<name>.refmap::\n     + \tThe default value of the `--refmap` option for linkgit:git-fetch[1].\n     +-\tOnly takes effect when the fetch names what to fetch explicitly,\n     +-\teither on the command line or via `remote.<name>.fetch`. See the\n     +-\t`--refmap` entry in linkgit:git-fetch[1].\n     ++\tIf `remote.<name>.fetch` is not set either, a refspec-less fetch\n     ++\tinfers what to fetch from local branches built on this remote,\n     ++\tinstead of fetching every branch it has. See the `--refmap` entry\n     ++\tin linkgit:git-fetch[1].\n     + \n     + remote.<name>.push::\n     + \tThe default set of \"refspec\" for linkgit:git-push[1]. See\n      \n     - ## Documentation/pull-fetch-param.adoc ##\n     -@@ Documentation/pull-fetch-param.adoc: Negative refspecs can themselves be pattern refspecs. However, they may only\n     - contain a _<src>_ and do not specify a _<dst>_. Fully spelled out hex object\n     - names are also not supported.\n     - +\n     -+The special refspec `:` (or `+:` to allow non-fast-forward updates)\n     -+directs Git to fetch whichever branches at the remote our local branches\n     -+have as their `@{upstream}`, plus the remote's default branch, which is\n     -+always included so that it is available even before anything is set up\n     -+to track it. Unlike the same syntax when pushing, the local branch name\n     -+plays no role here beyond identifying which remote branch to fetch: the\n     -+result is written to `refs/remotes/<remote>/<branch>` using the remote's\n     -+own branch name, and several local branches built on the same remote\n     -+branch only cause it to be fetched once.\n     -++\n     - `tag <tag>` means the same as `refs/tags/<tag>:refs/tags/<tag>`;\n     - it requests fetching everything up to the given tag.\n     + ## Documentation/fetch-options.adoc ##\n     +@@ Documentation/fetch-options.adoc: endif::git-pull[]\n     + \tcommand-line arguments. See section on \"Configured Remote-tracking\n     + \tBranches\" for details.\n       +\n     +-`remote.<name>.refmap` provides the default value for this option, the\n     +-same way `remote.<name>.fetch` provides the default refspecs to fetch.\n     ++When a refmap is active (from `--refmap` or `remote.<name>.refmap`) but\n     ++there is nothing to fetch, neither on the command line nor from\n     ++`remote.<name>.fetch`, Git infers what to fetch from the local branches\n     ++whose `@{upstream}` is on that remote, plus the remote's default branch,\n     ++which is always included so that it is available even before anything\n     ++is set up to track it.\n     + \n     + `-t`::\n     + `--tags`::\n      \n       ## builtin/fetch.c ##\n      @@ builtin/fetch.c: static void filter_prefetch_refspec(struct refspec *rs)\n     - \t}\n     - }\n     - \n     -+static void fetch_tracked_branches(struct remote *remote,\n     -+\t\t\t\t   const struct ref *remote_refs,\n     -+\t\t\t\t   struct ref ***tail,\n     -+\t\t\t\t   char **default_branch_out,\n     -+\t\t\t\t   int force)\n     -+{\n     -+\tstruct string_list tracked = STRING_LIST_INIT_DUP;\n     -+\tstruct string_list_item *item;\n     -+\tstruct ref *head;\n     -+\tchar *default_branch = NULL;\n     -+\n     -+\tbranches_tracking_remote(remote, &tracked);\n     -+\n     -+\thead = get_remote_ref(remote_refs, \"HEAD\");\n     -+\tif (head && head->symref && *head->symref) {\n     -+\t\tdefault_branch = xstrdup(head->symref);\n     -+\t\tstring_list_insert(&tracked, default_branch);\n     -+\t}\n     -+\tfree_one_ref(head);\n     -+\n     -+\tfor_each_string_list_item(item, &tracked) {\n     -+\t\tstruct refspec_item ref = { .force = force };\n     -+\t\tconst char *branch_name = item->string;\n     -+\t\tint is_default_branch = default_branch &&\n     -+\t\t\t!strcmp(item->string, default_branch);\n     -+\n     -+\t\tskip_prefix(branch_name, \"refs/heads/\", &branch_name);\n     -+\t\tref.src = xstrdup(is_default_branch ? \"HEAD\" : item->string);\n     -+\t\tref.dst = xstrfmt(\"refs/remotes/%s/%s\", remote->name, branch_name);\n     -+\t\tget_fetch_map(remote_refs, &ref, tail, 1);\n     -+\t\tfree(ref.src);\n     -+\t\tfree(ref.dst);\n     -+\t}\n     -+\n     -+\tif (default_branch && default_branch_out) {\n     -+\t\tconst char *branch_name = default_branch;\n     -+\t\tskip_prefix(branch_name, \"refs/heads/\", &branch_name);\n     -+\t\t*default_branch_out = xstrdup(branch_name);\n     -+\t}\n     -+\n     -+\tfree(default_branch);\n     -+\tstring_list_clear(&tracked, 0);\n     -+}\n     -+\n       static struct ref *get_ref_map(struct remote *remote,\n       \t\t\t       const struct ref *remote_refs,\n       \t\t\t       struct refspec *rs,\n     @@ builtin/fetch.c: static void filter_prefetch_refspec(struct refspec *rs)\n       \tint i;\n       \tstruct ref *rm;\n      @@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,\n     - \t\t     /* Note: has_merge implies non-NULL branch->remote_name */\n     - \t\t     (has_merge && !strcmp(branch->remote_name, remote->name)))) {\n     - \t\t\tfor (i = 0; i < remote->fetch.nr; i++) {\n     -+\t\t\t\tif (remote->fetch.items[i].tracking) {\n     -+\t\t\t\t\tfetch_tracked_branches(remote, remote_refs, &tail,\n     -+\t\t\t\t\t\t\t\tbootstrap_head_branch,\n     -+\t\t\t\t\t\t\t\tremote->fetch.items[i].force);\n     -+\t\t\t\t\tcontinue;\n     -+\t\t\t\t}\n     - \t\t\t\tget_fetch_map(remote_refs, &remote->fetch.items[i], &tail, 0);\n     - \t\t\t\tif (remote->fetch.items[i].dst &&\n     - \t\t\t\t    remote->fetch.items[i].dst[0])\n     + \tstruct ref **tail = &ref_map;\n     + \tstruct refspec *effective_refmap =\n     + \t\trefmap.nr ? &refmap : remote ? &remote->refmap : NULL;\n     ++\tstruct refspec inferred_rs;\n     ++\tint inferred_branches = 0;\n     + \n     + \t/* opportunistically-updated references: */\n     + \tstruct ref *orefs = NULL, **oref_tail = &orefs;\n     +@@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,\n     + \tstruct hashmap existing_refs;\n     + \tint existing_refs_populated = 0;\n     + \n     ++\trefspec_init_fetch(&inferred_rs, the_hash_algo);\n     ++\n     + \tfilter_prefetch_refspec(rs);\n     + \tif (remote)\n     + \t\tfilter_prefetch_refspec(&remote->fetch);\n     + \n     ++\tif (!rs->nr && remote && !remote->fetch.nr &&\n     ++\t    effective_refmap && effective_refmap->nr) {\n     ++\t\tstruct string_list tracked = STRING_LIST_INIT_DUP;\n     ++\t\tstruct string_list_item *item;\n     ++\t\tstruct ref *head;\n     ++\t\tchar *default_branch = NULL;\n     ++\t\tchar *default_branch_dst = NULL;\n     ++\n     ++\t\tbranches_tracking_remote(remote, &tracked);\n     ++\t\tfor_each_string_list_item(item, &tracked)\n     ++\t\t\trefspec_append(&inferred_rs, item->string);\n     ++\n     ++\t\t/* Always fetch the default branch too, as \"HEAD\". */\n     ++\t\thead = get_remote_ref(remote_refs, \"HEAD\");\n     ++\t\tif (head && head->symref && *head->symref)\n     ++\t\t\tdefault_branch = xstrdup(head->symref);\n     ++\t\tfree_one_ref(head);\n     ++\n     ++\t\tfor (i = 0; default_branch && !default_branch_dst &&\n     ++\t\t\t    !string_list_has_string(&tracked, default_branch) &&\n     ++\t\t\t    i < effective_refmap->nr; i++) {\n     ++\t\t\tstruct refspec_item *map = &effective_refmap->items[i];\n     ++\n     ++\t\t\tif (map->pattern)\n     ++\t\t\t\tmatch_refname_with_pattern(map->src, default_branch,\n     ++\t\t\t\t\t\t\t    map->dst, &default_branch_dst);\n     ++\t\t\telse if (!strcmp(map->src, default_branch))\n     ++\t\t\t\tdefault_branch_dst = xstrdup(map->dst);\n     ++\t\t}\n     ++\n     ++\t\tif (default_branch_dst) {\n     ++\t\t\tstruct refspec_item head_item = { .force = 1 };\n     ++\n     ++\t\t\thead_item.src = xstrdup(\"HEAD\");\n     ++\t\t\thead_item.dst = default_branch_dst;\n     ++\t\t\tget_fetch_map(remote_refs, &head_item, &tail, 1);\n     ++\t\t\tfree(head_item.src);\n     ++\t\t}\n     ++\n     ++\t\tif (default_branch && bootstrap_head_branch) {\n     ++\t\t\tconst char *branch_name = default_branch;\n     ++\t\t\tskip_prefix(branch_name, \"refs/heads/\", &branch_name);\n     ++\t\t\t*bootstrap_head_branch = xstrdup(branch_name);\n     ++\t\t}\n     ++\n     ++\t\tfree(default_branch);\n     ++\t\tfree(default_branch_dst);\n     ++\t\tstring_list_clear(&tracked, 0);\n     ++\n     ++\t\trs = &inferred_rs;\n     ++\t\tinferred_branches = 1;\n     ++\t}\n     ++\n     + \tif (rs->nr) {\n     + \t\tstruct refspec *fetch_refspec;\n     + \n     + \t\tfor (i = 0; i < rs->nr; i++) {\n     +-\t\t\tget_fetch_map(remote_refs, &rs->items[i], &tail, 0);\n     ++\t\t\tget_fetch_map(remote_refs, &rs->items[i], &tail,\n     ++\t\t\t\t      inferred_branches);\n     + \t\t\tif (rs->items[i].dst && rs->items[i].dst[0])\n     + \t\t\t\t*autotags = 1;\n     + \t\t}\n     +@@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,\n     + \n     + \t\tfor (i = 0; i < fetch_refspec->nr; i++)\n     + \t\t\tget_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1);\n     ++\t} else if (inferred_branches) {\n     ++\t\t/* Already fully handled above. */\n     + \t} else if (effective_refmap && effective_refmap->nr) {\n     + \t\tdie(\"--refmap option is only meaningful with command-line refspec(s)\");\n     + \t} else {\n     +@@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,\n     + \tif (existing_refs_populated)\n     + \t\thashmap_clear_and_free(&existing_refs, struct refname_hash_entry, ent);\n     + \n     ++\trefspec_clear(&inferred_rs);\n     + \treturn ref_map;\n     + }\n     + \n      @@ builtin/fetch.c: static void warn_set_head(const char *remote, const char *head_name,\n       }\n       \n     @@ builtin/fetch.c: static int do_fetch(struct transport *transport,\n       \t\trefspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);\n       \t} else {\n       \t\tstruct branch *branch = branch_get(NULL);\n     +-\n     +-\t\tif (transport->remote->fetch.nr) {\n      +\t\tint tracks_this_remote = branch && branch_has_merge_config(branch) &&\n      +\t\t\t!strcmp(branch->remote_name, transport->remote->name);\n     -+\t\tint tracking_refspec = transport->remote->fetch.nr == 1 &&\n     -+\t\t\ttransport->remote->fetch.items[0].tracking;\n     - \n     --\t\tif (transport->remote->fetch.nr) {\n     -+\t\tif (tracking_refspec) {\n     ++\t\tstruct refspec *effective_refmap = refmap.nr ? &refmap :\n     ++\t\t\t&transport->remote->refmap;\n     ++\t\tint inferred_branches = !transport->remote->fetch.nr &&\n     ++\t\t\teffective_refmap->nr;\n     ++\n     ++\t\tif (inferred_branches) {\n      +\t\t\tstruct string_list tracked = STRING_LIST_INIT_DUP;\n      +\t\t\tstruct string_list_item *item;\n      +\n     @@ builtin/fetch.c: static int do_fetch(struct transport *transport,\n       \t\t}\n      -\t\tif (branch && branch_has_merge_config(branch) &&\n      -\t\t    !strcmp(branch->remote_name, transport->remote->name)) {\n     -+\t\tif (transport->remote->fetch.nr &&\n     ++\t\tif ((transport->remote->fetch.nr || inferred_branches) &&\n      +\t\t    follow_remote_head != FOLLOW_REMOTE_NEVER)\n      +\t\t\tdo_set_head = 1;\n      +\t\tif (tracks_this_remote) {\n     @@ builtin/fetch.c: static int do_fetch(struct transport *transport,\n       \t * When using batched updates, we want to commit the non-rejected\n       \t * updates and also handle the rejections.\n      \n     - ## builtin/remote.c ##\n     -@@\n     - #include \"rebase.h\"\n     - #include \"refs.h\"\n     - #include \"refspec.h\"\n     -+#include \"shallow.h\"\n     - #include \"odb.h\"\n     - #include \"strvec.h\"\n     - #include \"commit-reach.h\"\n     -@@ builtin/remote.c: static void add_branch(const char *key, const char *branchname,\n     - \t\t       const char *remotename, int mirror, struct strbuf *tmp)\n     - {\n     - \tstrbuf_reset(tmp);\n     --\tstrbuf_addch(tmp, '+');\n     --\tif (mirror)\n     --\t\tstrbuf_addf(tmp, \"refs/%s:refs/%s\",\n     --\t\t\t\tbranchname, branchname);\n     --\telse\n     --\t\tstrbuf_addf(tmp, \"refs/heads/%s:refs/remotes/%s/%s\",\n     --\t\t\t\tbranchname, remotename, branchname);\n     -+\tif (!strcmp(branchname, \":\")) {\n     -+\t\tstrbuf_addstr(tmp, \"+:\");\n     -+\t} else {\n     -+\t\tstrbuf_addch(tmp, '+');\n     -+\t\tif (mirror)\n     -+\t\t\tstrbuf_addf(tmp, \"refs/%s:refs/%s\",\n     -+\t\t\t\t\tbranchname, branchname);\n     -+\t\telse\n     -+\t\t\tstrbuf_addf(tmp, \"refs/heads/%s:refs/remotes/%s/%s\",\n     -+\t\t\t\t\tbranchname, remotename, branchname);\n     -+\t}\n     - \trepo_config_set_multivar(the_repository, key, tmp->buf, \"^$\", 0);\n     - }\n     - \n     -@@ builtin/remote.c: static int add(int argc, const char **argv, const char *prefix,\n     - \t\tstrbuf_reset(&buf);\n     - \t\tstrbuf_addf(&buf, \"remote.%s.fetch\", name);\n     - \t\tif (track.nr == 0)\n     --\t\t\tstring_list_append(&track, \"*\");\n     --\t\tfor (size_t i = 0; i < track.nr; i++) {\n     -+\t\t\tstring_list_append(&track,\n     -+\t\t\t\tmirror == MIRROR_NONE &&\n     -+\t\t\t\tis_repository_shallow(the_repository) ?\n     -+\t\t\t\t\":\" : \"*\");\n     -+\t\tfor (size_t i = 0; i < track.nr; i++)\n     - \t\t\tadd_branch(buf.buf, track.items[i].string,\n     - \t\t\t\t   name, mirror, &buf2);\n     --\t\t}\n     - \t}\n     - \n     - \tif (mirror & MIRROR_PUSH) {\n     -\n     - ## refspec.c ##\n     -@@ refspec.c: static int parse_refspec(struct refspec_item *item, const char *refspec,\n     - \t\treturn 1;\n     - \t}\n     - \n     -+\tif (fetch && rhs == lhs && rhs[1] == '\\0') {\n     -+\t\titem->tracking = 1;\n     -+\t\treturn 1;\n     -+\t}\n     -+\n     - \tif (rhs) {\n     - \t\tsize_t rlen = strlen(++rhs);\n     - \t\tis_glob = (1 <= rlen && strchr(rhs, '*'));\n     -@@ refspec.c: void refspec_item_clear(struct refspec_item *item)\n     - \titem->force = 0;\n     - \titem->pattern = 0;\n     - \titem->matching = 0;\n     -+\titem->tracking = 0;\n     - \titem->exact_sha1 = 0;\n     - }\n     - \n     -@@ refspec.c: static int refspec_find_negative_match(struct refspec *rs, struct refspec_item *\n     - \t\tstruct refspec_item *refspec = &rs->items[i];\n     - \t\tchar *expn_name;\n     - \n     --\t\tif (refspec->negative)\n     -+\t\tif (refspec->negative || refspec->tracking)\n     - \t\t\tcontinue;\n     - \n     - \t\t/* Note the reversal of src and dst */\n     -\n     - ## refspec.h ##\n     -@@ refspec.h: struct refspec_item {\n     - \tunsigned matching : 1;\n     - \tunsigned exact_sha1 : 1;\n     - \tunsigned negative : 1;\n     -+\tunsigned tracking : 1;\n     - \n     - \tchar *src;\n     - \tchar *dst;\n     -\n       ## remote.c ##\n     -@@ remote.c: void ref_push_report_free(struct ref_push_report *report)\n     - \n     - int remote_find_tracking(struct remote *remote, struct refspec_item *refspec)\n     - {\n     -+\tif (remote->fetch.nr == 1 && remote->fetch.items[0].tracking) {\n     -+\t\tstruct strbuf prefix = STRBUF_INIT;\n     -+\t\tconst char *branch_name;\n     -+\t\tint ret = -1;\n     -+\n     -+\t\tstrbuf_addf(&prefix, \"refs/remotes/%s/\", remote->name);\n     -+\t\tif (!refspec->src) {\n     -+\t\t\tif (refspec->dst &&\n     -+\t\t\t    skip_prefix(refspec->dst, prefix.buf, &branch_name)) {\n     -+\t\t\t\trefspec->src = xstrfmt(\"refs/heads/%s\", branch_name);\n     -+\t\t\t\trefspec->force = 1;\n     -+\t\t\t\tret = 0;\n     -+\t\t\t}\n     -+\t\t} else if (skip_prefix(refspec->src, \"refs/heads/\", &branch_name)) {\n     -+\t\t\tfree(refspec->dst);\n     -+\t\t\trefspec->dst = xstrfmt(\"%s%s\", prefix.buf, branch_name);\n     -+\t\t\trefspec->force = 1;\n     -+\t\t\tret = 0;\n     -+\t\t}\n     -+\t\tstrbuf_release(&prefix);\n     -+\t\treturn ret;\n     -+\t}\n     -+\n     - \treturn refspec_find_match(&remote->fetch, refspec);\n     - }\n     - \n      @@ remote.c: int branch_merge_matches(struct branch *branch,\n       \treturn refname_match(branch->merge[i]->src, refname);\n       }\n     @@ remote.c: int branch_merge_matches(struct branch *branch,\n       __attribute__((format (printf,2,3)))\n       static char *error_buf(struct strbuf *err, const char *fmt, ...)\n       {\n     -@@ remote.c: int get_fetch_map(const struct ref *remote_refs,\n     - {\n     - \tstruct ref *ref_map, **rmp;\n     - \n     --\tif (refspec->negative)\n     -+\tif (refspec->negative || refspec->tracking)\n     - \t\treturn 0;\n     - \n     - \tif (refspec->pattern) {\n      \n       ## remote.h ##\n      @@ remote.h: int branch_has_merge_config(struct branch *branch);\n     @@ t/meson.build: integration_tests = [\n         't5582-fetch-negative-refspec.sh',\n         't5583-push-branches.sh',\n         't5584-http-429-retry.sh',\n     -+  't5585-fetch-tracking-refspec.sh',\n     ++  't5585-fetch-refmap.sh',\n         't5600-clone-fail-cleanup.sh',\n         't5601-clone.sh',\n         't5602-clone-remote-exec.sh',\n      \n     - ## t/t5505-remote.sh ##\n     -@@ t/t5505-remote.sh: test_expect_success 'filters are listed by git remote -v only' '\n     - \ttest_grep ! \"\\[blob:none\\]\" out\n     - '\n     - \n     -+test_expect_success 'add remote -t keeps an explicit refspec in a shallow repository' '\n     -+\ttest_when_finished \"rm -rf shallow-add\" &&\n     -+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n     -+\t\tone shallow-add &&\n     -+\t(\n     -+\t\tcd shallow-add &&\n     -+\t\tgit remote add -t main upstream ../two &&\n     -+\t\ttest_cmp_config \"+refs/heads/main:refs/remotes/upstream/main\" \\\n     -+\t\t\tremote.upstream.fetch\n     -+\t)\n     -+'\n     -+\n     -+test_expect_success 'add remote keeps the wildcard refspec in a full repository' '\n     -+\ttest_when_finished \"rm -rf full-add\" &&\n     -+\tgit clone --no-local one full-add &&\n     -+\t(\n     -+\t\tcd full-add &&\n     -+\t\tgit remote add upstream ../two &&\n     -+\t\ttest_cmp_config \"+refs/heads/*:refs/remotes/upstream/*\" \\\n     -+\t\t\tremote.upstream.fetch\n     -+\t)\n     -+'\n     -+\n     -+test_expect_success 'a remote added in a shallow repository defaults to the tracking refspec' '\n     -+\ttest_when_finished \"rm -rf shallow-add\" &&\n     -+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n     -+\t\tone shallow-add &&\n     -+\t(\n     -+\t\tcd shallow-add &&\n     -+\t\tgit remote add upstream ../two &&\n     -+\t\ttest_cmp_config \"+:\" remote.upstream.fetch &&\n     -+\t\tgit fetch upstream &&\n     -+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n     -+\t\tcat >expect <<-\\EOF &&\n     -+\t\trefs/remotes/upstream/HEAD\n     -+\t\trefs/remotes/upstream/main\n     -+\t\tEOF\n     -+\t\ttest_cmp expect actual &&\n     -+\t\tgit branch --set-upstream-to=upstream &&\n     -+\t\ttest_cmp_config upstream branch.main.remote &&\n     -+\t\ttest_cmp_config refs/heads/main branch.main.merge\n     -+\t)\n     -+'\n     -+\n     - test_expect_success 'check remote-tracking' '\n     - \t(\n     - \t\tcd test &&\n     -\n     - ## t/t5585-fetch-tracking-refspec.sh (new) ##\n     + ## t/t5585-fetch-refmap.sh (new) ##\n      @@\n      +#!/bin/sh\n      +\n     -+test_description='\"git fetch\" with the tracking refspec \":\" (or \"+:\")\n     ++test_description='\"git fetch\" with a remote.<name>.refmap but no remote.<name>.fetch\n      +\n     -+The special refspec fetches whichever branches at the remote our local\n     -+branches have as their @{upstream}, plus the default branch of that\n     -+remote, which is always included so that it is available even before\n     -+anything is set up to track it. git remote add uses it by default for\n     -+a new remote in an already shallow repository, so that adding a\n     -+second remote there does not turn the next plain fetch or pull into\n     -+one that negotiates history for every branch the remote has.\n     ++When a remote has a refmap configured but no fetch refspec, a\n     ++refspec-less fetch infers what to fetch from the local branches whose\n     ++@{upstream} is on that remote, plus the default branch of that remote,\n     ++which is always included so that it is available even before anything\n     ++is set up to track it.\n      +'\n      +\n      +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n     @@ t/t5585-fetch-tracking-refspec.sh (new)\n      +\t(\n      +\t\tcd client &&\n      +\t\tgit remote add upstream .. &&\n     -+\t\ttest_cmp_config \"+:\" remote.upstream.fetch\n     ++\t\ttest_might_fail git config unset remote.upstream.fetch &&\n     ++\t\tgit config remote.upstream.refmap \\\n     ++\t\t\t\"+refs/heads/*:refs/remotes/upstream/*\"\n      +\t)\n      +'\n      +\n     @@ t/t5585-fetch-tracking-refspec.sh (new)\n      +\t)\n      +'\n      +\n     -+test_expect_success 'git remote show does not choke on the tracking refspec' '\n     ++test_expect_success 'git remote show does not choke on a refmap-only remote' '\n      +\t(\n      +\t\tcd client &&\n      +\t\tgit remote show upstream\n -:  ---------- > 3:  604e584956 remote: add \"git remote add --limited-fetch\"\n -:  ---------- > 4:  31462e9445 remote: default to --limited-fetch in a shallow repository\n\n-- \ngitgitgadget\n"},{"id":"553274","messageId":"b04c00b974ce488ea1eb82556040fb54c05dad5a.1790333402.git.gitgitgadget@gmail.com","threadId":"66353","inReplyTo":"pull.2412.v3.git.git.1790333402.gitgitgadget@gmail.com","subject":"[PATCH v3 1/4] fetch: add remote.<name>.refmap","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-25T10:49:59Z","receivedAt":"2026-09-25T10:50:07Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nAdd a per-remote config variable, remote.<name>.refmap, that provides\nthe default value for --refmap the same way remote.<name>.fetch\nalready provides the default refspecs to fetch. It only takes effect\nwhen there is something explicit to fetch, on the command line or via\nremote.<name>.fetch, matching how --refmap itself already behaves.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/config/remote.adoc |  6 ++++++\n Documentation/fetch-options.adoc |  3 +++\n builtin/fetch.c                  |  8 +++++---\n remote.c                         | 12 +++++++++++-\n remote.h                         |  6 ++++++\n t/t5510-fetch.sh                 | 17 +++++++++++++++++\n 6 files changed, 48 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc\nindex 3a20d0f752..00f9ed8d3d 100644\n--- a/Documentation/config/remote.adoc\n+++ b/Documentation/config/remote.adoc\n@@ -33,6 +33,12 @@ remote.<name>.fetch::\n \tThe default set of \"refspec\" for linkgit:git-fetch[1]. See\n \tlinkgit:git-fetch[1].\n \n+remote.<name>.refmap::\n+\tThe default value of the `--refmap` option for linkgit:git-fetch[1].\n+\tOnly takes effect when the fetch names what to fetch explicitly,\n+\teither on the command line or via `remote.<name>.fetch`. See the\n+\t`--refmap` entry in linkgit:git-fetch[1].\n+\n remote.<name>.push::\n \tThe default set of \"refspec\" for linkgit:git-push[1]. See\n \tlinkgit:git-push[1].\ndiff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc\nindex 035f780e58..538914bc6e 100644\n--- a/Documentation/fetch-options.adoc\n+++ b/Documentation/fetch-options.adoc\n@@ -244,6 +244,9 @@ endif::git-pull[]\n \trefspecs and rely entirely on the refspecs supplied as\n \tcommand-line arguments. See section on \"Configured Remote-tracking\n \tBranches\" for details.\n++\n+`remote.<name>.refmap` provides the default value for this option, the\n+same way `remote.<name>.fetch` provides the default refspecs to fetch.\n \n `-t`::\n `--tags`::\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 533fdfe7d8..7651b41139 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -509,6 +509,8 @@ static struct ref *get_ref_map(struct remote *remote,\n \tstruct ref *rm;\n \tstruct ref *ref_map = NULL;\n \tstruct ref **tail = &ref_map;\n+\tstruct refspec *effective_refmap =\n+\t\trefmap.nr ? &refmap : remote ? &remote->refmap : NULL;\n \n \t/* opportunistically-updated references: */\n \tstruct ref *orefs = NULL, **oref_tail = &orefs;\n@@ -552,14 +554,14 @@ static struct ref *get_ref_map(struct remote *remote,\n \t\t * by ref_remove_duplicates() in favor of one of these\n \t\t * opportunistic entries with FETCH_HEAD_IGNORE.\n \t\t */\n-\t\tif (refmap.nr)\n-\t\t\tfetch_refspec = &refmap;\n+\t\tif (effective_refmap && effective_refmap->nr)\n+\t\t\tfetch_refspec = effective_refmap;\n \t\telse\n \t\t\tfetch_refspec = &remote->fetch;\n \n \t\tfor (i = 0; i < fetch_refspec->nr; i++)\n \t\t\tget_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1);\n-\t} else if (refmap.nr) {\n+\t} else if (effective_refmap && effective_refmap->nr) {\n \t\tdie(\"--refmap option is only meaningful with command-line refspec(s)\");\n \t} else {\n \t\t/* Use the defaults */\ndiff --git a/remote.c b/remote.c\nindex fe62068463..017cd9d13e 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -152,6 +152,7 @@ static struct remote *make_remote(struct remote_state *remote_state,\n \tret->name = xstrndup(name, len);\n \trefspec_init_push(&ret->push, the_hash_algo);\n \trefspec_init_fetch(&ret->fetch, the_hash_algo);\n+\trefspec_init_fetch(&ret->refmap, the_hash_algo);\n \tstring_list_init_dup(&ret->server_options);\n \tstring_list_init_dup(&ret->negotiation_restrict);\n \tstring_list_init_dup(&ret->negotiation_include);\n@@ -176,6 +177,7 @@ static void remote_clear(struct remote *remote)\n \n \trefspec_clear(&remote->push);\n \trefspec_clear(&remote->fetch);\n+\trefspec_clear(&remote->refmap);\n \n \tfree((char *)remote->receivepack);\n \tfree((char *)remote->uploadpack);\n@@ -539,6 +541,12 @@ static int handle_config(const char *key, const char *value,\n \t\t\treturn -1;\n \t\trefspec_append(&remote->fetch, v);\n \t\tfree(v);\n+\t} else if (!strcmp(subkey, \"refmap\")) {\n+\t\tchar *v;\n+\t\tif (git_config_string(&v, key, value))\n+\t\t\treturn -1;\n+\t\trefspec_append(&remote->refmap, v);\n+\t\tfree(v);\n \t} else if (!strcmp(subkey, \"receivepack\")) {\n \t\tchar *v;\n \t\tif (git_config_string(&v, key, value))\n@@ -988,7 +996,9 @@ void ref_push_report_free(struct ref_push_report *report)\n \n int remote_find_tracking(struct remote *remote, struct refspec_item *refspec)\n {\n-\treturn refspec_find_match(&remote->fetch, refspec);\n+\tif (remote->fetch.nr)\n+\t\treturn refspec_find_match(&remote->fetch, refspec);\n+\treturn refspec_find_match(&remote->refmap, refspec);\n }\n \n static struct ref *alloc_ref_with_prefix(const char *prefix, size_t prefixlen,\ndiff --git a/remote.h b/remote.h\nindex cca02033b9..ac485a584d 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -90,6 +90,12 @@ struct remote {\n \n \tstruct refspec fetch;\n \n+\t/*\n+\t * How to map refs fetched without an explicit destination into our\n+\t * own namespace, the same as the --refmap command line option.\n+\t */\n+\tstruct refspec refmap;\n+\n \t/*\n \t * The setting for whether to fetch tags (as a separate rule from the\n \t * configured refspecs);\ndiff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh\nindex a8d38d9176..bd853d0a84 100755\n--- a/t/t5510-fetch.sh\n+++ b/t/t5510-fetch.sh\n@@ -927,6 +927,23 @@ test_expect_success 'explicit --refmap option overrides remote.*.fetch' '\n \t)\n '\n \n+test_expect_success 'remote.*.refmap acts like --refmap on the command line' '\n+\ttest_when_finished \"git -C three config --unset remote.origin.refmap\" &&\n+\tgit branch -f side &&\n+\tgit -C three config remote.origin.refmap \\\n+\t\t\"refs/heads/*:refs/remotes/other/*\" &&\n+\t(\n+\t\tcd three &&\n+\t\tgit update-ref refs/remotes/origin/main base-origin-main &&\n+\t\to=$(git rev-parse --verify refs/remotes/origin/main) &&\n+\t\tgit fetch origin main &&\n+\t\tn=$(git rev-parse --verify refs/remotes/origin/main) &&\n+\t\ttest \"$o\" = \"$n\" &&\n+\t\ttest_must_fail git rev-parse --verify refs/remotes/origin/side &&\n+\t\tgit rev-parse --verify refs/remotes/other/main\n+\t)\n+'\n+\n test_expect_success 'explicitly empty --refmap option disables remote.*.fetch' '\n \tgit branch -f side &&\n \t(\n-- \ngitgitgadget\n\n"},{"id":"553276","messageId":"4ec508a223a19c3aac8cf368e3dc0314ff6de479.1790333402.git.gitgitgadget@gmail.com","threadId":"66353","inReplyTo":"pull.2412.v3.git.git.1790333402.gitgitgadget@gmail.com","subject":"[PATCH v3 2/4] fetch: infer branches to fetch from a refmap-only remote","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-25T10:50:00Z","receivedAt":"2026-09-25T10:50:09Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nConfiguring remote.<name>.refmap without remote.<name>.fetch used to\nmake a refspec-less \"git fetch <name>\" fail with \"--refmap option is\nonly meaningful with command-line refspec(s)\", since a refmap only\nsays where to put fetched refs, not what to fetch.\n\nMake that case infer what to fetch: the local branches whose\n@{upstream} is already on that remote, plus the remote's default\nbranch, which is always included so it is available even before\nanything is set up to track it. This lets a remote be configured to\nfetch only the branches actually in use, without listing them by\nhand in remote.<name>.fetch, and without needing to touch the\ncommand line every time.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/config/remote.adoc |   7 +-\n Documentation/fetch-options.adoc |   8 +-\n builtin/fetch.c                  | 134 +++++++++++++++++++++++++------\n remote.c                         |  29 +++++++\n remote.h                         |   3 +\n t/meson.build                    |   1 +\n t/t5585-fetch-refmap.sh          | 124 ++++++++++++++++++++++++++++\n 7 files changed, 277 insertions(+), 29 deletions(-)\n create mode 100755 t/t5585-fetch-refmap.sh\n\ndiff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc\nindex 00f9ed8d3d..5b68af3af1 100644\n--- a/Documentation/config/remote.adoc\n+++ b/Documentation/config/remote.adoc\n@@ -35,9 +35,10 @@ remote.<name>.fetch::\n \n remote.<name>.refmap::\n \tThe default value of the `--refmap` option for linkgit:git-fetch[1].\n-\tOnly takes effect when the fetch names what to fetch explicitly,\n-\teither on the command line or via `remote.<name>.fetch`. See the\n-\t`--refmap` entry in linkgit:git-fetch[1].\n+\tIf `remote.<name>.fetch` is not set either, a refspec-less fetch\n+\tinfers what to fetch from local branches built on this remote,\n+\tinstead of fetching every branch it has. See the `--refmap` entry\n+\tin linkgit:git-fetch[1].\n \n remote.<name>.push::\n \tThe default set of \"refspec\" for linkgit:git-push[1]. See\ndiff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc\nindex 538914bc6e..c973a07caf 100644\n--- a/Documentation/fetch-options.adoc\n+++ b/Documentation/fetch-options.adoc\n@@ -245,8 +245,12 @@ endif::git-pull[]\n \tcommand-line arguments. See section on \"Configured Remote-tracking\n \tBranches\" for details.\n +\n-`remote.<name>.refmap` provides the default value for this option, the\n-same way `remote.<name>.fetch` provides the default refspecs to fetch.\n+When a refmap is active (from `--refmap` or `remote.<name>.refmap`) but\n+there is nothing to fetch, neither on the command line nor from\n+`remote.<name>.fetch`, Git infers what to fetch from the local branches\n+whose `@{upstream}` is on that remote, plus the remote's default branch,\n+which is always included so that it is available even before anything\n+is set up to track it.\n \n `-t`::\n `--tags`::\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 7651b41139..c8ce89cf30 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -503,7 +503,8 @@ static void filter_prefetch_refspec(struct refspec *rs)\n static struct ref *get_ref_map(struct remote *remote,\n \t\t\t       const struct ref *remote_refs,\n \t\t\t       struct refspec *rs,\n-\t\t\t       int tags, int *autotags)\n+\t\t\t       int tags, int *autotags,\n+\t\t\t       char **bootstrap_head_branch)\n {\n \tint i;\n \tstruct ref *rm;\n@@ -511,6 +512,8 @@ static struct ref *get_ref_map(struct remote *remote,\n \tstruct ref **tail = &ref_map;\n \tstruct refspec *effective_refmap =\n \t\trefmap.nr ? &refmap : remote ? &remote->refmap : NULL;\n+\tstruct refspec inferred_rs;\n+\tint inferred_branches = 0;\n \n \t/* opportunistically-updated references: */\n \tstruct ref *orefs = NULL, **oref_tail = &orefs;\n@@ -518,15 +521,71 @@ static struct ref *get_ref_map(struct remote *remote,\n \tstruct hashmap existing_refs;\n \tint existing_refs_populated = 0;\n \n+\trefspec_init_fetch(&inferred_rs, the_hash_algo);\n+\n \tfilter_prefetch_refspec(rs);\n \tif (remote)\n \t\tfilter_prefetch_refspec(&remote->fetch);\n \n+\tif (!rs->nr && remote && !remote->fetch.nr &&\n+\t    effective_refmap && effective_refmap->nr) {\n+\t\tstruct string_list tracked = STRING_LIST_INIT_DUP;\n+\t\tstruct string_list_item *item;\n+\t\tstruct ref *head;\n+\t\tchar *default_branch = NULL;\n+\t\tchar *default_branch_dst = NULL;\n+\n+\t\tbranches_tracking_remote(remote, &tracked);\n+\t\tfor_each_string_list_item(item, &tracked)\n+\t\t\trefspec_append(&inferred_rs, item->string);\n+\n+\t\t/* Always fetch the default branch too, as \"HEAD\". */\n+\t\thead = get_remote_ref(remote_refs, \"HEAD\");\n+\t\tif (head && head->symref && *head->symref)\n+\t\t\tdefault_branch = xstrdup(head->symref);\n+\t\tfree_one_ref(head);\n+\n+\t\tfor (i = 0; default_branch && !default_branch_dst &&\n+\t\t\t    !string_list_has_string(&tracked, default_branch) &&\n+\t\t\t    i < effective_refmap->nr; i++) {\n+\t\t\tstruct refspec_item *map = &effective_refmap->items[i];\n+\n+\t\t\tif (map->pattern)\n+\t\t\t\tmatch_refname_with_pattern(map->src, default_branch,\n+\t\t\t\t\t\t\t    map->dst, &default_branch_dst);\n+\t\t\telse if (!strcmp(map->src, default_branch))\n+\t\t\t\tdefault_branch_dst = xstrdup(map->dst);\n+\t\t}\n+\n+\t\tif (default_branch_dst) {\n+\t\t\tstruct refspec_item head_item = { .force = 1 };\n+\n+\t\t\thead_item.src = xstrdup(\"HEAD\");\n+\t\t\thead_item.dst = default_branch_dst;\n+\t\t\tget_fetch_map(remote_refs, &head_item, &tail, 1);\n+\t\t\tfree(head_item.src);\n+\t\t}\n+\n+\t\tif (default_branch && bootstrap_head_branch) {\n+\t\t\tconst char *branch_name = default_branch;\n+\t\t\tskip_prefix(branch_name, \"refs/heads/\", &branch_name);\n+\t\t\t*bootstrap_head_branch = xstrdup(branch_name);\n+\t\t}\n+\n+\t\tfree(default_branch);\n+\t\tfree(default_branch_dst);\n+\t\tstring_list_clear(&tracked, 0);\n+\n+\t\trs = &inferred_rs;\n+\t\tinferred_branches = 1;\n+\t}\n+\n \tif (rs->nr) {\n \t\tstruct refspec *fetch_refspec;\n \n \t\tfor (i = 0; i < rs->nr; i++) {\n-\t\t\tget_fetch_map(remote_refs, &rs->items[i], &tail, 0);\n+\t\t\tget_fetch_map(remote_refs, &rs->items[i], &tail,\n+\t\t\t\t      inferred_branches);\n \t\t\tif (rs->items[i].dst && rs->items[i].dst[0])\n \t\t\t\t*autotags = 1;\n \t\t}\n@@ -561,6 +620,8 @@ static struct ref *get_ref_map(struct remote *remote,\n \n \t\tfor (i = 0; i < fetch_refspec->nr; i++)\n \t\t\tget_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1);\n+\t} else if (inferred_branches) {\n+\t\t/* Already fully handled above. */\n \t} else if (effective_refmap && effective_refmap->nr) {\n \t\tdie(\"--refmap option is only meaningful with command-line refspec(s)\");\n \t} else {\n@@ -656,6 +717,7 @@ static struct ref *get_ref_map(struct remote *remote,\n \tif (existing_refs_populated)\n \t\thashmap_clear_and_free(&existing_refs, struct refname_hash_entry, ent);\n \n+\trefspec_clear(&inferred_rs);\n \treturn ref_map;\n }\n \n@@ -1753,14 +1815,14 @@ static void warn_set_head(const char *remote, const char *head_name,\n }\n \n static int set_head(const struct ref *remote_refs, struct remote *remote,\n-\t\t\tint follow_remote_head)\n+\t\t\tint follow_remote_head, const char *known_head_branch)\n {\n \tint result = 0, create_only, baremirror, was_detached;\n \tstruct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,\n \t\t      b_local_head = STRBUF_INIT;\n \tconst char *no_warn_branch = remote->no_warn_branch;\n \tchar *head_name = NULL;\n-\tstruct ref *ref, *matches;\n+\tstruct ref *ref, *matches = NULL;\n \tstruct ref *fetch_map = NULL, **fetch_map_tail = &fetch_map;\n \tstruct refspec_item refspec = {\n \t\t.force = 0,\n@@ -1771,19 +1833,23 @@ static int set_head(const struct ref *remote_refs, struct remote *remote,\n \tstruct string_list heads = STRING_LIST_INIT_DUP;\n \tstruct ref_store *refs = get_main_ref_store(the_repository);\n \n-\tget_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);\n-\tmatches = guess_remote_head(find_ref_by_name(remote_refs, \"HEAD\"),\n-\t\t\t\t    fetch_map, REMOTE_GUESS_HEAD_ALL);\n-\tfor (ref = matches; ref; ref = ref->next) {\n-\t\tstring_list_append(&heads, strip_refshead(ref->name));\n-\t}\n+\tif (known_head_branch) {\n+\t\thead_name = xstrdup(known_head_branch);\n+\t} else {\n+\t\tget_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);\n+\t\tmatches = guess_remote_head(find_ref_by_name(remote_refs, \"HEAD\"),\n+\t\t\t\t\t    fetch_map, REMOTE_GUESS_HEAD_ALL);\n+\t\tfor (ref = matches; ref; ref = ref->next) {\n+\t\t\tstring_list_append(&heads, strip_refshead(ref->name));\n+\t\t}\n \n-\tif (!heads.nr)\n-\t\tresult = 1;\n-\telse if (heads.nr > 1)\n-\t\tresult = 1;\n-\telse\n-\t\thead_name = xstrdup(heads.items[0].string);\n+\t\tif (!heads.nr)\n+\t\t\tresult = 1;\n+\t\telse if (heads.nr > 1)\n+\t\t\tresult = 1;\n+\t\telse\n+\t\t\thead_name = xstrdup(heads.items[0].string);\n+\t}\n \n \tif (!head_name)\n \t\tgoto cleanup;\n@@ -1925,6 +1991,7 @@ static int do_fetch(struct transport *transport,\n \tstruct strmap rejected_refs = STRMAP_INIT;\n \tint summary_width = 0;\n \tint follow_remote_head;\n+\tchar *bootstrap_head_branch = NULL;\n \n \tif (tags == TAGS_DEFAULT) {\n \t\tif (transport->remote->fetch_tags == 2)\n@@ -1960,15 +2027,31 @@ static int do_fetch(struct transport *transport,\n \t\trefspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);\n \t} else {\n \t\tstruct branch *branch = branch_get(NULL);\n-\n-\t\tif (transport->remote->fetch.nr) {\n+\t\tint tracks_this_remote = branch && branch_has_merge_config(branch) &&\n+\t\t\t!strcmp(branch->remote_name, transport->remote->name);\n+\t\tstruct refspec *effective_refmap = refmap.nr ? &refmap :\n+\t\t\t&transport->remote->refmap;\n+\t\tint inferred_branches = !transport->remote->fetch.nr &&\n+\t\t\teffective_refmap->nr;\n+\n+\t\tif (inferred_branches) {\n+\t\t\tstruct string_list tracked = STRING_LIST_INIT_DUP;\n+\t\t\tstruct string_list_item *item;\n+\n+\t\t\tbranches_tracking_remote(transport->remote, &tracked);\n+\t\t\tfor_each_string_list_item(item, &tracked)\n+\t\t\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n+\t\t\t\t\t    item->string);\n+\t\t\tstring_list_clear(&tracked, 0);\n+\t\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n+\t\t} else if (transport->remote->fetch.nr) {\n \t\t\trefspec_ref_prefixes(&transport->remote->fetch,\n \t\t\t\t\t     &transport_ls_refs_options.ref_prefixes);\n-\t\t\tif (follow_remote_head != FOLLOW_REMOTE_NEVER)\n-\t\t\t\tdo_set_head = 1;\n \t\t}\n-\t\tif (branch && branch_has_merge_config(branch) &&\n-\t\t    !strcmp(branch->remote_name, transport->remote->name)) {\n+\t\tif ((transport->remote->fetch.nr || inferred_branches) &&\n+\t\t    follow_remote_head != FOLLOW_REMOTE_NEVER)\n+\t\t\tdo_set_head = 1;\n+\t\tif (tracks_this_remote) {\n \t\t\tint i;\n \t\t\tfor (i = 0; i < branch->merge_nr; i++) {\n \t\t\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n@@ -2008,7 +2091,8 @@ static int do_fetch(struct transport *transport,\n \ttransport_ls_refs_options_release(&transport_ls_refs_options);\n \n \tref_map = get_ref_map(transport->remote, remote_refs, rs,\n-\t\t\t      tags, &autotags);\n+\t\t\t      tags, &autotags, &bootstrap_head_branch);\n+\n \tif (!update_head_ok)\n \t\tcheck_not_current_branch(ref_map);\n \n@@ -2171,10 +2255,12 @@ static int do_fetch(struct transport *transport,\n \t\t * Way too many cases where this can go wrong so let's just\n \t\t * ignore errors and fail silently for now.\n \t\t */\n-\t\tset_head(remote_refs, transport->remote, follow_remote_head);\n+\t\tset_head(remote_refs, transport->remote, follow_remote_head,\n+\t\t\t bootstrap_head_branch);\n \t}\n \n cleanup:\n+\tfree(bootstrap_head_branch);\n \t/*\n \t * When using batched updates, we want to commit the non-rejected\n \t * updates and also handle the rejections.\ndiff --git a/remote.c b/remote.c\nindex 017cd9d13e..a5deb9b929 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -1884,6 +1884,35 @@ int branch_merge_matches(struct branch *branch,\n \treturn refname_match(branch->merge[i]->src, refname);\n }\n \n+struct branches_tracking_remote_cb_data {\n+\tstruct remote *remote;\n+\tstruct string_list *tracked;\n+};\n+\n+static int add_if_tracking_remote(const struct reference *ref, void *cb_data)\n+{\n+\tstruct branches_tracking_remote_cb_data *data = cb_data;\n+\tstruct branch *branch;\n+\n+\tbranch = branch_get(ref->name);\n+\tif (!branch_has_merge_config(branch) ||\n+\t    strcmp(branch->remote_name, data->remote->name))\n+\t\treturn 0;\n+\n+\tfor (int i = 0; i < branch->merge_nr; i++)\n+\t\tstring_list_insert(data->tracked, branch->merge[i]->src);\n+\n+\treturn 0;\n+}\n+\n+void branches_tracking_remote(struct remote *remote, struct string_list *tracked)\n+{\n+\tstruct branches_tracking_remote_cb_data data = { remote, tracked };\n+\n+\trefs_for_each_branch_ref(get_main_ref_store(the_repository),\n+\t\t\t\t  add_if_tracking_remote, &data);\n+}\n+\n __attribute__((format (printf,2,3)))\n static char *error_buf(struct strbuf *err, const char *fmt, ...)\n {\ndiff --git a/remote.h b/remote.h\nindex ac485a584d..da6c3ef53a 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -359,6 +359,9 @@ int branch_has_merge_config(struct branch *branch);\n \n int branch_merge_matches(struct branch *, int n, const char *);\n \n+/* fills tracked with the refname of every local branch's upstream on remote */\n+void branches_tracking_remote(struct remote *remote, struct string_list *tracked);\n+\n /* list of the remote in a group as configured */\n struct remote_group_data {\n \tconst char *name;\ndiff --git a/t/meson.build b/t/meson.build\nindex 3ca7b27104..ab83d5409c 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -728,6 +728,7 @@ integration_tests = [\n   't5582-fetch-negative-refspec.sh',\n   't5583-push-branches.sh',\n   't5584-http-429-retry.sh',\n+  't5585-fetch-refmap.sh',\n   't5600-clone-fail-cleanup.sh',\n   't5601-clone.sh',\n   't5602-clone-remote-exec.sh',\ndiff --git a/t/t5585-fetch-refmap.sh b/t/t5585-fetch-refmap.sh\nnew file mode 100755\nindex 0000000000..4b1f51f6cd\n--- /dev/null\n+++ b/t/t5585-fetch-refmap.sh\n@@ -0,0 +1,124 @@\n+#!/bin/sh\n+\n+test_description='\"git fetch\" with a remote.<name>.refmap but no remote.<name>.fetch\n+\n+When a remote has a refmap configured but no fetch refspec, a\n+refspec-less fetch infers what to fetch from the local branches whose\n+@{upstream} is on that remote, plus the default branch of that remote,\n+which is always included so that it is available even before anything\n+is set up to track it.\n+'\n+\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup' '\n+\ttest_commit main-1 &&\n+\ttest_commit main-2 &&\n+\tgit checkout -b side main-1 &&\n+\ttest_commit side-1 &&\n+\tgit checkout -b next main-1 &&\n+\ttest_commit next-1 &&\n+\tgit checkout main\n+'\n+\n+test_expect_success 'clone shallow and single-branch, then add a second remote' '\n+\tgit clone --no-local --depth=1 --branch main --single-branch . client &&\n+\t(\n+\t\tcd client &&\n+\t\tgit remote add upstream .. &&\n+\t\ttest_might_fail git config unset remote.upstream.fetch &&\n+\t\tgit config remote.upstream.refmap \\\n+\t\t\t\"+refs/heads/*:refs/remotes/upstream/*\"\n+\t)\n+'\n+\n+test_expect_success 'fetching the new remote does not need every branch it has' '\n+\t(\n+\t\tcd client &&\n+\t\tgit fetch upstream &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\tEOF\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'set-upstream-to now resolves right after that first fetch' '\n+\t(\n+\t\tcd client &&\n+\t\tgit branch --set-upstream-to=upstream &&\n+\t\ttest_cmp_config upstream branch.main.remote &&\n+\t\ttest_cmp_config refs/heads/main branch.main.merge\n+\t)\n+'\n+\n+test_expect_success 'a branch checked out from a one-time fetch is kept updated by later plain fetches' '\n+\t(\n+\t\tcd client &&\n+\t\tgit fetch upstream side:refs/remotes/upstream/side &&\n+\t\tgit branch side-topic upstream/side\n+\t) &&\n+\tgit checkout side &&\n+\ttest_commit side-2 &&\n+\tgit checkout main &&\n+\t(\n+\t\tcd client &&\n+\t\tgit fetch upstream &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\trefs/remotes/upstream/side\n+\t\tEOF\n+\t\ttest_cmp expect actual &&\n+\t\tgit rev-parse refs/remotes/upstream/side >actual-oid &&\n+\t\tgit -C .. rev-parse side >expect-oid &&\n+\t\ttest_cmp expect-oid actual-oid\n+\t)\n+'\n+\n+test_expect_success 'a second branch tracking the same upstream branch does not fetch it twice' '\n+\t(\n+\t\tcd client &&\n+\t\tgit branch side-topic-2 upstream/side &&\n+\t\tgit fetch upstream &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\trefs/remotes/upstream/side\n+\t\tEOF\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'a branch tracking a different remote is not fetched from upstream' '\n+\t(\n+\t\tcd client &&\n+\t\tgit remote add other .. &&\n+\t\tgit fetch other next:refs/remotes/other/next &&\n+\t\tgit branch next-topic other/next &&\n+\t\tgit fetch upstream &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\trefs/remotes/upstream/side\n+\t\tEOF\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'git remote show does not choke on a refmap-only remote' '\n+\t(\n+\t\tcd client &&\n+\t\tgit remote show upstream\n+\t)\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"553275","messageId":"604e5849565f9a1d1697cd0983809f80f0e1319b.1790333402.git.gitgitgadget@gmail.com","threadId":"66353","inReplyTo":"pull.2412.v3.git.git.1790333402.gitgitgadget@gmail.com","subject":"[PATCH v3 3/4] remote: add \"git remote add --limited-fetch\"","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-25T10:50:01Z","receivedAt":"2026-09-25T10:50:10Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nA remote added the ordinary way tracks every branch it has, via a\nwildcard remote.<name>.fetch refspec. That is wasteful for a remote\nwhose history is only worth following for the branches actually in\nuse locally, and it can make \"git fetch\" negotiate history for\nbranches nobody asked for.\n\nGive \"git remote add\" a --limited-fetch option that sets up\nremote.<name>.refmap instead of remote.<name>.fetch, so that\n\"git fetch <name>\" only fetches the branches already tracked, plus\nthe remote's default branch, as described in the previous commit.\nIt is rejected together with -t/--track or --mirror, since those\nalready say explicitly what to fetch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/git-remote.adoc |  8 +++++++-\n builtin/remote.c              | 28 ++++++++++++++++++++++------\n t/t5505-remote.sh             | 16 ++++++++++++++++\n 3 files changed, 45 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/git-remote.adoc b/Documentation/git-remote.adoc\nindex eaae30aa88..74fbce6e0e 100644\n--- a/Documentation/git-remote.adoc\n+++ b/Documentation/git-remote.adoc\n@@ -10,7 +10,7 @@ SYNOPSIS\n --------\n [synopsis]\n git remote [-v | --verbose]\n-git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] <name> <URL>\n+git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] [--[no-]limited-fetch] <name> <URL>\n git remote rename [--[no-]progress] <old> <new>\n git remote remove <name>\n git remote set-head <name> (-a | --auto | -d | --delete | <branch>)\n@@ -70,6 +70,12 @@ the `refs/remotes/<name>/` namespace, a refspec to track only _<branch>_\n is created.  You can give more than one `-t <branch>` to track\n multiple branches without grabbing all branches.\n +\n+With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec\n+that tracks all branches, `remote.<name>.refmap` is set up so that a\n+refspec-less `git fetch <name>` only fetches branches our local branches\n+are built on, plus the remote's default branch. See the `--refmap` entry\n+in linkgit:git-fetch[1] for details.\n++\n With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set\n up to point at remote's _<master>_ branch. See also the set-head command.\n +\ndiff --git a/builtin/remote.c b/builtin/remote.c\nindex de989ea3ba..f036dd5d6f 100644\n--- a/builtin/remote.c\n+++ b/builtin/remote.c\n@@ -179,6 +179,7 @@ static int add(int argc, const char **argv, const char *prefix,\n {\n \tint fetch = 0, fetch_tags = TAGS_DEFAULT;\n \tunsigned mirror = MIRROR_NONE;\n+\tint limited_fetch = -1; /* unspecified */\n \tstruct string_list track = STRING_LIST_INIT_NODUP;\n \tconst char *master = NULL;\n \tstruct remote *remote;\n@@ -198,6 +199,8 @@ static int add(int argc, const char **argv, const char *prefix,\n \t\tOPT_CALLBACK_F(0, \"mirror\", &mirror, \"(push|fetch)\",\n \t\t\tN_(\"set up remote as a mirror to push to or fetch from\"),\n \t\t\tPARSE_OPT_OPTARG | PARSE_OPT_COMP_ARG, parse_mirror_opt),\n+\t\tOPT_BOOL(0, \"limited-fetch\", &limited_fetch,\n+\t\t\tN_(\"fetch only the branches we build on, instead of every branch\")),\n \t\tOPT_END()\n \t};\n \n@@ -211,6 +214,10 @@ static int add(int argc, const char **argv, const char *prefix,\n \t\tdie(_(\"specifying a master branch makes no sense with --mirror\"));\n \tif (mirror && !(mirror & MIRROR_FETCH) && track.nr)\n \t\tdie(_(\"specifying branches to track makes sense only with fetch mirrors\"));\n+\tif (limited_fetch == 1 && track.nr)\n+\t\tdie(_(\"--limited-fetch does not make sense with -t/--track\"));\n+\tif (limited_fetch == 1 && mirror)\n+\t\tdie(_(\"--limited-fetch does not make sense with --mirror\"));\n \n \tname = argv[0];\n \turl = argv[1];\n@@ -230,13 +237,22 @@ static int add(int argc, const char **argv, const char *prefix,\n \trepo_config_set(the_repository, buf.buf, url);\n \n \tif (!mirror || mirror & MIRROR_FETCH) {\n+\t\tint use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&\n+\t\t\tlimited_fetch == 1;\n+\n \t\tstrbuf_reset(&buf);\n-\t\tstrbuf_addf(&buf, \"remote.%s.fetch\", name);\n-\t\tif (track.nr == 0)\n-\t\t\tstring_list_append(&track, \"*\");\n-\t\tfor (size_t i = 0; i < track.nr; i++) {\n-\t\t\tadd_branch(buf.buf, track.items[i].string,\n-\t\t\t\t   name, mirror, &buf2);\n+\t\tif (use_limited_fetch) {\n+\t\t\tstrbuf_addf(&buf, \"remote.%s.refmap\", name);\n+\t\t\tstrbuf_reset(&buf2);\n+\t\t\tstrbuf_addf(&buf2, \"+refs/heads/*:refs/remotes/%s/*\", name);\n+\t\t\trepo_config_set(the_repository, buf.buf, buf2.buf);\n+\t\t} else {\n+\t\t\tstrbuf_addf(&buf, \"remote.%s.fetch\", name);\n+\t\t\tif (track.nr == 0)\n+\t\t\t\tstring_list_append(&track, \"*\");\n+\t\t\tfor (size_t i = 0; i < track.nr; i++)\n+\t\t\t\tadd_branch(buf.buf, track.items[i].string,\n+\t\t\t\t\t   name, mirror, &buf2);\n \t\t}\n \t}\n \ndiff --git a/t/t5505-remote.sh b/t/t5505-remote.sh\nindex 5fbfcb0848..0168d5abfe 100755\n--- a/t/t5505-remote.sh\n+++ b/t/t5505-remote.sh\n@@ -137,6 +137,22 @@ test_expect_success 'filters are listed by git remote -v only' '\n \ttest_grep ! \"\\[blob:none\\]\" out\n '\n \n+test_expect_success '--limited-fetch works in a full repository too' '\n+\ttest_when_finished \"rm -rf full-add\" &&\n+\tgit clone --no-local one full-add &&\n+\t(\n+\t\tcd full-add &&\n+\t\tgit remote add --limited-fetch upstream ../two &&\n+\t\ttest_cmp_config \"+refs/heads/*:refs/remotes/upstream/*\" \\\n+\t\t\tremote.upstream.refmap &&\n+\t\ttest_must_fail git config get remote.upstream.fetch\n+\t)\n+'\n+\n+test_expect_success '--limited-fetch conflicts with -t' '\n+\ttest_must_fail git remote add --limited-fetch -t main upstream ../two\n+'\n+\n test_expect_success 'check remote-tracking' '\n \t(\n \t\tcd test &&\n-- \ngitgitgadget\n\n"},{"id":"553277","messageId":"31462e944563c94829a32e35577d7d2567ff8b36.1790333402.git.gitgitgadget@gmail.com","threadId":"66353","inReplyTo":"pull.2412.v3.git.git.1790333402.gitgitgadget@gmail.com","subject":"[PATCH v3 4/4] remote: default to --limited-fetch in a shallow repository","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-25T10:50:02Z","receivedAt":"2026-09-25T10:50:12Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nAdding a second remote to a shallow, single-branch clone used to\nstill fetch every branch that remote has, since \"git remote add\"\nalways set up a wildcard remote.<name>.fetch refspec regardless of\nhow shallow the repository already was. That defeats the purpose of\nhaving cloned shallow and single-branch in the first place, and can\nmake a plain \"git fetch\" on that remote hang or take a very long\ntime on a repository with many branches.\n\nTurn --limited-fetch on by default when the repository is already\nshallow and neither -t/--track nor --mirror was given, so that\nadding a remote there does not by itself commit to following every\nbranch it has. --no-limited-fetch keeps the previous behavior for\nwhoever wants it.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/git-remote.adoc |  8 ++++-\n builtin/remote.c              |  5 ++-\n t/t5505-remote.sh             | 59 +++++++++++++++++++++++++++++++++++\n 3 files changed, 70 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/git-remote.adoc b/Documentation/git-remote.adoc\nindex 74fbce6e0e..82b21eafa7 100644\n--- a/Documentation/git-remote.adoc\n+++ b/Documentation/git-remote.adoc\n@@ -52,6 +52,11 @@ Add a remote named _<name>_ for the repository at\n _<URL>_.  The command `git fetch <name>` can then be used to create and\n update remote-tracking branches `<name>/<branch>`.\n +\n+If the repository is already a shallow repository (see linkgit:git-clone[1]\n+`--depth`) and neither `-t`, `--mirror` nor `--no-limited-fetch` is given,\n+`--limited-fetch` is turned on by default, so that `git fetch <name>` does\n+not need to negotiate history for every branch the remote has.\n++\n With `-f` option, `git fetch <name>` is run immediately after\n the remote information is set up.\n +\n@@ -74,7 +79,8 @@ With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec\n that tracks all branches, `remote.<name>.refmap` is set up so that a\n refspec-less `git fetch <name>` only fetches branches our local branches\n are built on, plus the remote's default branch. See the `--refmap` entry\n-in linkgit:git-fetch[1] for details.\n+in linkgit:git-fetch[1] for details. `--no-limited-fetch` explicitly\n+disables this, overriding the shallow-repository default described above.\n +\n With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set\n up to point at remote's _<master>_ branch. See also the set-head command.\ndiff --git a/builtin/remote.c b/builtin/remote.c\nindex f036dd5d6f..0cf1126976 100644\n--- a/builtin/remote.c\n+++ b/builtin/remote.c\n@@ -16,6 +16,7 @@\n #include \"rebase.h\"\n #include \"refs.h\"\n #include \"refspec.h\"\n+#include \"shallow.h\"\n #include \"odb.h\"\n #include \"strvec.h\"\n #include \"commit-reach.h\"\n@@ -238,7 +239,9 @@ static int add(int argc, const char **argv, const char *prefix,\n \n \tif (!mirror || mirror & MIRROR_FETCH) {\n \t\tint use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&\n-\t\t\tlimited_fetch == 1;\n+\t\t\t(limited_fetch == 1 ||\n+\t\t\t (limited_fetch == -1 &&\n+\t\t\t  is_repository_shallow(the_repository)));\n \n \t\tstrbuf_reset(&buf);\n \t\tif (use_limited_fetch) {\ndiff --git a/t/t5505-remote.sh b/t/t5505-remote.sh\nindex 0168d5abfe..9cb88d8806 100755\n--- a/t/t5505-remote.sh\n+++ b/t/t5505-remote.sh\n@@ -137,6 +137,65 @@ test_expect_success 'filters are listed by git remote -v only' '\n \ttest_grep ! \"\\[blob:none\\]\" out\n '\n \n+test_expect_success 'add remote -t keeps an explicit refspec in a shallow repository' '\n+\ttest_when_finished \"rm -rf shallow-add\" &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\tone shallow-add &&\n+\t(\n+\t\tcd shallow-add &&\n+\t\tgit remote add -t main upstream ../two &&\n+\t\ttest_cmp_config \"+refs/heads/main:refs/remotes/upstream/main\" \\\n+\t\t\tremote.upstream.fetch\n+\t)\n+'\n+\n+test_expect_success 'add remote keeps the wildcard refspec in a full repository' '\n+\ttest_when_finished \"rm -rf full-add\" &&\n+\tgit clone --no-local one full-add &&\n+\t(\n+\t\tcd full-add &&\n+\t\tgit remote add upstream ../two &&\n+\t\ttest_cmp_config \"+refs/heads/*:refs/remotes/upstream/*\" \\\n+\t\t\tremote.upstream.fetch\n+\t)\n+'\n+\n+test_expect_success 'a remote added in a shallow repository defaults to --limited-fetch' '\n+\ttest_when_finished \"rm -rf shallow-add\" &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\tone shallow-add &&\n+\t(\n+\t\tcd shallow-add &&\n+\t\tgit remote add upstream ../two &&\n+\t\ttest_cmp_config \"+refs/heads/*:refs/remotes/upstream/*\" \\\n+\t\t\tremote.upstream.refmap &&\n+\t\ttest_must_fail git config get remote.upstream.fetch &&\n+\t\tgit fetch upstream &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\tEOF\n+\t\ttest_cmp expect actual &&\n+\t\tgit branch --set-upstream-to=upstream &&\n+\t\ttest_cmp_config upstream branch.main.remote &&\n+\t\ttest_cmp_config refs/heads/main branch.main.merge\n+\t)\n+'\n+\n+test_expect_success '--no-limited-fetch overrides the shallow-repository default' '\n+\ttest_when_finished \"rm -rf shallow-add\" &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\tone shallow-add &&\n+\t(\n+\t\tcd shallow-add &&\n+\t\tgit remote add --no-limited-fetch upstream ../two &&\n+\t\ttest_cmp_config \"+refs/heads/*:refs/remotes/upstream/*\" \\\n+\t\t\tremote.upstream.fetch &&\n+\t\ttest_must_fail git config get remote.upstream.refmap\n+\t)\n+'\n+\n test_expect_success '--limited-fetch works in a full repository too' '\n \ttest_when_finished \"rm -rf full-add\" &&\n \tgit clone --no-local one full-add &&\n-- \ngitgitgadget\n"},{"id":"553337","messageId":"xmqq5wztt0r4.fsf@gitster.g","threadId":"66353","inReplyTo":"b04c00b974ce488ea1eb82556040fb54c05dad5a.1790333402.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 1/4] fetch: add remote.<name>.refmap","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-25T22:38:39Z","receivedAt":"2026-09-25T22:38:42Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Harald Nordgren <haraldnordgren@gmail.com>\n>\n> Add a per-remote config variable, remote.<name>.refmap, that provides\n> the default value for --refmap the same way remote.<name>.fetch\n> already provides the default refspecs to fetch. It only takes effect\n> when there is something explicit to fetch, on the command line or via\n\nThis ...\n\n> remote.<name>.fetch, matching how --refmap itself already behaves.\n>\n> Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n\n> +remote.<name>.refmap::\n> +\tThe default value of the `--refmap` option for linkgit:git-fetch[1].\n> +\tOnly takes effect when the fetch names what to fetch explicitly,\n> +\teither on the command line or via `remote.<name>.fetch`. See the\n> +\t`--refmap` entry in linkgit:git-fetch[1].\n\n... and this made me a bit puzzled.  It may be a philosophical\ndifference, but I've always viewed --refmap=<src>:<dst> to \"take\neffect\" whenever they are given, regardless of 0, 1, or more\nexplicit things to fetch.  It is just when you have zero explicit\nthings to fetch, 0 things are mapped via the refmap mechanism and 0\nthings are fetched.\n\nIn other words, what does not \"take effect\" when 0 things are given\nexplicitly to fetch is not the effect of refmap alone, but the\nentire 'git fetch' operation itself.\n\n    The default value of the `--refmap` option for linkgit:git-fetch[1].\n    Used to map remote refs being fetched to remote-tracking refs to\n    store.  See the `--refmap` entry in linkgit:git-fetch[1].\n\n> @@ -244,6 +244,9 @@ endif::git-pull[]\n>  \trefspecs and rely entirely on the refspecs supplied as\n>  \tcommand-line arguments. See section on \"Configured Remote-tracking\n>  \tBranches\" for details.\n> ++\n> +`remote.<name>.refmap` provides the default value for this option, the\n> +same way `remote.<name>.fetch` provides the default refspecs to fetch.\n\nThis is perfect.\n\n> diff --git a/builtin/fetch.c b/builtin/fetch.c\n> index 533fdfe7d8..7651b41139 100644\n> --- a/builtin/fetch.c\n> +++ b/builtin/fetch.c\n> @@ -509,6 +509,8 @@ static struct ref *get_ref_map(struct remote *remote,\n>  \tstruct ref *rm;\n>  \tstruct ref *ref_map = NULL;\n>  \tstruct ref **tail = &ref_map;\n> +\tstruct refspec *effective_refmap =\n> +\t\trefmap.nr ? &refmap : remote ? &remote->refmap : NULL;\n>  \n>  \t/* opportunistically-updated references: */\n>  \tstruct ref *orefs = NULL, **oref_tail = &orefs;\n> @@ -552,14 +554,14 @@ static struct ref *get_ref_map(struct remote *remote,\n>  \t\t * by ref_remove_duplicates() in favor of one of these\n>  \t\t * opportunistic entries with FETCH_HEAD_IGNORE.\n>  \t\t */\n> -\t\tif (refmap.nr)\n> -\t\t\tfetch_refspec = &refmap;\n> +\t\tif (effective_refmap && effective_refmap->nr)\n> +\t\t\tfetch_refspec = effective_refmap;\n>  \t\telse\n>  \t\t\tfetch_refspec = &remote->fetch;\n>  \n>  \t\tfor (i = 0; i < fetch_refspec->nr; i++)\n>  \t\t\tget_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1);\n> -\t} else if (refmap.nr) {\n> +\t} else if (effective_refmap && effective_refmap->nr) {\n>  \t\tdie(\"--refmap option is only meaningful with command-line refspec(s)\");\n>  \t} else {\n>  \t\t/* Use the defaults */\n\nLooking good.  One of these days, we probably should reduce our\nreliance on the file-scope static \"global variables\" but that is\nclearly outside the scope of this topic.  Perhaps once the dust\nsettles after this topic stabilizes.  I wonder if most of them can\nbe added as members to \"struct fetch_config\" and then we can pass\none instance of such struct around in the call chain, or if it\nneeds a lot more involved changes.\n"},{"id":"553339","messageId":"xmqqo6dksyj2.fsf@gitster.g","threadId":"66353","inReplyTo":"4ec508a223a19c3aac8cf368e3dc0314ff6de479.1790333402.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v3 2/4] fetch: infer branches to fetch from a refmap-only remote","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-25T23:26:41Z","receivedAt":"2026-09-25T23:26:45Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Harald Nordgren <haraldnordgren@gmail.com>\n>\n> Configuring remote.<name>.refmap without remote.<name>.fetch used to\n> make a refspec-less \"git fetch <name>\" fail with \"--refmap option is\n> only meaningful with command-line refspec(s)\", since a refmap only\n> says where to put fetched refs, not what to fetch.\n>\n> Make that case infer what to fetch: the local branches whose\n> @{upstream} is already on that remote, plus the remote's default\n> branch, which is always included so it is available even before\n> anything is set up to track it. This lets a remote be configured to\n> fetch only the branches actually in use, without listing them by\n> hand in remote.<name>.fetch, and without needing to touch the\n> command line every time.\n\nYou may not necessarily be interested in what the remote's HEAD is\npointing at.\n\n - You may only be working on top of their 'maint' and their 'next'\n   without ever touching their 'master'.  It would be rude for Git\n   to decide for you to fetch their 'master', which you have no use\n   for, without being asked at all.\n\n - Or you may be very much interested in their 'master' that is\n   pointed at by their HEAD.  In such a case, you would have your\n   local branch that is forked from their 'master' *anyway*, and\n   you'll get one, without special casing their HEAD.\n\nEspecially since we are about to add the knob to trigger this\nmechanism to \"git remote add\", presumably this is to be used for\nsecond and later remotes.  The primary remote may already be\nsupplying \"refs/heads/master -> refs/remotes/origin/master\" when the\nrepository was made with \"git clone --shallow --single-branch\" or\nsomething like that.  That makes it doubly dubious to assume that\nthe branch the HEAD of this secondary remote points at is so special\nand everybody that does \"git remote add\" wants to interact with it.\n\n> diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc\n> index 538914bc6e..c973a07caf 100644\n> --- a/Documentation/fetch-options.adoc\n> +++ b/Documentation/fetch-options.adoc\n> @@ -245,8 +245,12 @@ endif::git-pull[]\n>  \tcommand-line arguments. See section on \"Configured Remote-tracking\n>  \tBranches\" for details.\n>  +\n> -`remote.<name>.refmap` provides the default value for this option, the\n> -same way `remote.<name>.fetch` provides the default refspecs to fetch.\n> +When a refmap is active (from `--refmap` or `remote.<name>.refmap`) but\n> +there is nothing to fetch, neither on the command line nor from\n> +`remote.<name>.fetch`, Git infers what to fetch from the local branches\n> +whose `@{upstream}` is on that remote, plus the remote's default branch,\n> +which is always included so that it is available even before anything\n> +is set up to track it.\n\nSo, I would \"plus the remote's default branch, which is always\nincluded\".\n\n> diff --git a/builtin/fetch.c b/builtin/fetch.c\n> index 7651b41139..c8ce89cf30 100644\n> --- a/builtin/fetch.c\n> +++ b/builtin/fetch.c\n> ...\n> +\t\tif (default_branch_dst) {\n> +\t\t\tstruct refspec_item head_item = { .force = 1 };\n> +\n> +\t\t\thead_item.src = xstrdup(\"HEAD\");\n> +\t\t\thead_item.dst = default_branch_dst;\n> +\t\t\tget_fetch_map(remote_refs, &head_item, &tail, 1);\n> +\t\t\tfree(head_item.src);\n\nThis adds a fetch map entry with rm->name = \"HEAD\" and\nrm->peer_ref->name = \"refs/remotes/second/main\".  Because \"HEAD\"\ndoes not begin with \"refs/heads/\", the report after fetch says\n\n\t* [new ref]    HEAD  -> second/main\n\ninstead the usual\n\n\t* [new branch] main  -> second/main\n\ndoesn't it?  If we do not force-include HEAD, the code would be\nsimplar and we won't have to worry about this.\n\nThanks.\n"},{"id":"553564","messageId":"d48a7004e409952cd1be104840a83168e5e72659.1790673598.git.gitgitgadget@gmail.com","threadId":"66353","inReplyTo":"pull.2412.v4.git.git.1790673598.gitgitgadget@gmail.com","subject":"[PATCH v4 1/4] fetch: add remote.<name>.refmap","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-29T09:19:55Z","receivedAt":"2026-09-29T09:20:03Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nAdd a per-remote config variable, remote.<name>.refmap, that provides\nthe default value for --refmap the same way remote.<name>.fetch\nalready provides the default refspecs to fetch. Like --refmap itself,\nit only maps refs that are actually being fetched, so it has nothing\nto do when there is nothing explicit to fetch, on the command line or\nvia remote.<name>.fetch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/config/remote.adoc |  5 +++++\n Documentation/fetch-options.adoc |  3 +++\n builtin/fetch.c                  |  8 +++++---\n remote.c                         | 12 +++++++++++-\n remote.h                         |  6 ++++++\n t/t5510-fetch.sh                 | 17 +++++++++++++++++\n 6 files changed, 47 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc\nindex 3a20d0f752..103eada406 100644\n--- a/Documentation/config/remote.adoc\n+++ b/Documentation/config/remote.adoc\n@@ -33,6 +33,11 @@ remote.<name>.fetch::\n \tThe default set of \"refspec\" for linkgit:git-fetch[1]. See\n \tlinkgit:git-fetch[1].\n \n+remote.<name>.refmap::\n+\tThe default value of the `--refmap` option for linkgit:git-fetch[1].\n+\tUsed to map remote refs being fetched to remote-tracking refs to\n+\tstore. See the `--refmap` entry in linkgit:git-fetch[1].\n+\n remote.<name>.push::\n \tThe default set of \"refspec\" for linkgit:git-push[1]. See\n \tlinkgit:git-push[1].\ndiff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc\nindex 035f780e58..538914bc6e 100644\n--- a/Documentation/fetch-options.adoc\n+++ b/Documentation/fetch-options.adoc\n@@ -244,6 +244,9 @@ endif::git-pull[]\n \trefspecs and rely entirely on the refspecs supplied as\n \tcommand-line arguments. See section on \"Configured Remote-tracking\n \tBranches\" for details.\n++\n+`remote.<name>.refmap` provides the default value for this option, the\n+same way `remote.<name>.fetch` provides the default refspecs to fetch.\n \n `-t`::\n `--tags`::\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 533fdfe7d8..7651b41139 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -509,6 +509,8 @@ static struct ref *get_ref_map(struct remote *remote,\n \tstruct ref *rm;\n \tstruct ref *ref_map = NULL;\n \tstruct ref **tail = &ref_map;\n+\tstruct refspec *effective_refmap =\n+\t\trefmap.nr ? &refmap : remote ? &remote->refmap : NULL;\n \n \t/* opportunistically-updated references: */\n \tstruct ref *orefs = NULL, **oref_tail = &orefs;\n@@ -552,14 +554,14 @@ static struct ref *get_ref_map(struct remote *remote,\n \t\t * by ref_remove_duplicates() in favor of one of these\n \t\t * opportunistic entries with FETCH_HEAD_IGNORE.\n \t\t */\n-\t\tif (refmap.nr)\n-\t\t\tfetch_refspec = &refmap;\n+\t\tif (effective_refmap && effective_refmap->nr)\n+\t\t\tfetch_refspec = effective_refmap;\n \t\telse\n \t\t\tfetch_refspec = &remote->fetch;\n \n \t\tfor (i = 0; i < fetch_refspec->nr; i++)\n \t\t\tget_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1);\n-\t} else if (refmap.nr) {\n+\t} else if (effective_refmap && effective_refmap->nr) {\n \t\tdie(\"--refmap option is only meaningful with command-line refspec(s)\");\n \t} else {\n \t\t/* Use the defaults */\ndiff --git a/remote.c b/remote.c\nindex fe62068463..017cd9d13e 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -152,6 +152,7 @@ static struct remote *make_remote(struct remote_state *remote_state,\n \tret->name = xstrndup(name, len);\n \trefspec_init_push(&ret->push, the_hash_algo);\n \trefspec_init_fetch(&ret->fetch, the_hash_algo);\n+\trefspec_init_fetch(&ret->refmap, the_hash_algo);\n \tstring_list_init_dup(&ret->server_options);\n \tstring_list_init_dup(&ret->negotiation_restrict);\n \tstring_list_init_dup(&ret->negotiation_include);\n@@ -176,6 +177,7 @@ static void remote_clear(struct remote *remote)\n \n \trefspec_clear(&remote->push);\n \trefspec_clear(&remote->fetch);\n+\trefspec_clear(&remote->refmap);\n \n \tfree((char *)remote->receivepack);\n \tfree((char *)remote->uploadpack);\n@@ -539,6 +541,12 @@ static int handle_config(const char *key, const char *value,\n \t\t\treturn -1;\n \t\trefspec_append(&remote->fetch, v);\n \t\tfree(v);\n+\t} else if (!strcmp(subkey, \"refmap\")) {\n+\t\tchar *v;\n+\t\tif (git_config_string(&v, key, value))\n+\t\t\treturn -1;\n+\t\trefspec_append(&remote->refmap, v);\n+\t\tfree(v);\n \t} else if (!strcmp(subkey, \"receivepack\")) {\n \t\tchar *v;\n \t\tif (git_config_string(&v, key, value))\n@@ -988,7 +996,9 @@ void ref_push_report_free(struct ref_push_report *report)\n \n int remote_find_tracking(struct remote *remote, struct refspec_item *refspec)\n {\n-\treturn refspec_find_match(&remote->fetch, refspec);\n+\tif (remote->fetch.nr)\n+\t\treturn refspec_find_match(&remote->fetch, refspec);\n+\treturn refspec_find_match(&remote->refmap, refspec);\n }\n \n static struct ref *alloc_ref_with_prefix(const char *prefix, size_t prefixlen,\ndiff --git a/remote.h b/remote.h\nindex cca02033b9..ac485a584d 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -90,6 +90,12 @@ struct remote {\n \n \tstruct refspec fetch;\n \n+\t/*\n+\t * How to map refs fetched without an explicit destination into our\n+\t * own namespace, the same as the --refmap command line option.\n+\t */\n+\tstruct refspec refmap;\n+\n \t/*\n \t * The setting for whether to fetch tags (as a separate rule from the\n \t * configured refspecs);\ndiff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh\nindex a8d38d9176..bd853d0a84 100755\n--- a/t/t5510-fetch.sh\n+++ b/t/t5510-fetch.sh\n@@ -927,6 +927,23 @@ test_expect_success 'explicit --refmap option overrides remote.*.fetch' '\n \t)\n '\n \n+test_expect_success 'remote.*.refmap acts like --refmap on the command line' '\n+\ttest_when_finished \"git -C three config --unset remote.origin.refmap\" &&\n+\tgit branch -f side &&\n+\tgit -C three config remote.origin.refmap \\\n+\t\t\"refs/heads/*:refs/remotes/other/*\" &&\n+\t(\n+\t\tcd three &&\n+\t\tgit update-ref refs/remotes/origin/main base-origin-main &&\n+\t\to=$(git rev-parse --verify refs/remotes/origin/main) &&\n+\t\tgit fetch origin main &&\n+\t\tn=$(git rev-parse --verify refs/remotes/origin/main) &&\n+\t\ttest \"$o\" = \"$n\" &&\n+\t\ttest_must_fail git rev-parse --verify refs/remotes/origin/side &&\n+\t\tgit rev-parse --verify refs/remotes/other/main\n+\t)\n+'\n+\n test_expect_success 'explicitly empty --refmap option disables remote.*.fetch' '\n \tgit branch -f side &&\n \t(\n-- \ngitgitgadget\n\n"},{"id":"553566","messageId":"pull.2412.v4.git.git.1790673598.gitgitgadget@gmail.com","threadId":"66353","inReplyTo":"pull.2412.git.git.1789829246437.gitgitgadget@gmail.com","subject":"[PATCH v4 0/4] fetch: avoid fetching every branch of a new remote in a shallow repo","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-29T09:19:54Z","receivedAt":"2026-09-29T09:20:03Z","isPatch":true,"body":"Avoid fetching every branch of a new remote in a shallow repo.\n\nChanges in v4:\n\n * Removed the automatic default-branch fetch. A fresh remote fetches\n   nothing until you track a branch explicitly.\n * Fixed fetch report showing \"new ref HEAD\" instead of \"new branch \"\n * Reworded remote..refmap docs and commit message.\n\nChanges in v3:\n\n * Replace the special \":\"/\"+:\" fetch refspec with remote.<name>.refmap,\n   reusing git's existing --refmap mechanism instead of inventing new\n   refspec syntax.\n * Split the change into 4 commits.\n\nChanges in v2:\n\n * Replaced the opt-in fetch.shallow config entirely with a new special\n   fetch refspec (+:) that git remote add now defaults new remotes to in a\n   shallow repository. The new refspec fetches whichever branches any local\n   branch tracks at that remote, plus the remote's default branch.\n\nHarald Nordgren (4):\n  fetch: add remote.<name>.refmap\n  fetch: infer branches to fetch from a refmap-only remote\n  remote: add \"git remote add --limited-fetch\"\n  remote: default to --limited-fetch in a shallow repository\n\n Documentation/config/remote.adoc |   8 +++\n Documentation/fetch-options.adoc |   5 ++\n Documentation/git-remote.adoc    |  14 +++-\n builtin/fetch.c                  |  60 +++++++++++++---\n builtin/remote.c                 |  31 ++++++--\n remote.c                         |  41 ++++++++++-\n remote.h                         |   9 +++\n t/meson.build                    |   1 +\n t/t5505-remote.sh                |  76 ++++++++++++++++++++\n t/t5510-fetch.sh                 |  17 +++++\n t/t5585-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++\n 11 files changed, 363 insertions(+), 18 deletions(-)\n create mode 100755 t/t5585-fetch-refmap.sh\n\n\nbase-commit: a018953688f1b10bddf91bff8747068f5f4746a4\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2412%2FHaraldNordgren%2Ffetch-shallow-narrow-refspec-v4\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2412/HaraldNordgren/fetch-shallow-narrow-refspec-v4\nPull-Request: https://github.com/git/git/pull/2412\n\nRange-diff vs v3:\n\n 1:  b04c00b974 ! 1:  d48a7004e4 fetch: add remote.<name>.refmap\n     @@ Commit message\n      \n          Add a per-remote config variable, remote.<name>.refmap, that provides\n          the default value for --refmap the same way remote.<name>.fetch\n     -    already provides the default refspecs to fetch. It only takes effect\n     -    when there is something explicit to fetch, on the command line or via\n     -    remote.<name>.fetch, matching how --refmap itself already behaves.\n     +    already provides the default refspecs to fetch. Like --refmap itself,\n     +    it only maps refs that are actually being fetched, so it has nothing\n     +    to do when there is nothing explicit to fetch, on the command line or\n     +    via remote.<name>.fetch.\n      \n          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n      \n     @@ Documentation/config/remote.adoc: remote.<name>.fetch::\n       \n      +remote.<name>.refmap::\n      +\tThe default value of the `--refmap` option for linkgit:git-fetch[1].\n     -+\tOnly takes effect when the fetch names what to fetch explicitly,\n     -+\teither on the command line or via `remote.<name>.fetch`. See the\n     -+\t`--refmap` entry in linkgit:git-fetch[1].\n     ++\tUsed to map remote refs being fetched to remote-tracking refs to\n     ++\tstore. See the `--refmap` entry in linkgit:git-fetch[1].\n      +\n       remote.<name>.push::\n       \tThe default set of \"refspec\" for linkgit:git-push[1]. See\n 2:  4ec508a223 ! 2:  45b26e2bb2 fetch: infer branches to fetch from a refmap-only remote\n     @@ Commit message\n          says where to put fetched refs, not what to fetch.\n      \n          Make that case infer what to fetch: the local branches whose\n     -    @{upstream} is already on that remote, plus the remote's default\n     -    branch, which is always included so it is available even before\n     -    anything is set up to track it. This lets a remote be configured to\n     -    fetch only the branches actually in use, without listing them by\n     -    hand in remote.<name>.fetch, and without needing to touch the\n     -    command line every time.\n     +    @{upstream} is already on that remote. This lets a remote be\n     +    configured to fetch only the branches actually in use, without\n     +    listing them by hand in remote.<name>.fetch, and without needing to\n     +    touch the command line every time.\n      \n          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n      \n       ## Documentation/config/remote.adoc ##\n      @@ Documentation/config/remote.adoc: remote.<name>.fetch::\n     - \n       remote.<name>.refmap::\n       \tThe default value of the `--refmap` option for linkgit:git-fetch[1].\n     --\tOnly takes effect when the fetch names what to fetch explicitly,\n     --\teither on the command line or via `remote.<name>.fetch`. See the\n     --\t`--refmap` entry in linkgit:git-fetch[1].\n     -+\tIf `remote.<name>.fetch` is not set either, a refspec-less fetch\n     -+\tinfers what to fetch from local branches built on this remote,\n     -+\tinstead of fetching every branch it has. See the `--refmap` entry\n     -+\tin linkgit:git-fetch[1].\n     + \tUsed to map remote refs being fetched to remote-tracking refs to\n     +-\tstore. See the `--refmap` entry in linkgit:git-fetch[1].\n     ++\tstore. If `remote.<name>.fetch` is not set either, a refspec-less\n     ++\tfetch infers what to fetch from local branches built on this\n     ++\tremote, instead of fetching every branch it has. See the\n     ++\t`--refmap` entry in linkgit:git-fetch[1].\n       \n       remote.<name>.push::\n       \tThe default set of \"refspec\" for linkgit:git-push[1]. See\n     @@ Documentation/fetch-options.adoc: endif::git-pull[]\n      +When a refmap is active (from `--refmap` or `remote.<name>.refmap`) but\n      +there is nothing to fetch, neither on the command line nor from\n      +`remote.<name>.fetch`, Git infers what to fetch from the local branches\n     -+whose `@{upstream}` is on that remote, plus the remote's default branch,\n     -+which is always included so that it is available even before anything\n     -+is set up to track it.\n     ++whose `@{upstream}` is on that remote.\n       \n       `-t`::\n       `--tags`::\n      \n       ## builtin/fetch.c ##\n     -@@ builtin/fetch.c: static void filter_prefetch_refspec(struct refspec *rs)\n     - static struct ref *get_ref_map(struct remote *remote,\n     - \t\t\t       const struct ref *remote_refs,\n     - \t\t\t       struct refspec *rs,\n     --\t\t\t       int tags, int *autotags)\n     -+\t\t\t       int tags, int *autotags,\n     -+\t\t\t       char **bootstrap_head_branch)\n     - {\n     - \tint i;\n     - \tstruct ref *rm;\n      @@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,\n       \tstruct ref **tail = &ref_map;\n       \tstruct refspec *effective_refmap =\n     @@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,\n      +\t    effective_refmap && effective_refmap->nr) {\n      +\t\tstruct string_list tracked = STRING_LIST_INIT_DUP;\n      +\t\tstruct string_list_item *item;\n     -+\t\tstruct ref *head;\n     -+\t\tchar *default_branch = NULL;\n     -+\t\tchar *default_branch_dst = NULL;\n      +\n      +\t\tbranches_tracking_remote(remote, &tracked);\n      +\t\tfor_each_string_list_item(item, &tracked)\n      +\t\t\trefspec_append(&inferred_rs, item->string);\n     -+\n     -+\t\t/* Always fetch the default branch too, as \"HEAD\". */\n     -+\t\thead = get_remote_ref(remote_refs, \"HEAD\");\n     -+\t\tif (head && head->symref && *head->symref)\n     -+\t\t\tdefault_branch = xstrdup(head->symref);\n     -+\t\tfree_one_ref(head);\n     -+\n     -+\t\tfor (i = 0; default_branch && !default_branch_dst &&\n     -+\t\t\t    !string_list_has_string(&tracked, default_branch) &&\n     -+\t\t\t    i < effective_refmap->nr; i++) {\n     -+\t\t\tstruct refspec_item *map = &effective_refmap->items[i];\n     -+\n     -+\t\t\tif (map->pattern)\n     -+\t\t\t\tmatch_refname_with_pattern(map->src, default_branch,\n     -+\t\t\t\t\t\t\t    map->dst, &default_branch_dst);\n     -+\t\t\telse if (!strcmp(map->src, default_branch))\n     -+\t\t\t\tdefault_branch_dst = xstrdup(map->dst);\n     -+\t\t}\n     -+\n     -+\t\tif (default_branch_dst) {\n     -+\t\t\tstruct refspec_item head_item = { .force = 1 };\n     -+\n     -+\t\t\thead_item.src = xstrdup(\"HEAD\");\n     -+\t\t\thead_item.dst = default_branch_dst;\n     -+\t\t\tget_fetch_map(remote_refs, &head_item, &tail, 1);\n     -+\t\t\tfree(head_item.src);\n     -+\t\t}\n     -+\n     -+\t\tif (default_branch && bootstrap_head_branch) {\n     -+\t\t\tconst char *branch_name = default_branch;\n     -+\t\t\tskip_prefix(branch_name, \"refs/heads/\", &branch_name);\n     -+\t\t\t*bootstrap_head_branch = xstrdup(branch_name);\n     -+\t\t}\n     -+\n     -+\t\tfree(default_branch);\n     -+\t\tfree(default_branch_dst);\n      +\t\tstring_list_clear(&tracked, 0);\n      +\n      +\t\trs = &inferred_rs;\n     @@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,\n       \treturn ref_map;\n       }\n       \n     -@@ builtin/fetch.c: static void warn_set_head(const char *remote, const char *head_name,\n     - }\n     - \n     - static int set_head(const struct ref *remote_refs, struct remote *remote,\n     --\t\t\tint follow_remote_head)\n     -+\t\t\tint follow_remote_head, const char *known_head_branch)\n     - {\n     - \tint result = 0, create_only, baremirror, was_detached;\n     - \tstruct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,\n     - \t\t      b_local_head = STRBUF_INIT;\n     - \tconst char *no_warn_branch = remote->no_warn_branch;\n     - \tchar *head_name = NULL;\n     --\tstruct ref *ref, *matches;\n     -+\tstruct ref *ref, *matches = NULL;\n     - \tstruct ref *fetch_map = NULL, **fetch_map_tail = &fetch_map;\n     - \tstruct refspec_item refspec = {\n     - \t\t.force = 0,\n     -@@ builtin/fetch.c: static int set_head(const struct ref *remote_refs, struct remote *remote,\n     - \tstruct string_list heads = STRING_LIST_INIT_DUP;\n     - \tstruct ref_store *refs = get_main_ref_store(the_repository);\n     - \n     --\tget_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);\n     --\tmatches = guess_remote_head(find_ref_by_name(remote_refs, \"HEAD\"),\n     --\t\t\t\t    fetch_map, REMOTE_GUESS_HEAD_ALL);\n     --\tfor (ref = matches; ref; ref = ref->next) {\n     --\t\tstring_list_append(&heads, strip_refshead(ref->name));\n     --\t}\n     -+\tif (known_head_branch) {\n     -+\t\thead_name = xstrdup(known_head_branch);\n     -+\t} else {\n     -+\t\tget_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);\n     -+\t\tmatches = guess_remote_head(find_ref_by_name(remote_refs, \"HEAD\"),\n     -+\t\t\t\t\t    fetch_map, REMOTE_GUESS_HEAD_ALL);\n     -+\t\tfor (ref = matches; ref; ref = ref->next) {\n     -+\t\t\tstring_list_append(&heads, strip_refshead(ref->name));\n     -+\t\t}\n     - \n     --\tif (!heads.nr)\n     --\t\tresult = 1;\n     --\telse if (heads.nr > 1)\n     --\t\tresult = 1;\n     --\telse\n     --\t\thead_name = xstrdup(heads.items[0].string);\n     -+\t\tif (!heads.nr)\n     -+\t\t\tresult = 1;\n     -+\t\telse if (heads.nr > 1)\n     -+\t\t\tresult = 1;\n     -+\t\telse\n     -+\t\t\thead_name = xstrdup(heads.items[0].string);\n     -+\t}\n     - \n     - \tif (!head_name)\n     - \t\tgoto cleanup;\n     -@@ builtin/fetch.c: static int do_fetch(struct transport *transport,\n     - \tstruct strmap rejected_refs = STRMAP_INIT;\n     - \tint summary_width = 0;\n     - \tint follow_remote_head;\n     -+\tchar *bootstrap_head_branch = NULL;\n     - \n     - \tif (tags == TAGS_DEFAULT) {\n     - \t\tif (transport->remote->fetch_tags == 2)\n      @@ builtin/fetch.c: static int do_fetch(struct transport *transport,\n       \t\trefspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);\n       \t} else {\n     @@ builtin/fetch.c: static int do_fetch(struct transport *transport,\n      +\t\t\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n      +\t\t\t\t\t    item->string);\n      +\t\t\tstring_list_clear(&tracked, 0);\n     -+\t\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes, \"HEAD\");\n      +\t\t} else if (transport->remote->fetch.nr) {\n       \t\t\trefspec_ref_prefixes(&transport->remote->fetch,\n       \t\t\t\t\t     &transport_ls_refs_options.ref_prefixes);\n     @@ builtin/fetch.c: static int do_fetch(struct transport *transport,\n       \t\t\tfor (i = 0; i < branch->merge_nr; i++) {\n       \t\t\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n      @@ builtin/fetch.c: static int do_fetch(struct transport *transport,\n     - \ttransport_ls_refs_options_release(&transport_ls_refs_options);\n       \n       \tref_map = get_ref_map(transport->remote, remote_refs, rs,\n     --\t\t\t      tags, &autotags);\n     -+\t\t\t      tags, &autotags, &bootstrap_head_branch);\n     + \t\t\t      tags, &autotags);\n      +\n       \tif (!update_head_ok)\n       \t\tcheck_not_current_branch(ref_map);\n       \n     -@@ builtin/fetch.c: static int do_fetch(struct transport *transport,\n     - \t\t * Way too many cases where this can go wrong so let's just\n     - \t\t * ignore errors and fail silently for now.\n     - \t\t */\n     --\t\tset_head(remote_refs, transport->remote, follow_remote_head);\n     -+\t\tset_head(remote_refs, transport->remote, follow_remote_head,\n     -+\t\t\t bootstrap_head_branch);\n     - \t}\n     - \n     - cleanup:\n     -+\tfree(bootstrap_head_branch);\n     - \t/*\n     - \t * When using batched updates, we want to commit the non-rejected\n     - \t * updates and also handle the rejections.\n      \n       ## remote.c ##\n      @@ remote.c: int branch_merge_matches(struct branch *branch,\n     @@ t/t5585-fetch-refmap.sh (new)\n      +\n      +When a remote has a refmap configured but no fetch refspec, a\n      +refspec-less fetch infers what to fetch from the local branches whose\n     -+@{upstream} is on that remote, plus the default branch of that remote,\n     -+which is always included so that it is available even before anything\n     -+is set up to track it.\n     ++@{upstream} is on that remote.\n      +'\n      +\n      +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n     @@ t/t5585-fetch-refmap.sh (new)\n      +\t)\n      +'\n      +\n     -+test_expect_success 'fetching the new remote does not need every branch it has' '\n     ++test_expect_success 'a bare fetch needs nothing until a branch is tracked' '\n      +\t(\n      +\t\tcd client &&\n      +\t\tgit fetch upstream &&\n      +\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n     -+\t\tcat >expect <<-\\EOF &&\n     -+\t\trefs/remotes/upstream/HEAD\n     -+\t\trefs/remotes/upstream/main\n     -+\t\tEOF\n     -+\t\ttest_cmp expect actual\n     ++\t\ttest_must_be_empty actual\n      +\t)\n      +'\n      +\n     -+test_expect_success 'set-upstream-to now resolves right after that first fetch' '\n     ++test_expect_success 'an explicit one-time fetch lets a branch be tracked' '\n      +\t(\n      +\t\tcd client &&\n     -+\t\tgit branch --set-upstream-to=upstream &&\n     ++\t\tgit fetch upstream main &&\n     ++\t\tgit branch --set-upstream-to=upstream/main &&\n      +\t\ttest_cmp_config upstream branch.main.remote &&\n      +\t\ttest_cmp_config refs/heads/main branch.main.merge\n      +\t)\n 3:  604e584956 ! 3:  e2f072c254 remote: add \"git remote add --limited-fetch\"\n     @@ Documentation/git-remote.adoc: the `refs/remotes/<name>/` namespace, a refspec t\n      +With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec\n      +that tracks all branches, `remote.<name>.refmap` is set up so that a\n      +refspec-less `git fetch <name>` only fetches branches our local branches\n     -+are built on, plus the remote's default branch. See the `--refmap` entry\n     -+in linkgit:git-fetch[1] for details.\n     ++are built on. See the `--refmap` entry in linkgit:git-fetch[1] for\n     ++details.\n      ++\n       With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set\n       up to point at remote's _<master>_ branch. See also the set-head command.\n 4:  31462e9445 ! 4:  43b9711a2c remote: default to --limited-fetch in a shallow repository\n     @@ Documentation/git-remote.adoc: Add a remote named _<name>_ for the repository at\n      @@ Documentation/git-remote.adoc: With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec\n       that tracks all branches, `remote.<name>.refmap` is set up so that a\n       refspec-less `git fetch <name>` only fetches branches our local branches\n     - are built on, plus the remote's default branch. See the `--refmap` entry\n     --in linkgit:git-fetch[1] for details.\n     -+in linkgit:git-fetch[1] for details. `--no-limited-fetch` explicitly\n     -+disables this, overriding the shallow-repository default described above.\n     + are built on. See the `--refmap` entry in linkgit:git-fetch[1] for\n     +-details.\n     ++details. `--no-limited-fetch` explicitly disables this, overriding the\n     ++shallow-repository default described above.\n       +\n       With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set\n       up to point at remote's _<master>_ branch. See also the set-head command.\n     @@ t/t5505-remote.sh: test_expect_success 'filters are listed by git remote -v only\n      +\t\ttest_cmp_config \"+refs/heads/*:refs/remotes/upstream/*\" \\\n      +\t\t\tremote.upstream.refmap &&\n      +\t\ttest_must_fail git config get remote.upstream.fetch &&\n     ++\t\tgit fetch upstream main &&\n     ++\t\tgit branch --set-upstream-to=upstream/main &&\n     ++\t\ttest_cmp_config upstream branch.main.remote &&\n     ++\t\ttest_cmp_config refs/heads/main branch.main.merge &&\n      +\t\tgit fetch upstream &&\n      +\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n      +\t\tcat >expect <<-\\EOF &&\n      +\t\trefs/remotes/upstream/HEAD\n      +\t\trefs/remotes/upstream/main\n      +\t\tEOF\n     -+\t\ttest_cmp expect actual &&\n     -+\t\tgit branch --set-upstream-to=upstream &&\n     -+\t\ttest_cmp_config upstream branch.main.remote &&\n     -+\t\ttest_cmp_config refs/heads/main branch.main.merge\n     ++\t\ttest_cmp expect actual\n      +\t)\n      +'\n      +\n\n-- \ngitgitgadget\n"},{"id":"553563","messageId":"45b26e2bb292f31eb36d88e2fcf8801eb8313374.1790673598.git.gitgitgadget@gmail.com","threadId":"66353","inReplyTo":"pull.2412.v4.git.git.1790673598.gitgitgadget@gmail.com","subject":"[PATCH v4 2/4] fetch: infer branches to fetch from a refmap-only remote","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-29T09:19:56Z","receivedAt":"2026-09-29T09:20:05Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nConfiguring remote.<name>.refmap without remote.<name>.fetch used to\nmake a refspec-less \"git fetch <name>\" fail with \"--refmap option is\nonly meaningful with command-line refspec(s)\", since a refmap only\nsays where to put fetched refs, not what to fetch.\n\nMake that case infer what to fetch: the local branches whose\n@{upstream} is already on that remote. This lets a remote be\nconfigured to fetch only the branches actually in use, without\nlisting them by hand in remote.<name>.fetch, and without needing to\ntouch the command line every time.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/config/remote.adoc |   5 +-\n Documentation/fetch-options.adoc |   6 +-\n builtin/fetch.c                  |  52 ++++++++++++--\n remote.c                         |  29 ++++++++\n remote.h                         |   3 +\n t/meson.build                    |   1 +\n t/t5585-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++\n 7 files changed, 205 insertions(+), 10 deletions(-)\n create mode 100755 t/t5585-fetch-refmap.sh\n\ndiff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc\nindex 103eada406..7c52e443a5 100644\n--- a/Documentation/config/remote.adoc\n+++ b/Documentation/config/remote.adoc\n@@ -36,7 +36,10 @@ remote.<name>.fetch::\n remote.<name>.refmap::\n \tThe default value of the `--refmap` option for linkgit:git-fetch[1].\n \tUsed to map remote refs being fetched to remote-tracking refs to\n-\tstore. See the `--refmap` entry in linkgit:git-fetch[1].\n+\tstore. If `remote.<name>.fetch` is not set either, a refspec-less\n+\tfetch infers what to fetch from local branches built on this\n+\tremote, instead of fetching every branch it has. See the\n+\t`--refmap` entry in linkgit:git-fetch[1].\n \n remote.<name>.push::\n \tThe default set of \"refspec\" for linkgit:git-push[1]. See\ndiff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc\nindex 538914bc6e..315daf97d7 100644\n--- a/Documentation/fetch-options.adoc\n+++ b/Documentation/fetch-options.adoc\n@@ -245,8 +245,10 @@ endif::git-pull[]\n \tcommand-line arguments. See section on \"Configured Remote-tracking\n \tBranches\" for details.\n +\n-`remote.<name>.refmap` provides the default value for this option, the\n-same way `remote.<name>.fetch` provides the default refspecs to fetch.\n+When a refmap is active (from `--refmap` or `remote.<name>.refmap`) but\n+there is nothing to fetch, neither on the command line nor from\n+`remote.<name>.fetch`, Git infers what to fetch from the local branches\n+whose `@{upstream}` is on that remote.\n \n `-t`::\n `--tags`::\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 7651b41139..7ef2776f17 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -511,6 +511,8 @@ static struct ref *get_ref_map(struct remote *remote,\n \tstruct ref **tail = &ref_map;\n \tstruct refspec *effective_refmap =\n \t\trefmap.nr ? &refmap : remote ? &remote->refmap : NULL;\n+\tstruct refspec inferred_rs;\n+\tint inferred_branches = 0;\n \n \t/* opportunistically-updated references: */\n \tstruct ref *orefs = NULL, **oref_tail = &orefs;\n@@ -518,15 +520,32 @@ static struct ref *get_ref_map(struct remote *remote,\n \tstruct hashmap existing_refs;\n \tint existing_refs_populated = 0;\n \n+\trefspec_init_fetch(&inferred_rs, the_hash_algo);\n+\n \tfilter_prefetch_refspec(rs);\n \tif (remote)\n \t\tfilter_prefetch_refspec(&remote->fetch);\n \n+\tif (!rs->nr && remote && !remote->fetch.nr &&\n+\t    effective_refmap && effective_refmap->nr) {\n+\t\tstruct string_list tracked = STRING_LIST_INIT_DUP;\n+\t\tstruct string_list_item *item;\n+\n+\t\tbranches_tracking_remote(remote, &tracked);\n+\t\tfor_each_string_list_item(item, &tracked)\n+\t\t\trefspec_append(&inferred_rs, item->string);\n+\t\tstring_list_clear(&tracked, 0);\n+\n+\t\trs = &inferred_rs;\n+\t\tinferred_branches = 1;\n+\t}\n+\n \tif (rs->nr) {\n \t\tstruct refspec *fetch_refspec;\n \n \t\tfor (i = 0; i < rs->nr; i++) {\n-\t\t\tget_fetch_map(remote_refs, &rs->items[i], &tail, 0);\n+\t\t\tget_fetch_map(remote_refs, &rs->items[i], &tail,\n+\t\t\t\t      inferred_branches);\n \t\t\tif (rs->items[i].dst && rs->items[i].dst[0])\n \t\t\t\t*autotags = 1;\n \t\t}\n@@ -561,6 +580,8 @@ static struct ref *get_ref_map(struct remote *remote,\n \n \t\tfor (i = 0; i < fetch_refspec->nr; i++)\n \t\t\tget_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1);\n+\t} else if (inferred_branches) {\n+\t\t/* Already fully handled above. */\n \t} else if (effective_refmap && effective_refmap->nr) {\n \t\tdie(\"--refmap option is only meaningful with command-line refspec(s)\");\n \t} else {\n@@ -656,6 +677,7 @@ static struct ref *get_ref_map(struct remote *remote,\n \tif (existing_refs_populated)\n \t\thashmap_clear_and_free(&existing_refs, struct refname_hash_entry, ent);\n \n+\trefspec_clear(&inferred_rs);\n \treturn ref_map;\n }\n \n@@ -1960,15 +1982,30 @@ static int do_fetch(struct transport *transport,\n \t\trefspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);\n \t} else {\n \t\tstruct branch *branch = branch_get(NULL);\n-\n-\t\tif (transport->remote->fetch.nr) {\n+\t\tint tracks_this_remote = branch && branch_has_merge_config(branch) &&\n+\t\t\t!strcmp(branch->remote_name, transport->remote->name);\n+\t\tstruct refspec *effective_refmap = refmap.nr ? &refmap :\n+\t\t\t&transport->remote->refmap;\n+\t\tint inferred_branches = !transport->remote->fetch.nr &&\n+\t\t\teffective_refmap->nr;\n+\n+\t\tif (inferred_branches) {\n+\t\t\tstruct string_list tracked = STRING_LIST_INIT_DUP;\n+\t\t\tstruct string_list_item *item;\n+\n+\t\t\tbranches_tracking_remote(transport->remote, &tracked);\n+\t\t\tfor_each_string_list_item(item, &tracked)\n+\t\t\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n+\t\t\t\t\t    item->string);\n+\t\t\tstring_list_clear(&tracked, 0);\n+\t\t} else if (transport->remote->fetch.nr) {\n \t\t\trefspec_ref_prefixes(&transport->remote->fetch,\n \t\t\t\t\t     &transport_ls_refs_options.ref_prefixes);\n-\t\t\tif (follow_remote_head != FOLLOW_REMOTE_NEVER)\n-\t\t\t\tdo_set_head = 1;\n \t\t}\n-\t\tif (branch && branch_has_merge_config(branch) &&\n-\t\t    !strcmp(branch->remote_name, transport->remote->name)) {\n+\t\tif ((transport->remote->fetch.nr || inferred_branches) &&\n+\t\t    follow_remote_head != FOLLOW_REMOTE_NEVER)\n+\t\t\tdo_set_head = 1;\n+\t\tif (tracks_this_remote) {\n \t\t\tint i;\n \t\t\tfor (i = 0; i < branch->merge_nr; i++) {\n \t\t\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n@@ -2009,6 +2046,7 @@ static int do_fetch(struct transport *transport,\n \n \tref_map = get_ref_map(transport->remote, remote_refs, rs,\n \t\t\t      tags, &autotags);\n+\n \tif (!update_head_ok)\n \t\tcheck_not_current_branch(ref_map);\n \ndiff --git a/remote.c b/remote.c\nindex 017cd9d13e..a5deb9b929 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -1884,6 +1884,35 @@ int branch_merge_matches(struct branch *branch,\n \treturn refname_match(branch->merge[i]->src, refname);\n }\n \n+struct branches_tracking_remote_cb_data {\n+\tstruct remote *remote;\n+\tstruct string_list *tracked;\n+};\n+\n+static int add_if_tracking_remote(const struct reference *ref, void *cb_data)\n+{\n+\tstruct branches_tracking_remote_cb_data *data = cb_data;\n+\tstruct branch *branch;\n+\n+\tbranch = branch_get(ref->name);\n+\tif (!branch_has_merge_config(branch) ||\n+\t    strcmp(branch->remote_name, data->remote->name))\n+\t\treturn 0;\n+\n+\tfor (int i = 0; i < branch->merge_nr; i++)\n+\t\tstring_list_insert(data->tracked, branch->merge[i]->src);\n+\n+\treturn 0;\n+}\n+\n+void branches_tracking_remote(struct remote *remote, struct string_list *tracked)\n+{\n+\tstruct branches_tracking_remote_cb_data data = { remote, tracked };\n+\n+\trefs_for_each_branch_ref(get_main_ref_store(the_repository),\n+\t\t\t\t  add_if_tracking_remote, &data);\n+}\n+\n __attribute__((format (printf,2,3)))\n static char *error_buf(struct strbuf *err, const char *fmt, ...)\n {\ndiff --git a/remote.h b/remote.h\nindex ac485a584d..da6c3ef53a 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -359,6 +359,9 @@ int branch_has_merge_config(struct branch *branch);\n \n int branch_merge_matches(struct branch *, int n, const char *);\n \n+/* fills tracked with the refname of every local branch's upstream on remote */\n+void branches_tracking_remote(struct remote *remote, struct string_list *tracked);\n+\n /* list of the remote in a group as configured */\n struct remote_group_data {\n \tconst char *name;\ndiff --git a/t/meson.build b/t/meson.build\nindex 3ca7b27104..ab83d5409c 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -728,6 +728,7 @@ integration_tests = [\n   't5582-fetch-negative-refspec.sh',\n   't5583-push-branches.sh',\n   't5584-http-429-retry.sh',\n+  't5585-fetch-refmap.sh',\n   't5600-clone-fail-cleanup.sh',\n   't5601-clone.sh',\n   't5602-clone-remote-exec.sh',\ndiff --git a/t/t5585-fetch-refmap.sh b/t/t5585-fetch-refmap.sh\nnew file mode 100755\nindex 0000000000..b81fc48cbe\n--- /dev/null\n+++ b/t/t5585-fetch-refmap.sh\n@@ -0,0 +1,119 @@\n+#!/bin/sh\n+\n+test_description='\"git fetch\" with a remote.<name>.refmap but no remote.<name>.fetch\n+\n+When a remote has a refmap configured but no fetch refspec, a\n+refspec-less fetch infers what to fetch from the local branches whose\n+@{upstream} is on that remote.\n+'\n+\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup' '\n+\ttest_commit main-1 &&\n+\ttest_commit main-2 &&\n+\tgit checkout -b side main-1 &&\n+\ttest_commit side-1 &&\n+\tgit checkout -b next main-1 &&\n+\ttest_commit next-1 &&\n+\tgit checkout main\n+'\n+\n+test_expect_success 'clone shallow and single-branch, then add a second remote' '\n+\tgit clone --no-local --depth=1 --branch main --single-branch . client &&\n+\t(\n+\t\tcd client &&\n+\t\tgit remote add upstream .. &&\n+\t\ttest_might_fail git config unset remote.upstream.fetch &&\n+\t\tgit config remote.upstream.refmap \\\n+\t\t\t\"+refs/heads/*:refs/remotes/upstream/*\"\n+\t)\n+'\n+\n+test_expect_success 'a bare fetch needs nothing until a branch is tracked' '\n+\t(\n+\t\tcd client &&\n+\t\tgit fetch upstream &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n+\t\ttest_must_be_empty actual\n+\t)\n+'\n+\n+test_expect_success 'an explicit one-time fetch lets a branch be tracked' '\n+\t(\n+\t\tcd client &&\n+\t\tgit fetch upstream main &&\n+\t\tgit branch --set-upstream-to=upstream/main &&\n+\t\ttest_cmp_config upstream branch.main.remote &&\n+\t\ttest_cmp_config refs/heads/main branch.main.merge\n+\t)\n+'\n+\n+test_expect_success 'a branch checked out from a one-time fetch is kept updated by later plain fetches' '\n+\t(\n+\t\tcd client &&\n+\t\tgit fetch upstream side:refs/remotes/upstream/side &&\n+\t\tgit branch side-topic upstream/side\n+\t) &&\n+\tgit checkout side &&\n+\ttest_commit side-2 &&\n+\tgit checkout main &&\n+\t(\n+\t\tcd client &&\n+\t\tgit fetch upstream &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\trefs/remotes/upstream/side\n+\t\tEOF\n+\t\ttest_cmp expect actual &&\n+\t\tgit rev-parse refs/remotes/upstream/side >actual-oid &&\n+\t\tgit -C .. rev-parse side >expect-oid &&\n+\t\ttest_cmp expect-oid actual-oid\n+\t)\n+'\n+\n+test_expect_success 'a second branch tracking the same upstream branch does not fetch it twice' '\n+\t(\n+\t\tcd client &&\n+\t\tgit branch side-topic-2 upstream/side &&\n+\t\tgit fetch upstream &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\trefs/remotes/upstream/side\n+\t\tEOF\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'a branch tracking a different remote is not fetched from upstream' '\n+\t(\n+\t\tcd client &&\n+\t\tgit remote add other .. &&\n+\t\tgit fetch other next:refs/remotes/other/next &&\n+\t\tgit branch next-topic other/next &&\n+\t\tgit fetch upstream &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\trefs/remotes/upstream/side\n+\t\tEOF\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'git remote show does not choke on a refmap-only remote' '\n+\t(\n+\t\tcd client &&\n+\t\tgit remote show upstream\n+\t)\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"553567","messageId":"e2f072c254d00473c11c1ac83cc0631acffbaecb.1790673598.git.gitgitgadget@gmail.com","threadId":"66353","inReplyTo":"pull.2412.v4.git.git.1790673598.gitgitgadget@gmail.com","subject":"[PATCH v4 3/4] remote: add \"git remote add --limited-fetch\"","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-29T09:19:57Z","receivedAt":"2026-09-29T09:20:05Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nA remote added the ordinary way tracks every branch it has, via a\nwildcard remote.<name>.fetch refspec. That is wasteful for a remote\nwhose history is only worth following for the branches actually in\nuse locally, and it can make \"git fetch\" negotiate history for\nbranches nobody asked for.\n\nGive \"git remote add\" a --limited-fetch option that sets up\nremote.<name>.refmap instead of remote.<name>.fetch, so that\n\"git fetch <name>\" only fetches the branches already tracked, plus\nthe remote's default branch, as described in the previous commit.\nIt is rejected together with -t/--track or --mirror, since those\nalready say explicitly what to fetch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/git-remote.adoc |  8 +++++++-\n builtin/remote.c              | 28 ++++++++++++++++++++++------\n t/t5505-remote.sh             | 16 ++++++++++++++++\n 3 files changed, 45 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/git-remote.adoc b/Documentation/git-remote.adoc\nindex eaae30aa88..4255f8b3e6 100644\n--- a/Documentation/git-remote.adoc\n+++ b/Documentation/git-remote.adoc\n@@ -10,7 +10,7 @@ SYNOPSIS\n --------\n [synopsis]\n git remote [-v | --verbose]\n-git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] <name> <URL>\n+git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] [--[no-]limited-fetch] <name> <URL>\n git remote rename [--[no-]progress] <old> <new>\n git remote remove <name>\n git remote set-head <name> (-a | --auto | -d | --delete | <branch>)\n@@ -70,6 +70,12 @@ the `refs/remotes/<name>/` namespace, a refspec to track only _<branch>_\n is created.  You can give more than one `-t <branch>` to track\n multiple branches without grabbing all branches.\n +\n+With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec\n+that tracks all branches, `remote.<name>.refmap` is set up so that a\n+refspec-less `git fetch <name>` only fetches branches our local branches\n+are built on. See the `--refmap` entry in linkgit:git-fetch[1] for\n+details.\n++\n With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set\n up to point at remote's _<master>_ branch. See also the set-head command.\n +\ndiff --git a/builtin/remote.c b/builtin/remote.c\nindex de989ea3ba..f036dd5d6f 100644\n--- a/builtin/remote.c\n+++ b/builtin/remote.c\n@@ -179,6 +179,7 @@ static int add(int argc, const char **argv, const char *prefix,\n {\n \tint fetch = 0, fetch_tags = TAGS_DEFAULT;\n \tunsigned mirror = MIRROR_NONE;\n+\tint limited_fetch = -1; /* unspecified */\n \tstruct string_list track = STRING_LIST_INIT_NODUP;\n \tconst char *master = NULL;\n \tstruct remote *remote;\n@@ -198,6 +199,8 @@ static int add(int argc, const char **argv, const char *prefix,\n \t\tOPT_CALLBACK_F(0, \"mirror\", &mirror, \"(push|fetch)\",\n \t\t\tN_(\"set up remote as a mirror to push to or fetch from\"),\n \t\t\tPARSE_OPT_OPTARG | PARSE_OPT_COMP_ARG, parse_mirror_opt),\n+\t\tOPT_BOOL(0, \"limited-fetch\", &limited_fetch,\n+\t\t\tN_(\"fetch only the branches we build on, instead of every branch\")),\n \t\tOPT_END()\n \t};\n \n@@ -211,6 +214,10 @@ static int add(int argc, const char **argv, const char *prefix,\n \t\tdie(_(\"specifying a master branch makes no sense with --mirror\"));\n \tif (mirror && !(mirror & MIRROR_FETCH) && track.nr)\n \t\tdie(_(\"specifying branches to track makes sense only with fetch mirrors\"));\n+\tif (limited_fetch == 1 && track.nr)\n+\t\tdie(_(\"--limited-fetch does not make sense with -t/--track\"));\n+\tif (limited_fetch == 1 && mirror)\n+\t\tdie(_(\"--limited-fetch does not make sense with --mirror\"));\n \n \tname = argv[0];\n \turl = argv[1];\n@@ -230,13 +237,22 @@ static int add(int argc, const char **argv, const char *prefix,\n \trepo_config_set(the_repository, buf.buf, url);\n \n \tif (!mirror || mirror & MIRROR_FETCH) {\n+\t\tint use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&\n+\t\t\tlimited_fetch == 1;\n+\n \t\tstrbuf_reset(&buf);\n-\t\tstrbuf_addf(&buf, \"remote.%s.fetch\", name);\n-\t\tif (track.nr == 0)\n-\t\t\tstring_list_append(&track, \"*\");\n-\t\tfor (size_t i = 0; i < track.nr; i++) {\n-\t\t\tadd_branch(buf.buf, track.items[i].string,\n-\t\t\t\t   name, mirror, &buf2);\n+\t\tif (use_limited_fetch) {\n+\t\t\tstrbuf_addf(&buf, \"remote.%s.refmap\", name);\n+\t\t\tstrbuf_reset(&buf2);\n+\t\t\tstrbuf_addf(&buf2, \"+refs/heads/*:refs/remotes/%s/*\", name);\n+\t\t\trepo_config_set(the_repository, buf.buf, buf2.buf);\n+\t\t} else {\n+\t\t\tstrbuf_addf(&buf, \"remote.%s.fetch\", name);\n+\t\t\tif (track.nr == 0)\n+\t\t\t\tstring_list_append(&track, \"*\");\n+\t\t\tfor (size_t i = 0; i < track.nr; i++)\n+\t\t\t\tadd_branch(buf.buf, track.items[i].string,\n+\t\t\t\t\t   name, mirror, &buf2);\n \t\t}\n \t}\n \ndiff --git a/t/t5505-remote.sh b/t/t5505-remote.sh\nindex 5fbfcb0848..0168d5abfe 100755\n--- a/t/t5505-remote.sh\n+++ b/t/t5505-remote.sh\n@@ -137,6 +137,22 @@ test_expect_success 'filters are listed by git remote -v only' '\n \ttest_grep ! \"\\[blob:none\\]\" out\n '\n \n+test_expect_success '--limited-fetch works in a full repository too' '\n+\ttest_when_finished \"rm -rf full-add\" &&\n+\tgit clone --no-local one full-add &&\n+\t(\n+\t\tcd full-add &&\n+\t\tgit remote add --limited-fetch upstream ../two &&\n+\t\ttest_cmp_config \"+refs/heads/*:refs/remotes/upstream/*\" \\\n+\t\t\tremote.upstream.refmap &&\n+\t\ttest_must_fail git config get remote.upstream.fetch\n+\t)\n+'\n+\n+test_expect_success '--limited-fetch conflicts with -t' '\n+\ttest_must_fail git remote add --limited-fetch -t main upstream ../two\n+'\n+\n test_expect_success 'check remote-tracking' '\n \t(\n \t\tcd test &&\n-- \ngitgitgadget\n\n"},{"id":"553565","messageId":"43b9711a2cd100e4987393acd588de2823b0acb3.1790673598.git.gitgitgadget@gmail.com","threadId":"66353","inReplyTo":"pull.2412.v4.git.git.1790673598.gitgitgadget@gmail.com","subject":"[PATCH v4 4/4] remote: default to --limited-fetch in a shallow repository","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-09-29T09:19:58Z","receivedAt":"2026-09-29T09:20:08Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nAdding a second remote to a shallow, single-branch clone used to\nstill fetch every branch that remote has, since \"git remote add\"\nalways set up a wildcard remote.<name>.fetch refspec regardless of\nhow shallow the repository already was. That defeats the purpose of\nhaving cloned shallow and single-branch in the first place, and can\nmake a plain \"git fetch\" on that remote hang or take a very long\ntime on a repository with many branches.\n\nTurn --limited-fetch on by default when the repository is already\nshallow and neither -t/--track nor --mirror was given, so that\nadding a remote there does not by itself commit to following every\nbranch it has. --no-limited-fetch keeps the previous behavior for\nwhoever wants it.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/git-remote.adoc |  8 ++++-\n builtin/remote.c              |  5 ++-\n t/t5505-remote.sh             | 60 +++++++++++++++++++++++++++++++++++\n 3 files changed, 71 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/git-remote.adoc b/Documentation/git-remote.adoc\nindex 4255f8b3e6..80f8a4a183 100644\n--- a/Documentation/git-remote.adoc\n+++ b/Documentation/git-remote.adoc\n@@ -52,6 +52,11 @@ Add a remote named _<name>_ for the repository at\n _<URL>_.  The command `git fetch <name>` can then be used to create and\n update remote-tracking branches `<name>/<branch>`.\n +\n+If the repository is already a shallow repository (see linkgit:git-clone[1]\n+`--depth`) and neither `-t`, `--mirror` nor `--no-limited-fetch` is given,\n+`--limited-fetch` is turned on by default, so that `git fetch <name>` does\n+not need to negotiate history for every branch the remote has.\n++\n With `-f` option, `git fetch <name>` is run immediately after\n the remote information is set up.\n +\n@@ -74,7 +79,8 @@ With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec\n that tracks all branches, `remote.<name>.refmap` is set up so that a\n refspec-less `git fetch <name>` only fetches branches our local branches\n are built on. See the `--refmap` entry in linkgit:git-fetch[1] for\n-details.\n+details. `--no-limited-fetch` explicitly disables this, overriding the\n+shallow-repository default described above.\n +\n With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set\n up to point at remote's _<master>_ branch. See also the set-head command.\ndiff --git a/builtin/remote.c b/builtin/remote.c\nindex f036dd5d6f..0cf1126976 100644\n--- a/builtin/remote.c\n+++ b/builtin/remote.c\n@@ -16,6 +16,7 @@\n #include \"rebase.h\"\n #include \"refs.h\"\n #include \"refspec.h\"\n+#include \"shallow.h\"\n #include \"odb.h\"\n #include \"strvec.h\"\n #include \"commit-reach.h\"\n@@ -238,7 +239,9 @@ static int add(int argc, const char **argv, const char *prefix,\n \n \tif (!mirror || mirror & MIRROR_FETCH) {\n \t\tint use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&\n-\t\t\tlimited_fetch == 1;\n+\t\t\t(limited_fetch == 1 ||\n+\t\t\t (limited_fetch == -1 &&\n+\t\t\t  is_repository_shallow(the_repository)));\n \n \t\tstrbuf_reset(&buf);\n \t\tif (use_limited_fetch) {\ndiff --git a/t/t5505-remote.sh b/t/t5505-remote.sh\nindex 0168d5abfe..f3b5905e9e 100755\n--- a/t/t5505-remote.sh\n+++ b/t/t5505-remote.sh\n@@ -137,6 +137,66 @@ test_expect_success 'filters are listed by git remote -v only' '\n \ttest_grep ! \"\\[blob:none\\]\" out\n '\n \n+test_expect_success 'add remote -t keeps an explicit refspec in a shallow repository' '\n+\ttest_when_finished \"rm -rf shallow-add\" &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\tone shallow-add &&\n+\t(\n+\t\tcd shallow-add &&\n+\t\tgit remote add -t main upstream ../two &&\n+\t\ttest_cmp_config \"+refs/heads/main:refs/remotes/upstream/main\" \\\n+\t\t\tremote.upstream.fetch\n+\t)\n+'\n+\n+test_expect_success 'add remote keeps the wildcard refspec in a full repository' '\n+\ttest_when_finished \"rm -rf full-add\" &&\n+\tgit clone --no-local one full-add &&\n+\t(\n+\t\tcd full-add &&\n+\t\tgit remote add upstream ../two &&\n+\t\ttest_cmp_config \"+refs/heads/*:refs/remotes/upstream/*\" \\\n+\t\t\tremote.upstream.fetch\n+\t)\n+'\n+\n+test_expect_success 'a remote added in a shallow repository defaults to --limited-fetch' '\n+\ttest_when_finished \"rm -rf shallow-add\" &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\tone shallow-add &&\n+\t(\n+\t\tcd shallow-add &&\n+\t\tgit remote add upstream ../two &&\n+\t\ttest_cmp_config \"+refs/heads/*:refs/remotes/upstream/*\" \\\n+\t\t\tremote.upstream.refmap &&\n+\t\ttest_must_fail git config get remote.upstream.fetch &&\n+\t\tgit fetch upstream main &&\n+\t\tgit branch --set-upstream-to=upstream/main &&\n+\t\ttest_cmp_config upstream branch.main.remote &&\n+\t\ttest_cmp_config refs/heads/main branch.main.merge &&\n+\t\tgit fetch upstream &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\tEOF\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '--no-limited-fetch overrides the shallow-repository default' '\n+\ttest_when_finished \"rm -rf shallow-add\" &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\tone shallow-add &&\n+\t(\n+\t\tcd shallow-add &&\n+\t\tgit remote add --no-limited-fetch upstream ../two &&\n+\t\ttest_cmp_config \"+refs/heads/*:refs/remotes/upstream/*\" \\\n+\t\t\tremote.upstream.fetch &&\n+\t\ttest_must_fail git config get remote.upstream.refmap\n+\t)\n+'\n+\n test_expect_success '--limited-fetch works in a full repository too' '\n \ttest_when_finished \"rm -rf full-add\" &&\n \tgit clone --no-local one full-add &&\n-- \ngitgitgadget\n"},{"id":"553568","messageId":"CAHwyqnX2O1e+f6qss5+AL3f-Q-PHQmZjbayiph2cQiQGDjSAkA@mail.gmail.com","threadId":"66353","inReplyTo":"45b26e2bb292f31eb36d88e2fcf8801eb8313374.1790673598.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 2/4] fetch: infer branches to fetch from a refmap-only remote","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-09-29T09:27:29Z","receivedAt":"2026-09-29T09:28:08Z","isPatch":true,"body":"> +test_expect_success 'a bare fetch needs nothing until a branch is tracked' '\n> +       (\n> +               cd client &&\n> +               git fetch upstream &&\n> +               git for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n> +               test_must_be_empty actual\n> +       )\n> +'\n\nI still don't think it makes sense that bare 'git fetch upstream'\ndoesn't fetch the HEAD branch to then allow me to do\n\n    git branch --set-upstream-to=upstream\n\nBut maybe that's for another topic.\n\n\nHarald\n"},{"id":"553634","messageId":"xmqqqzibg884.fsf@gitster.g","threadId":"66353","inReplyTo":"pull.2412.v4.git.git.1790673598.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 0/4] fetch: avoid fetching every branch of a new remote in a shallow repo","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-29T19:36:59Z","receivedAt":"2026-09-29T19:37:02Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> Avoid fetching every branch of a new remote in a shallow repo.\n>\n> Changes in v4:\n>\n>  * Removed the automatic default-branch fetch. A fresh remote fetches\n>    nothing until you track a branch explicitly.\n>  * Fixed fetch report showing \"new ref HEAD\" instead of \"new branch \"\n>  * Reworded remote..refmap docs and commit message.\n\nWill replace.\n\nAs my eyes have been contaminated by and biased for this topic, I'll\nspend my time on other topics first to let my eyes and brain\n\"forget\" about it before I revisit these patches.  I'd really\nwelcome reviews with fresh eyes to happen in the meantime.\n\nThanks.\n"},{"id":"553639","messageId":"xmqqzewzerrh.fsf@gitster.g","threadId":"66353","inReplyTo":"45b26e2bb292f31eb36d88e2fcf8801eb8313374.1790673598.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v4 2/4] fetch: infer branches to fetch from a refmap-only remote","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-09-29T20:17:54Z","receivedAt":"2026-09-29T20:17:56Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n>  t/t5585-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++\n\nPlease renumber.  This number is taken by another topic already in\nflight, I think.\n"},{"id":"553890","messageId":"pull.2412.v5.git.git.1790925198.gitgitgadget@gmail.com","threadId":"66353","inReplyTo":"pull.2412.git.git.1789829246437.gitgitgadget@gmail.com","subject":"[PATCH v5 0/4] fetch: avoid fetching every branch of a new remote in a shallow repo","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-02T07:13:14Z","receivedAt":"2026-10-02T07:13:22Z","isPatch":true,"body":"Avoid fetching every branch of a new remote in a shallow repo.\n\nChanges in v5:\n\n * Renumber test file from t5585 to t5586.\n\nChanges in v4:\n\n * Removed the automatic default-branch fetch. A fresh remote fetches\n   nothing until you track a branch explicitly.\n * Fixed fetch report showing \"new ref HEAD\" instead of \"new branch \"\n * Reworded remote..refmap docs and commit message.\n\nChanges in v3:\n\n * Replace the special \":\"/\"+:\" fetch refspec with remote.<name>.refmap,\n   reusing git's existing --refmap mechanism instead of inventing new\n   refspec syntax.\n * Split the change into 4 commits.\n\nChanges in v2:\n\n * Replaced the opt-in fetch.shallow config entirely with a new special\n   fetch refspec (+:) that git remote add now defaults new remotes to in a\n   shallow repository. The new refspec fetches whichever branches any local\n   branch tracks at that remote, plus the remote's default branch.\n\nHarald Nordgren (4):\n  fetch: add remote.<name>.refmap\n  fetch: infer branches to fetch from a refmap-only remote\n  remote: add \"git remote add --limited-fetch\"\n  remote: default to --limited-fetch in a shallow repository\n\n Documentation/config/remote.adoc |   8 +++\n Documentation/fetch-options.adoc |   5 ++\n Documentation/git-remote.adoc    |  14 +++-\n builtin/fetch.c                  |  60 +++++++++++++---\n builtin/remote.c                 |  31 ++++++--\n remote.c                         |  41 ++++++++++-\n remote.h                         |   9 +++\n t/meson.build                    |   1 +\n t/t5505-remote.sh                |  76 ++++++++++++++++++++\n t/t5510-fetch.sh                 |  17 +++++\n t/t5586-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++\n 11 files changed, 363 insertions(+), 18 deletions(-)\n create mode 100755 t/t5586-fetch-refmap.sh\n\n\nbase-commit: a018953688f1b10bddf91bff8747068f5f4746a4\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2412%2FHaraldNordgren%2Ffetch-shallow-narrow-refspec-v5\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2412/HaraldNordgren/fetch-shallow-narrow-refspec-v5\nPull-Request: https://github.com/git/git/pull/2412\n\nRange-diff vs v4:\n\n 1:  d48a7004e4 = 1:  d48a7004e4 fetch: add remote.<name>.refmap\n 2:  45b26e2bb2 ! 2:  fecdbc43b8 fetch: infer branches to fetch from a refmap-only remote\n     @@ t/meson.build: integration_tests = [\n         't5582-fetch-negative-refspec.sh',\n         't5583-push-branches.sh',\n         't5584-http-429-retry.sh',\n     -+  't5585-fetch-refmap.sh',\n     ++  't5586-fetch-refmap.sh',\n         't5600-clone-fail-cleanup.sh',\n         't5601-clone.sh',\n         't5602-clone-remote-exec.sh',\n      \n     - ## t/t5585-fetch-refmap.sh (new) ##\n     + ## t/t5586-fetch-refmap.sh (new) ##\n      @@\n      +#!/bin/sh\n      +\n 3:  e2f072c254 = 3:  b45f3cf0ab remote: add \"git remote add --limited-fetch\"\n 4:  43b9711a2c = 4:  8c2a144eb7 remote: default to --limited-fetch in a shallow repository\n\n-- \ngitgitgadget\n"},{"id":"553891","messageId":"d48a7004e409952cd1be104840a83168e5e72659.1790925198.git.gitgitgadget@gmail.com","threadId":"66353","inReplyTo":"pull.2412.v5.git.git.1790925198.gitgitgadget@gmail.com","subject":"[PATCH v5 1/4] fetch: add remote.<name>.refmap","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-02T07:13:15Z","receivedAt":"2026-10-02T07:13:25Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nAdd a per-remote config variable, remote.<name>.refmap, that provides\nthe default value for --refmap the same way remote.<name>.fetch\nalready provides the default refspecs to fetch. Like --refmap itself,\nit only maps refs that are actually being fetched, so it has nothing\nto do when there is nothing explicit to fetch, on the command line or\nvia remote.<name>.fetch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/config/remote.adoc |  5 +++++\n Documentation/fetch-options.adoc |  3 +++\n builtin/fetch.c                  |  8 +++++---\n remote.c                         | 12 +++++++++++-\n remote.h                         |  6 ++++++\n t/t5510-fetch.sh                 | 17 +++++++++++++++++\n 6 files changed, 47 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc\nindex 3a20d0f752..103eada406 100644\n--- a/Documentation/config/remote.adoc\n+++ b/Documentation/config/remote.adoc\n@@ -33,6 +33,11 @@ remote.<name>.fetch::\n \tThe default set of \"refspec\" for linkgit:git-fetch[1]. See\n \tlinkgit:git-fetch[1].\n \n+remote.<name>.refmap::\n+\tThe default value of the `--refmap` option for linkgit:git-fetch[1].\n+\tUsed to map remote refs being fetched to remote-tracking refs to\n+\tstore. See the `--refmap` entry in linkgit:git-fetch[1].\n+\n remote.<name>.push::\n \tThe default set of \"refspec\" for linkgit:git-push[1]. See\n \tlinkgit:git-push[1].\ndiff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc\nindex 035f780e58..538914bc6e 100644\n--- a/Documentation/fetch-options.adoc\n+++ b/Documentation/fetch-options.adoc\n@@ -244,6 +244,9 @@ endif::git-pull[]\n \trefspecs and rely entirely on the refspecs supplied as\n \tcommand-line arguments. See section on \"Configured Remote-tracking\n \tBranches\" for details.\n++\n+`remote.<name>.refmap` provides the default value for this option, the\n+same way `remote.<name>.fetch` provides the default refspecs to fetch.\n \n `-t`::\n `--tags`::\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 533fdfe7d8..7651b41139 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -509,6 +509,8 @@ static struct ref *get_ref_map(struct remote *remote,\n \tstruct ref *rm;\n \tstruct ref *ref_map = NULL;\n \tstruct ref **tail = &ref_map;\n+\tstruct refspec *effective_refmap =\n+\t\trefmap.nr ? &refmap : remote ? &remote->refmap : NULL;\n \n \t/* opportunistically-updated references: */\n \tstruct ref *orefs = NULL, **oref_tail = &orefs;\n@@ -552,14 +554,14 @@ static struct ref *get_ref_map(struct remote *remote,\n \t\t * by ref_remove_duplicates() in favor of one of these\n \t\t * opportunistic entries with FETCH_HEAD_IGNORE.\n \t\t */\n-\t\tif (refmap.nr)\n-\t\t\tfetch_refspec = &refmap;\n+\t\tif (effective_refmap && effective_refmap->nr)\n+\t\t\tfetch_refspec = effective_refmap;\n \t\telse\n \t\t\tfetch_refspec = &remote->fetch;\n \n \t\tfor (i = 0; i < fetch_refspec->nr; i++)\n \t\t\tget_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1);\n-\t} else if (refmap.nr) {\n+\t} else if (effective_refmap && effective_refmap->nr) {\n \t\tdie(\"--refmap option is only meaningful with command-line refspec(s)\");\n \t} else {\n \t\t/* Use the defaults */\ndiff --git a/remote.c b/remote.c\nindex fe62068463..017cd9d13e 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -152,6 +152,7 @@ static struct remote *make_remote(struct remote_state *remote_state,\n \tret->name = xstrndup(name, len);\n \trefspec_init_push(&ret->push, the_hash_algo);\n \trefspec_init_fetch(&ret->fetch, the_hash_algo);\n+\trefspec_init_fetch(&ret->refmap, the_hash_algo);\n \tstring_list_init_dup(&ret->server_options);\n \tstring_list_init_dup(&ret->negotiation_restrict);\n \tstring_list_init_dup(&ret->negotiation_include);\n@@ -176,6 +177,7 @@ static void remote_clear(struct remote *remote)\n \n \trefspec_clear(&remote->push);\n \trefspec_clear(&remote->fetch);\n+\trefspec_clear(&remote->refmap);\n \n \tfree((char *)remote->receivepack);\n \tfree((char *)remote->uploadpack);\n@@ -539,6 +541,12 @@ static int handle_config(const char *key, const char *value,\n \t\t\treturn -1;\n \t\trefspec_append(&remote->fetch, v);\n \t\tfree(v);\n+\t} else if (!strcmp(subkey, \"refmap\")) {\n+\t\tchar *v;\n+\t\tif (git_config_string(&v, key, value))\n+\t\t\treturn -1;\n+\t\trefspec_append(&remote->refmap, v);\n+\t\tfree(v);\n \t} else if (!strcmp(subkey, \"receivepack\")) {\n \t\tchar *v;\n \t\tif (git_config_string(&v, key, value))\n@@ -988,7 +996,9 @@ void ref_push_report_free(struct ref_push_report *report)\n \n int remote_find_tracking(struct remote *remote, struct refspec_item *refspec)\n {\n-\treturn refspec_find_match(&remote->fetch, refspec);\n+\tif (remote->fetch.nr)\n+\t\treturn refspec_find_match(&remote->fetch, refspec);\n+\treturn refspec_find_match(&remote->refmap, refspec);\n }\n \n static struct ref *alloc_ref_with_prefix(const char *prefix, size_t prefixlen,\ndiff --git a/remote.h b/remote.h\nindex cca02033b9..ac485a584d 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -90,6 +90,12 @@ struct remote {\n \n \tstruct refspec fetch;\n \n+\t/*\n+\t * How to map refs fetched without an explicit destination into our\n+\t * own namespace, the same as the --refmap command line option.\n+\t */\n+\tstruct refspec refmap;\n+\n \t/*\n \t * The setting for whether to fetch tags (as a separate rule from the\n \t * configured refspecs);\ndiff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh\nindex a8d38d9176..bd853d0a84 100755\n--- a/t/t5510-fetch.sh\n+++ b/t/t5510-fetch.sh\n@@ -927,6 +927,23 @@ test_expect_success 'explicit --refmap option overrides remote.*.fetch' '\n \t)\n '\n \n+test_expect_success 'remote.*.refmap acts like --refmap on the command line' '\n+\ttest_when_finished \"git -C three config --unset remote.origin.refmap\" &&\n+\tgit branch -f side &&\n+\tgit -C three config remote.origin.refmap \\\n+\t\t\"refs/heads/*:refs/remotes/other/*\" &&\n+\t(\n+\t\tcd three &&\n+\t\tgit update-ref refs/remotes/origin/main base-origin-main &&\n+\t\to=$(git rev-parse --verify refs/remotes/origin/main) &&\n+\t\tgit fetch origin main &&\n+\t\tn=$(git rev-parse --verify refs/remotes/origin/main) &&\n+\t\ttest \"$o\" = \"$n\" &&\n+\t\ttest_must_fail git rev-parse --verify refs/remotes/origin/side &&\n+\t\tgit rev-parse --verify refs/remotes/other/main\n+\t)\n+'\n+\n test_expect_success 'explicitly empty --refmap option disables remote.*.fetch' '\n \tgit branch -f side &&\n \t(\n-- \ngitgitgadget\n\n"},{"id":"553892","messageId":"fecdbc43b8fcf1563756234f28928c737562c583.1790925198.git.gitgitgadget@gmail.com","threadId":"66353","inReplyTo":"pull.2412.v5.git.git.1790925198.gitgitgadget@gmail.com","subject":"[PATCH v5 2/4] fetch: infer branches to fetch from a refmap-only remote","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-02T07:13:16Z","receivedAt":"2026-10-02T07:13:28Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nConfiguring remote.<name>.refmap without remote.<name>.fetch used to\nmake a refspec-less \"git fetch <name>\" fail with \"--refmap option is\nonly meaningful with command-line refspec(s)\", since a refmap only\nsays where to put fetched refs, not what to fetch.\n\nMake that case infer what to fetch: the local branches whose\n@{upstream} is already on that remote. This lets a remote be\nconfigured to fetch only the branches actually in use, without\nlisting them by hand in remote.<name>.fetch, and without needing to\ntouch the command line every time.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/config/remote.adoc |   5 +-\n Documentation/fetch-options.adoc |   6 +-\n builtin/fetch.c                  |  52 ++++++++++++--\n remote.c                         |  29 ++++++++\n remote.h                         |   3 +\n t/meson.build                    |   1 +\n t/t5586-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++\n 7 files changed, 205 insertions(+), 10 deletions(-)\n create mode 100755 t/t5586-fetch-refmap.sh\n\ndiff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc\nindex 103eada406..7c52e443a5 100644\n--- a/Documentation/config/remote.adoc\n+++ b/Documentation/config/remote.adoc\n@@ -36,7 +36,10 @@ remote.<name>.fetch::\n remote.<name>.refmap::\n \tThe default value of the `--refmap` option for linkgit:git-fetch[1].\n \tUsed to map remote refs being fetched to remote-tracking refs to\n-\tstore. See the `--refmap` entry in linkgit:git-fetch[1].\n+\tstore. If `remote.<name>.fetch` is not set either, a refspec-less\n+\tfetch infers what to fetch from local branches built on this\n+\tremote, instead of fetching every branch it has. See the\n+\t`--refmap` entry in linkgit:git-fetch[1].\n \n remote.<name>.push::\n \tThe default set of \"refspec\" for linkgit:git-push[1]. See\ndiff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc\nindex 538914bc6e..315daf97d7 100644\n--- a/Documentation/fetch-options.adoc\n+++ b/Documentation/fetch-options.adoc\n@@ -245,8 +245,10 @@ endif::git-pull[]\n \tcommand-line arguments. See section on \"Configured Remote-tracking\n \tBranches\" for details.\n +\n-`remote.<name>.refmap` provides the default value for this option, the\n-same way `remote.<name>.fetch` provides the default refspecs to fetch.\n+When a refmap is active (from `--refmap` or `remote.<name>.refmap`) but\n+there is nothing to fetch, neither on the command line nor from\n+`remote.<name>.fetch`, Git infers what to fetch from the local branches\n+whose `@{upstream}` is on that remote.\n \n `-t`::\n `--tags`::\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 7651b41139..7ef2776f17 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -511,6 +511,8 @@ static struct ref *get_ref_map(struct remote *remote,\n \tstruct ref **tail = &ref_map;\n \tstruct refspec *effective_refmap =\n \t\trefmap.nr ? &refmap : remote ? &remote->refmap : NULL;\n+\tstruct refspec inferred_rs;\n+\tint inferred_branches = 0;\n \n \t/* opportunistically-updated references: */\n \tstruct ref *orefs = NULL, **oref_tail = &orefs;\n@@ -518,15 +520,32 @@ static struct ref *get_ref_map(struct remote *remote,\n \tstruct hashmap existing_refs;\n \tint existing_refs_populated = 0;\n \n+\trefspec_init_fetch(&inferred_rs, the_hash_algo);\n+\n \tfilter_prefetch_refspec(rs);\n \tif (remote)\n \t\tfilter_prefetch_refspec(&remote->fetch);\n \n+\tif (!rs->nr && remote && !remote->fetch.nr &&\n+\t    effective_refmap && effective_refmap->nr) {\n+\t\tstruct string_list tracked = STRING_LIST_INIT_DUP;\n+\t\tstruct string_list_item *item;\n+\n+\t\tbranches_tracking_remote(remote, &tracked);\n+\t\tfor_each_string_list_item(item, &tracked)\n+\t\t\trefspec_append(&inferred_rs, item->string);\n+\t\tstring_list_clear(&tracked, 0);\n+\n+\t\trs = &inferred_rs;\n+\t\tinferred_branches = 1;\n+\t}\n+\n \tif (rs->nr) {\n \t\tstruct refspec *fetch_refspec;\n \n \t\tfor (i = 0; i < rs->nr; i++) {\n-\t\t\tget_fetch_map(remote_refs, &rs->items[i], &tail, 0);\n+\t\t\tget_fetch_map(remote_refs, &rs->items[i], &tail,\n+\t\t\t\t      inferred_branches);\n \t\t\tif (rs->items[i].dst && rs->items[i].dst[0])\n \t\t\t\t*autotags = 1;\n \t\t}\n@@ -561,6 +580,8 @@ static struct ref *get_ref_map(struct remote *remote,\n \n \t\tfor (i = 0; i < fetch_refspec->nr; i++)\n \t\t\tget_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1);\n+\t} else if (inferred_branches) {\n+\t\t/* Already fully handled above. */\n \t} else if (effective_refmap && effective_refmap->nr) {\n \t\tdie(\"--refmap option is only meaningful with command-line refspec(s)\");\n \t} else {\n@@ -656,6 +677,7 @@ static struct ref *get_ref_map(struct remote *remote,\n \tif (existing_refs_populated)\n \t\thashmap_clear_and_free(&existing_refs, struct refname_hash_entry, ent);\n \n+\trefspec_clear(&inferred_rs);\n \treturn ref_map;\n }\n \n@@ -1960,15 +1982,30 @@ static int do_fetch(struct transport *transport,\n \t\trefspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);\n \t} else {\n \t\tstruct branch *branch = branch_get(NULL);\n-\n-\t\tif (transport->remote->fetch.nr) {\n+\t\tint tracks_this_remote = branch && branch_has_merge_config(branch) &&\n+\t\t\t!strcmp(branch->remote_name, transport->remote->name);\n+\t\tstruct refspec *effective_refmap = refmap.nr ? &refmap :\n+\t\t\t&transport->remote->refmap;\n+\t\tint inferred_branches = !transport->remote->fetch.nr &&\n+\t\t\teffective_refmap->nr;\n+\n+\t\tif (inferred_branches) {\n+\t\t\tstruct string_list tracked = STRING_LIST_INIT_DUP;\n+\t\t\tstruct string_list_item *item;\n+\n+\t\t\tbranches_tracking_remote(transport->remote, &tracked);\n+\t\t\tfor_each_string_list_item(item, &tracked)\n+\t\t\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n+\t\t\t\t\t    item->string);\n+\t\t\tstring_list_clear(&tracked, 0);\n+\t\t} else if (transport->remote->fetch.nr) {\n \t\t\trefspec_ref_prefixes(&transport->remote->fetch,\n \t\t\t\t\t     &transport_ls_refs_options.ref_prefixes);\n-\t\t\tif (follow_remote_head != FOLLOW_REMOTE_NEVER)\n-\t\t\t\tdo_set_head = 1;\n \t\t}\n-\t\tif (branch && branch_has_merge_config(branch) &&\n-\t\t    !strcmp(branch->remote_name, transport->remote->name)) {\n+\t\tif ((transport->remote->fetch.nr || inferred_branches) &&\n+\t\t    follow_remote_head != FOLLOW_REMOTE_NEVER)\n+\t\t\tdo_set_head = 1;\n+\t\tif (tracks_this_remote) {\n \t\t\tint i;\n \t\t\tfor (i = 0; i < branch->merge_nr; i++) {\n \t\t\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n@@ -2009,6 +2046,7 @@ static int do_fetch(struct transport *transport,\n \n \tref_map = get_ref_map(transport->remote, remote_refs, rs,\n \t\t\t      tags, &autotags);\n+\n \tif (!update_head_ok)\n \t\tcheck_not_current_branch(ref_map);\n \ndiff --git a/remote.c b/remote.c\nindex 017cd9d13e..a5deb9b929 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -1884,6 +1884,35 @@ int branch_merge_matches(struct branch *branch,\n \treturn refname_match(branch->merge[i]->src, refname);\n }\n \n+struct branches_tracking_remote_cb_data {\n+\tstruct remote *remote;\n+\tstruct string_list *tracked;\n+};\n+\n+static int add_if_tracking_remote(const struct reference *ref, void *cb_data)\n+{\n+\tstruct branches_tracking_remote_cb_data *data = cb_data;\n+\tstruct branch *branch;\n+\n+\tbranch = branch_get(ref->name);\n+\tif (!branch_has_merge_config(branch) ||\n+\t    strcmp(branch->remote_name, data->remote->name))\n+\t\treturn 0;\n+\n+\tfor (int i = 0; i < branch->merge_nr; i++)\n+\t\tstring_list_insert(data->tracked, branch->merge[i]->src);\n+\n+\treturn 0;\n+}\n+\n+void branches_tracking_remote(struct remote *remote, struct string_list *tracked)\n+{\n+\tstruct branches_tracking_remote_cb_data data = { remote, tracked };\n+\n+\trefs_for_each_branch_ref(get_main_ref_store(the_repository),\n+\t\t\t\t  add_if_tracking_remote, &data);\n+}\n+\n __attribute__((format (printf,2,3)))\n static char *error_buf(struct strbuf *err, const char *fmt, ...)\n {\ndiff --git a/remote.h b/remote.h\nindex ac485a584d..da6c3ef53a 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -359,6 +359,9 @@ int branch_has_merge_config(struct branch *branch);\n \n int branch_merge_matches(struct branch *, int n, const char *);\n \n+/* fills tracked with the refname of every local branch's upstream on remote */\n+void branches_tracking_remote(struct remote *remote, struct string_list *tracked);\n+\n /* list of the remote in a group as configured */\n struct remote_group_data {\n \tconst char *name;\ndiff --git a/t/meson.build b/t/meson.build\nindex 3ca7b27104..a1c7465c2b 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -728,6 +728,7 @@ integration_tests = [\n   't5582-fetch-negative-refspec.sh',\n   't5583-push-branches.sh',\n   't5584-http-429-retry.sh',\n+  't5586-fetch-refmap.sh',\n   't5600-clone-fail-cleanup.sh',\n   't5601-clone.sh',\n   't5602-clone-remote-exec.sh',\ndiff --git a/t/t5586-fetch-refmap.sh b/t/t5586-fetch-refmap.sh\nnew file mode 100755\nindex 0000000000..b81fc48cbe\n--- /dev/null\n+++ b/t/t5586-fetch-refmap.sh\n@@ -0,0 +1,119 @@\n+#!/bin/sh\n+\n+test_description='\"git fetch\" with a remote.<name>.refmap but no remote.<name>.fetch\n+\n+When a remote has a refmap configured but no fetch refspec, a\n+refspec-less fetch infers what to fetch from the local branches whose\n+@{upstream} is on that remote.\n+'\n+\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup' '\n+\ttest_commit main-1 &&\n+\ttest_commit main-2 &&\n+\tgit checkout -b side main-1 &&\n+\ttest_commit side-1 &&\n+\tgit checkout -b next main-1 &&\n+\ttest_commit next-1 &&\n+\tgit checkout main\n+'\n+\n+test_expect_success 'clone shallow and single-branch, then add a second remote' '\n+\tgit clone --no-local --depth=1 --branch main --single-branch . client &&\n+\t(\n+\t\tcd client &&\n+\t\tgit remote add upstream .. &&\n+\t\ttest_might_fail git config unset remote.upstream.fetch &&\n+\t\tgit config remote.upstream.refmap \\\n+\t\t\t\"+refs/heads/*:refs/remotes/upstream/*\"\n+\t)\n+'\n+\n+test_expect_success 'a bare fetch needs nothing until a branch is tracked' '\n+\t(\n+\t\tcd client &&\n+\t\tgit fetch upstream &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n+\t\ttest_must_be_empty actual\n+\t)\n+'\n+\n+test_expect_success 'an explicit one-time fetch lets a branch be tracked' '\n+\t(\n+\t\tcd client &&\n+\t\tgit fetch upstream main &&\n+\t\tgit branch --set-upstream-to=upstream/main &&\n+\t\ttest_cmp_config upstream branch.main.remote &&\n+\t\ttest_cmp_config refs/heads/main branch.main.merge\n+\t)\n+'\n+\n+test_expect_success 'a branch checked out from a one-time fetch is kept updated by later plain fetches' '\n+\t(\n+\t\tcd client &&\n+\t\tgit fetch upstream side:refs/remotes/upstream/side &&\n+\t\tgit branch side-topic upstream/side\n+\t) &&\n+\tgit checkout side &&\n+\ttest_commit side-2 &&\n+\tgit checkout main &&\n+\t(\n+\t\tcd client &&\n+\t\tgit fetch upstream &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\trefs/remotes/upstream/side\n+\t\tEOF\n+\t\ttest_cmp expect actual &&\n+\t\tgit rev-parse refs/remotes/upstream/side >actual-oid &&\n+\t\tgit -C .. rev-parse side >expect-oid &&\n+\t\ttest_cmp expect-oid actual-oid\n+\t)\n+'\n+\n+test_expect_success 'a second branch tracking the same upstream branch does not fetch it twice' '\n+\t(\n+\t\tcd client &&\n+\t\tgit branch side-topic-2 upstream/side &&\n+\t\tgit fetch upstream &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\trefs/remotes/upstream/side\n+\t\tEOF\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'a branch tracking a different remote is not fetched from upstream' '\n+\t(\n+\t\tcd client &&\n+\t\tgit remote add other .. &&\n+\t\tgit fetch other next:refs/remotes/other/next &&\n+\t\tgit branch next-topic other/next &&\n+\t\tgit fetch upstream &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\trefs/remotes/upstream/side\n+\t\tEOF\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'git remote show does not choke on a refmap-only remote' '\n+\t(\n+\t\tcd client &&\n+\t\tgit remote show upstream\n+\t)\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"553893","messageId":"b45f3cf0ab1e2dccd87cc4615e0ba15e6cc996b6.1790925198.git.gitgitgadget@gmail.com","threadId":"66353","inReplyTo":"pull.2412.v5.git.git.1790925198.gitgitgadget@gmail.com","subject":"[PATCH v5 3/4] remote: add \"git remote add --limited-fetch\"","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-02T07:13:17Z","receivedAt":"2026-10-02T07:13:28Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nA remote added the ordinary way tracks every branch it has, via a\nwildcard remote.<name>.fetch refspec. That is wasteful for a remote\nwhose history is only worth following for the branches actually in\nuse locally, and it can make \"git fetch\" negotiate history for\nbranches nobody asked for.\n\nGive \"git remote add\" a --limited-fetch option that sets up\nremote.<name>.refmap instead of remote.<name>.fetch, so that\n\"git fetch <name>\" only fetches the branches already tracked, plus\nthe remote's default branch, as described in the previous commit.\nIt is rejected together with -t/--track or --mirror, since those\nalready say explicitly what to fetch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/git-remote.adoc |  8 +++++++-\n builtin/remote.c              | 28 ++++++++++++++++++++++------\n t/t5505-remote.sh             | 16 ++++++++++++++++\n 3 files changed, 45 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/git-remote.adoc b/Documentation/git-remote.adoc\nindex eaae30aa88..4255f8b3e6 100644\n--- a/Documentation/git-remote.adoc\n+++ b/Documentation/git-remote.adoc\n@@ -10,7 +10,7 @@ SYNOPSIS\n --------\n [synopsis]\n git remote [-v | --verbose]\n-git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] <name> <URL>\n+git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] [--[no-]limited-fetch] <name> <URL>\n git remote rename [--[no-]progress] <old> <new>\n git remote remove <name>\n git remote set-head <name> (-a | --auto | -d | --delete | <branch>)\n@@ -70,6 +70,12 @@ the `refs/remotes/<name>/` namespace, a refspec to track only _<branch>_\n is created.  You can give more than one `-t <branch>` to track\n multiple branches without grabbing all branches.\n +\n+With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec\n+that tracks all branches, `remote.<name>.refmap` is set up so that a\n+refspec-less `git fetch <name>` only fetches branches our local branches\n+are built on. See the `--refmap` entry in linkgit:git-fetch[1] for\n+details.\n++\n With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set\n up to point at remote's _<master>_ branch. See also the set-head command.\n +\ndiff --git a/builtin/remote.c b/builtin/remote.c\nindex de989ea3ba..f036dd5d6f 100644\n--- a/builtin/remote.c\n+++ b/builtin/remote.c\n@@ -179,6 +179,7 @@ static int add(int argc, const char **argv, const char *prefix,\n {\n \tint fetch = 0, fetch_tags = TAGS_DEFAULT;\n \tunsigned mirror = MIRROR_NONE;\n+\tint limited_fetch = -1; /* unspecified */\n \tstruct string_list track = STRING_LIST_INIT_NODUP;\n \tconst char *master = NULL;\n \tstruct remote *remote;\n@@ -198,6 +199,8 @@ static int add(int argc, const char **argv, const char *prefix,\n \t\tOPT_CALLBACK_F(0, \"mirror\", &mirror, \"(push|fetch)\",\n \t\t\tN_(\"set up remote as a mirror to push to or fetch from\"),\n \t\t\tPARSE_OPT_OPTARG | PARSE_OPT_COMP_ARG, parse_mirror_opt),\n+\t\tOPT_BOOL(0, \"limited-fetch\", &limited_fetch,\n+\t\t\tN_(\"fetch only the branches we build on, instead of every branch\")),\n \t\tOPT_END()\n \t};\n \n@@ -211,6 +214,10 @@ static int add(int argc, const char **argv, const char *prefix,\n \t\tdie(_(\"specifying a master branch makes no sense with --mirror\"));\n \tif (mirror && !(mirror & MIRROR_FETCH) && track.nr)\n \t\tdie(_(\"specifying branches to track makes sense only with fetch mirrors\"));\n+\tif (limited_fetch == 1 && track.nr)\n+\t\tdie(_(\"--limited-fetch does not make sense with -t/--track\"));\n+\tif (limited_fetch == 1 && mirror)\n+\t\tdie(_(\"--limited-fetch does not make sense with --mirror\"));\n \n \tname = argv[0];\n \turl = argv[1];\n@@ -230,13 +237,22 @@ static int add(int argc, const char **argv, const char *prefix,\n \trepo_config_set(the_repository, buf.buf, url);\n \n \tif (!mirror || mirror & MIRROR_FETCH) {\n+\t\tint use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&\n+\t\t\tlimited_fetch == 1;\n+\n \t\tstrbuf_reset(&buf);\n-\t\tstrbuf_addf(&buf, \"remote.%s.fetch\", name);\n-\t\tif (track.nr == 0)\n-\t\t\tstring_list_append(&track, \"*\");\n-\t\tfor (size_t i = 0; i < track.nr; i++) {\n-\t\t\tadd_branch(buf.buf, track.items[i].string,\n-\t\t\t\t   name, mirror, &buf2);\n+\t\tif (use_limited_fetch) {\n+\t\t\tstrbuf_addf(&buf, \"remote.%s.refmap\", name);\n+\t\t\tstrbuf_reset(&buf2);\n+\t\t\tstrbuf_addf(&buf2, \"+refs/heads/*:refs/remotes/%s/*\", name);\n+\t\t\trepo_config_set(the_repository, buf.buf, buf2.buf);\n+\t\t} else {\n+\t\t\tstrbuf_addf(&buf, \"remote.%s.fetch\", name);\n+\t\t\tif (track.nr == 0)\n+\t\t\t\tstring_list_append(&track, \"*\");\n+\t\t\tfor (size_t i = 0; i < track.nr; i++)\n+\t\t\t\tadd_branch(buf.buf, track.items[i].string,\n+\t\t\t\t\t   name, mirror, &buf2);\n \t\t}\n \t}\n \ndiff --git a/t/t5505-remote.sh b/t/t5505-remote.sh\nindex 5fbfcb0848..0168d5abfe 100755\n--- a/t/t5505-remote.sh\n+++ b/t/t5505-remote.sh\n@@ -137,6 +137,22 @@ test_expect_success 'filters are listed by git remote -v only' '\n \ttest_grep ! \"\\[blob:none\\]\" out\n '\n \n+test_expect_success '--limited-fetch works in a full repository too' '\n+\ttest_when_finished \"rm -rf full-add\" &&\n+\tgit clone --no-local one full-add &&\n+\t(\n+\t\tcd full-add &&\n+\t\tgit remote add --limited-fetch upstream ../two &&\n+\t\ttest_cmp_config \"+refs/heads/*:refs/remotes/upstream/*\" \\\n+\t\t\tremote.upstream.refmap &&\n+\t\ttest_must_fail git config get remote.upstream.fetch\n+\t)\n+'\n+\n+test_expect_success '--limited-fetch conflicts with -t' '\n+\ttest_must_fail git remote add --limited-fetch -t main upstream ../two\n+'\n+\n test_expect_success 'check remote-tracking' '\n \t(\n \t\tcd test &&\n-- \ngitgitgadget\n\n"},{"id":"553894","messageId":"8c2a144eb7d9010ce1239d92aadd1288108921c3.1790925198.git.gitgitgadget@gmail.com","threadId":"66353","inReplyTo":"pull.2412.v5.git.git.1790925198.gitgitgadget@gmail.com","subject":"[PATCH v5 4/4] remote: default to --limited-fetch in a shallow repository","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-02T07:13:18Z","receivedAt":"2026-10-02T07:13:30Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nAdding a second remote to a shallow, single-branch clone used to\nstill fetch every branch that remote has, since \"git remote add\"\nalways set up a wildcard remote.<name>.fetch refspec regardless of\nhow shallow the repository already was. That defeats the purpose of\nhaving cloned shallow and single-branch in the first place, and can\nmake a plain \"git fetch\" on that remote hang or take a very long\ntime on a repository with many branches.\n\nTurn --limited-fetch on by default when the repository is already\nshallow and neither -t/--track nor --mirror was given, so that\nadding a remote there does not by itself commit to following every\nbranch it has. --no-limited-fetch keeps the previous behavior for\nwhoever wants it.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/git-remote.adoc |  8 ++++-\n builtin/remote.c              |  5 ++-\n t/t5505-remote.sh             | 60 +++++++++++++++++++++++++++++++++++\n 3 files changed, 71 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/git-remote.adoc b/Documentation/git-remote.adoc\nindex 4255f8b3e6..80f8a4a183 100644\n--- a/Documentation/git-remote.adoc\n+++ b/Documentation/git-remote.adoc\n@@ -52,6 +52,11 @@ Add a remote named _<name>_ for the repository at\n _<URL>_.  The command `git fetch <name>` can then be used to create and\n update remote-tracking branches `<name>/<branch>`.\n +\n+If the repository is already a shallow repository (see linkgit:git-clone[1]\n+`--depth`) and neither `-t`, `--mirror` nor `--no-limited-fetch` is given,\n+`--limited-fetch` is turned on by default, so that `git fetch <name>` does\n+not need to negotiate history for every branch the remote has.\n++\n With `-f` option, `git fetch <name>` is run immediately after\n the remote information is set up.\n +\n@@ -74,7 +79,8 @@ With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec\n that tracks all branches, `remote.<name>.refmap` is set up so that a\n refspec-less `git fetch <name>` only fetches branches our local branches\n are built on. See the `--refmap` entry in linkgit:git-fetch[1] for\n-details.\n+details. `--no-limited-fetch` explicitly disables this, overriding the\n+shallow-repository default described above.\n +\n With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set\n up to point at remote's _<master>_ branch. See also the set-head command.\ndiff --git a/builtin/remote.c b/builtin/remote.c\nindex f036dd5d6f..0cf1126976 100644\n--- a/builtin/remote.c\n+++ b/builtin/remote.c\n@@ -16,6 +16,7 @@\n #include \"rebase.h\"\n #include \"refs.h\"\n #include \"refspec.h\"\n+#include \"shallow.h\"\n #include \"odb.h\"\n #include \"strvec.h\"\n #include \"commit-reach.h\"\n@@ -238,7 +239,9 @@ static int add(int argc, const char **argv, const char *prefix,\n \n \tif (!mirror || mirror & MIRROR_FETCH) {\n \t\tint use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&\n-\t\t\tlimited_fetch == 1;\n+\t\t\t(limited_fetch == 1 ||\n+\t\t\t (limited_fetch == -1 &&\n+\t\t\t  is_repository_shallow(the_repository)));\n \n \t\tstrbuf_reset(&buf);\n \t\tif (use_limited_fetch) {\ndiff --git a/t/t5505-remote.sh b/t/t5505-remote.sh\nindex 0168d5abfe..f3b5905e9e 100755\n--- a/t/t5505-remote.sh\n+++ b/t/t5505-remote.sh\n@@ -137,6 +137,66 @@ test_expect_success 'filters are listed by git remote -v only' '\n \ttest_grep ! \"\\[blob:none\\]\" out\n '\n \n+test_expect_success 'add remote -t keeps an explicit refspec in a shallow repository' '\n+\ttest_when_finished \"rm -rf shallow-add\" &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\tone shallow-add &&\n+\t(\n+\t\tcd shallow-add &&\n+\t\tgit remote add -t main upstream ../two &&\n+\t\ttest_cmp_config \"+refs/heads/main:refs/remotes/upstream/main\" \\\n+\t\t\tremote.upstream.fetch\n+\t)\n+'\n+\n+test_expect_success 'add remote keeps the wildcard refspec in a full repository' '\n+\ttest_when_finished \"rm -rf full-add\" &&\n+\tgit clone --no-local one full-add &&\n+\t(\n+\t\tcd full-add &&\n+\t\tgit remote add upstream ../two &&\n+\t\ttest_cmp_config \"+refs/heads/*:refs/remotes/upstream/*\" \\\n+\t\t\tremote.upstream.fetch\n+\t)\n+'\n+\n+test_expect_success 'a remote added in a shallow repository defaults to --limited-fetch' '\n+\ttest_when_finished \"rm -rf shallow-add\" &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\tone shallow-add &&\n+\t(\n+\t\tcd shallow-add &&\n+\t\tgit remote add upstream ../two &&\n+\t\ttest_cmp_config \"+refs/heads/*:refs/remotes/upstream/*\" \\\n+\t\t\tremote.upstream.refmap &&\n+\t\ttest_must_fail git config get remote.upstream.fetch &&\n+\t\tgit fetch upstream main &&\n+\t\tgit branch --set-upstream-to=upstream/main &&\n+\t\ttest_cmp_config upstream branch.main.remote &&\n+\t\ttest_cmp_config refs/heads/main branch.main.merge &&\n+\t\tgit fetch upstream &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\tEOF\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '--no-limited-fetch overrides the shallow-repository default' '\n+\ttest_when_finished \"rm -rf shallow-add\" &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\tone shallow-add &&\n+\t(\n+\t\tcd shallow-add &&\n+\t\tgit remote add --no-limited-fetch upstream ../two &&\n+\t\ttest_cmp_config \"+refs/heads/*:refs/remotes/upstream/*\" \\\n+\t\t\tremote.upstream.fetch &&\n+\t\ttest_must_fail git config get remote.upstream.refmap\n+\t)\n+'\n+\n test_expect_success '--limited-fetch works in a full repository too' '\n \ttest_when_finished \"rm -rf full-add\" &&\n \tgit clone --no-local one full-add &&\n-- \ngitgitgadget\n"},{"id":"553981","messageId":"xmqqzewwxe1y.fsf@gitster.g","threadId":"66353","inReplyTo":"b45f3cf0ab1e2dccd87cc4615e0ba15e6cc996b6.1790925198.git.gitgitgadget@gmail.com","subject":"Re: [PATCH v5 3/4] remote: add \"git remote add --limited-fetch\"","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-10-02T16:28:09Z","receivedAt":"2026-10-02T16:28:12Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: Harald Nordgren <haraldnordgren@gmail.com>\n>\n> A remote added the ordinary way tracks every branch it has, via a\n> wildcard remote.<name>.fetch refspec. That is wasteful for a remote\n> whose history is only worth following for the branches actually in\n> use locally, and it can make \"git fetch\" negotiate history for\n> branches nobody asked for.\n>\n> Give \"git remote add\" a --limited-fetch option that sets up\n> remote.<name>.refmap instead of remote.<name>.fetch, so that\n> \"git fetch <name>\" only fetches the branches already tracked, plus\n> the remote's default branch, as described in the previous commit.\n> It is rejected together with -t/--track or --mirror, since those\n> already say explicitly what to fetch.\n\nYou forgot to remove \", plus the ... in the previous commit\",\nprobably, when you updated the series to stop doing so in the step\n[2/4].\n"},{"id":"554095","messageId":"pull.2412.v6.git.git.1791102684.gitgitgadget@gmail.com","threadId":"66353","inReplyTo":"pull.2412.git.git.1789829246437.gitgitgadget@gmail.com","subject":"[PATCH v6 0/4] fetch: avoid fetching every branch of a new remote in a shallow repo","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-04T08:31:20Z","receivedAt":"2026-10-04T08:31:27Z","isPatch":true,"body":"Avoid fetching every branch of a new remote in a shallow repo.\n\nChanges in v6:\n\n * Remove leftover reference to deleted default-branch logic in commit\n   message.\n\nChanges in v5:\n\n * Renumber test file from t5585 to t5586.\n\nChanges in v4:\n\n * Removed the automatic default-branch fetch. A fresh remote fetches\n   nothing until you track a branch explicitly.\n * Fixed fetch report showing \"new ref HEAD\" instead of \"new branch \"\n * Reworded remote..refmap docs and commit message.\n\nChanges in v3:\n\n * Replace the special \":\"/\"+:\" fetch refspec with remote.<name>.refmap,\n   reusing git's existing --refmap mechanism instead of inventing new\n   refspec syntax.\n * Split the change into 4 commits.\n\nChanges in v2:\n\n * Replaced the opt-in fetch.shallow config entirely with a new special\n   fetch refspec (+:) that git remote add now defaults new remotes to in a\n   shallow repository. The new refspec fetches whichever branches any local\n   branch tracks at that remote, plus the remote's default branch.\n\nHarald Nordgren (4):\n  fetch: add remote.<name>.refmap\n  fetch: infer branches to fetch from a refmap-only remote\n  remote: add \"git remote add --limited-fetch\"\n  remote: default to --limited-fetch in a shallow repository\n\n Documentation/config/remote.adoc |   8 +++\n Documentation/fetch-options.adoc |   5 ++\n Documentation/git-remote.adoc    |  14 +++-\n builtin/fetch.c                  |  60 +++++++++++++---\n builtin/remote.c                 |  31 ++++++--\n remote.c                         |  41 ++++++++++-\n remote.h                         |   9 +++\n t/meson.build                    |   1 +\n t/t5505-remote.sh                |  76 ++++++++++++++++++++\n t/t5510-fetch.sh                 |  17 +++++\n t/t5586-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++\n 11 files changed, 363 insertions(+), 18 deletions(-)\n create mode 100755 t/t5586-fetch-refmap.sh\n\n\nbase-commit: c46c1e37724f0478939de636ab8ea5a89086d532\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2412%2FHaraldNordgren%2Ffetch-shallow-narrow-refspec-v6\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2412/HaraldNordgren/fetch-shallow-narrow-refspec-v6\nPull-Request: https://github.com/git/git/pull/2412\n\nRange-diff vs v5:\n\n 1:  d48a7004e4 = 1:  5c31a51bb7 fetch: add remote.<name>.refmap\n 2:  fecdbc43b8 = 2:  74c177e3e2 fetch: infer branches to fetch from a refmap-only remote\n 3:  b45f3cf0ab ! 3:  3abcc8915b remote: add \"git remote add --limited-fetch\"\n     @@ Commit message\n      \n          Give \"git remote add\" a --limited-fetch option that sets up\n          remote.<name>.refmap instead of remote.<name>.fetch, so that\n     -    \"git fetch <name>\" only fetches the branches already tracked, plus\n     -    the remote's default branch, as described in the previous commit.\n     -    It is rejected together with -t/--track or --mirror, since those\n     -    already say explicitly what to fetch.\n     +    \"git fetch <name>\" only fetches the branches already tracked, as\n     +    described in the previous commit. It is rejected together with\n     +    -t/--track or --mirror, since those already say explicitly what to\n     +    fetch.\n      \n          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n      \n 4:  8c2a144eb7 = 4:  84d192445c remote: default to --limited-fetch in a shallow repository\n\n-- \ngitgitgadget\n"},{"id":"554096","messageId":"5c31a51bb7ad9d8e7e88522e3e4903a07d830568.1791102684.git.gitgitgadget@gmail.com","threadId":"66353","inReplyTo":"pull.2412.v6.git.git.1791102684.gitgitgadget@gmail.com","subject":"[PATCH v6 1/4] fetch: add remote.<name>.refmap","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-04T08:31:21Z","receivedAt":"2026-10-04T08:31:29Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nAdd a per-remote config variable, remote.<name>.refmap, that provides\nthe default value for --refmap the same way remote.<name>.fetch\nalready provides the default refspecs to fetch. Like --refmap itself,\nit only maps refs that are actually being fetched, so it has nothing\nto do when there is nothing explicit to fetch, on the command line or\nvia remote.<name>.fetch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/config/remote.adoc |  5 +++++\n Documentation/fetch-options.adoc |  3 +++\n builtin/fetch.c                  |  8 +++++---\n remote.c                         | 12 +++++++++++-\n remote.h                         |  6 ++++++\n t/t5510-fetch.sh                 | 17 +++++++++++++++++\n 6 files changed, 47 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc\nindex 3a20d0f752..103eada406 100644\n--- a/Documentation/config/remote.adoc\n+++ b/Documentation/config/remote.adoc\n@@ -33,6 +33,11 @@ remote.<name>.fetch::\n \tThe default set of \"refspec\" for linkgit:git-fetch[1]. See\n \tlinkgit:git-fetch[1].\n \n+remote.<name>.refmap::\n+\tThe default value of the `--refmap` option for linkgit:git-fetch[1].\n+\tUsed to map remote refs being fetched to remote-tracking refs to\n+\tstore. See the `--refmap` entry in linkgit:git-fetch[1].\n+\n remote.<name>.push::\n \tThe default set of \"refspec\" for linkgit:git-push[1]. See\n \tlinkgit:git-push[1].\ndiff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc\nindex 035f780e58..538914bc6e 100644\n--- a/Documentation/fetch-options.adoc\n+++ b/Documentation/fetch-options.adoc\n@@ -244,6 +244,9 @@ endif::git-pull[]\n \trefspecs and rely entirely on the refspecs supplied as\n \tcommand-line arguments. See section on \"Configured Remote-tracking\n \tBranches\" for details.\n++\n+`remote.<name>.refmap` provides the default value for this option, the\n+same way `remote.<name>.fetch` provides the default refspecs to fetch.\n \n `-t`::\n `--tags`::\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex b2decc6cfd..04d0a78ecf 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -509,6 +509,8 @@ static struct ref *get_ref_map(struct remote *remote,\n \tstruct ref *rm;\n \tstruct ref *ref_map = NULL;\n \tstruct ref **tail = &ref_map;\n+\tstruct refspec *effective_refmap =\n+\t\trefmap.nr ? &refmap : remote ? &remote->refmap : NULL;\n \n \t/* opportunistically-updated references: */\n \tstruct ref *orefs = NULL, **oref_tail = &orefs;\n@@ -552,14 +554,14 @@ static struct ref *get_ref_map(struct remote *remote,\n \t\t * by ref_remove_duplicates() in favor of one of these\n \t\t * opportunistic entries with FETCH_HEAD_IGNORE.\n \t\t */\n-\t\tif (refmap.nr)\n-\t\t\tfetch_refspec = &refmap;\n+\t\tif (effective_refmap && effective_refmap->nr)\n+\t\t\tfetch_refspec = effective_refmap;\n \t\telse\n \t\t\tfetch_refspec = &remote->fetch;\n \n \t\tfor (i = 0; i < fetch_refspec->nr; i++)\n \t\t\tget_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1);\n-\t} else if (refmap.nr) {\n+\t} else if (effective_refmap && effective_refmap->nr) {\n \t\tdie(\"--refmap option is only meaningful with command-line refspec(s)\");\n \t} else {\n \t\t/* Use the defaults */\ndiff --git a/remote.c b/remote.c\nindex fe62068463..017cd9d13e 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -152,6 +152,7 @@ static struct remote *make_remote(struct remote_state *remote_state,\n \tret->name = xstrndup(name, len);\n \trefspec_init_push(&ret->push, the_hash_algo);\n \trefspec_init_fetch(&ret->fetch, the_hash_algo);\n+\trefspec_init_fetch(&ret->refmap, the_hash_algo);\n \tstring_list_init_dup(&ret->server_options);\n \tstring_list_init_dup(&ret->negotiation_restrict);\n \tstring_list_init_dup(&ret->negotiation_include);\n@@ -176,6 +177,7 @@ static void remote_clear(struct remote *remote)\n \n \trefspec_clear(&remote->push);\n \trefspec_clear(&remote->fetch);\n+\trefspec_clear(&remote->refmap);\n \n \tfree((char *)remote->receivepack);\n \tfree((char *)remote->uploadpack);\n@@ -539,6 +541,12 @@ static int handle_config(const char *key, const char *value,\n \t\t\treturn -1;\n \t\trefspec_append(&remote->fetch, v);\n \t\tfree(v);\n+\t} else if (!strcmp(subkey, \"refmap\")) {\n+\t\tchar *v;\n+\t\tif (git_config_string(&v, key, value))\n+\t\t\treturn -1;\n+\t\trefspec_append(&remote->refmap, v);\n+\t\tfree(v);\n \t} else if (!strcmp(subkey, \"receivepack\")) {\n \t\tchar *v;\n \t\tif (git_config_string(&v, key, value))\n@@ -988,7 +996,9 @@ void ref_push_report_free(struct ref_push_report *report)\n \n int remote_find_tracking(struct remote *remote, struct refspec_item *refspec)\n {\n-\treturn refspec_find_match(&remote->fetch, refspec);\n+\tif (remote->fetch.nr)\n+\t\treturn refspec_find_match(&remote->fetch, refspec);\n+\treturn refspec_find_match(&remote->refmap, refspec);\n }\n \n static struct ref *alloc_ref_with_prefix(const char *prefix, size_t prefixlen,\ndiff --git a/remote.h b/remote.h\nindex cca02033b9..ac485a584d 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -90,6 +90,12 @@ struct remote {\n \n \tstruct refspec fetch;\n \n+\t/*\n+\t * How to map refs fetched without an explicit destination into our\n+\t * own namespace, the same as the --refmap command line option.\n+\t */\n+\tstruct refspec refmap;\n+\n \t/*\n \t * The setting for whether to fetch tags (as a separate rule from the\n \t * configured refspecs);\ndiff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh\nindex 300bd5396d..36fa9319e1 100755\n--- a/t/t5510-fetch.sh\n+++ b/t/t5510-fetch.sh\n@@ -927,6 +927,23 @@ test_expect_success 'explicit --refmap option overrides remote.*.fetch' '\n \t)\n '\n \n+test_expect_success 'remote.*.refmap acts like --refmap on the command line' '\n+\ttest_when_finished \"git -C three config --unset remote.origin.refmap\" &&\n+\tgit branch -f side &&\n+\tgit -C three config remote.origin.refmap \\\n+\t\t\"refs/heads/*:refs/remotes/other/*\" &&\n+\t(\n+\t\tcd three &&\n+\t\tgit update-ref refs/remotes/origin/main base-origin-main &&\n+\t\to=$(git rev-parse --verify refs/remotes/origin/main) &&\n+\t\tgit fetch origin main &&\n+\t\tn=$(git rev-parse --verify refs/remotes/origin/main) &&\n+\t\ttest \"$o\" = \"$n\" &&\n+\t\ttest_must_fail git rev-parse --verify refs/remotes/origin/side &&\n+\t\tgit rev-parse --verify refs/remotes/other/main\n+\t)\n+'\n+\n test_expect_success 'explicitly empty --refmap option disables remote.*.fetch' '\n \tgit branch -f side &&\n \t(\n-- \ngitgitgadget\n\n"},{"id":"554097","messageId":"74c177e3e22d140c7cd29b8e8a2a550944eb5cd2.1791102684.git.gitgitgadget@gmail.com","threadId":"66353","inReplyTo":"pull.2412.v6.git.git.1791102684.gitgitgadget@gmail.com","subject":"[PATCH v6 2/4] fetch: infer branches to fetch from a refmap-only remote","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-04T08:31:22Z","receivedAt":"2026-10-04T08:31:30Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nConfiguring remote.<name>.refmap without remote.<name>.fetch used to\nmake a refspec-less \"git fetch <name>\" fail with \"--refmap option is\nonly meaningful with command-line refspec(s)\", since a refmap only\nsays where to put fetched refs, not what to fetch.\n\nMake that case infer what to fetch: the local branches whose\n@{upstream} is already on that remote. This lets a remote be\nconfigured to fetch only the branches actually in use, without\nlisting them by hand in remote.<name>.fetch, and without needing to\ntouch the command line every time.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/config/remote.adoc |   5 +-\n Documentation/fetch-options.adoc |   6 +-\n builtin/fetch.c                  |  52 ++++++++++++--\n remote.c                         |  29 ++++++++\n remote.h                         |   3 +\n t/meson.build                    |   1 +\n t/t5586-fetch-refmap.sh          | 119 +++++++++++++++++++++++++++++++\n 7 files changed, 205 insertions(+), 10 deletions(-)\n create mode 100755 t/t5586-fetch-refmap.sh\n\ndiff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc\nindex 103eada406..7c52e443a5 100644\n--- a/Documentation/config/remote.adoc\n+++ b/Documentation/config/remote.adoc\n@@ -36,7 +36,10 @@ remote.<name>.fetch::\n remote.<name>.refmap::\n \tThe default value of the `--refmap` option for linkgit:git-fetch[1].\n \tUsed to map remote refs being fetched to remote-tracking refs to\n-\tstore. See the `--refmap` entry in linkgit:git-fetch[1].\n+\tstore. If `remote.<name>.fetch` is not set either, a refspec-less\n+\tfetch infers what to fetch from local branches built on this\n+\tremote, instead of fetching every branch it has. See the\n+\t`--refmap` entry in linkgit:git-fetch[1].\n \n remote.<name>.push::\n \tThe default set of \"refspec\" for linkgit:git-push[1]. See\ndiff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc\nindex 538914bc6e..315daf97d7 100644\n--- a/Documentation/fetch-options.adoc\n+++ b/Documentation/fetch-options.adoc\n@@ -245,8 +245,10 @@ endif::git-pull[]\n \tcommand-line arguments. See section on \"Configured Remote-tracking\n \tBranches\" for details.\n +\n-`remote.<name>.refmap` provides the default value for this option, the\n-same way `remote.<name>.fetch` provides the default refspecs to fetch.\n+When a refmap is active (from `--refmap` or `remote.<name>.refmap`) but\n+there is nothing to fetch, neither on the command line nor from\n+`remote.<name>.fetch`, Git infers what to fetch from the local branches\n+whose `@{upstream}` is on that remote.\n \n `-t`::\n `--tags`::\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 04d0a78ecf..e928f5d7e0 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -511,6 +511,8 @@ static struct ref *get_ref_map(struct remote *remote,\n \tstruct ref **tail = &ref_map;\n \tstruct refspec *effective_refmap =\n \t\trefmap.nr ? &refmap : remote ? &remote->refmap : NULL;\n+\tstruct refspec inferred_rs;\n+\tint inferred_branches = 0;\n \n \t/* opportunistically-updated references: */\n \tstruct ref *orefs = NULL, **oref_tail = &orefs;\n@@ -518,15 +520,32 @@ static struct ref *get_ref_map(struct remote *remote,\n \tstruct hashmap existing_refs;\n \tint existing_refs_populated = 0;\n \n+\trefspec_init_fetch(&inferred_rs, the_hash_algo);\n+\n \tfilter_prefetch_refspec(rs);\n \tif (remote)\n \t\tfilter_prefetch_refspec(&remote->fetch);\n \n+\tif (!rs->nr && remote && !remote->fetch.nr &&\n+\t    effective_refmap && effective_refmap->nr) {\n+\t\tstruct string_list tracked = STRING_LIST_INIT_DUP;\n+\t\tstruct string_list_item *item;\n+\n+\t\tbranches_tracking_remote(remote, &tracked);\n+\t\tfor_each_string_list_item(item, &tracked)\n+\t\t\trefspec_append(&inferred_rs, item->string);\n+\t\tstring_list_clear(&tracked, 0);\n+\n+\t\trs = &inferred_rs;\n+\t\tinferred_branches = 1;\n+\t}\n+\n \tif (rs->nr) {\n \t\tstruct refspec *fetch_refspec;\n \n \t\tfor (i = 0; i < rs->nr; i++) {\n-\t\t\tget_fetch_map(remote_refs, &rs->items[i], &tail, 0);\n+\t\t\tget_fetch_map(remote_refs, &rs->items[i], &tail,\n+\t\t\t\t      inferred_branches);\n \t\t\tif (rs->items[i].dst && rs->items[i].dst[0])\n \t\t\t\t*autotags = 1;\n \t\t}\n@@ -561,6 +580,8 @@ static struct ref *get_ref_map(struct remote *remote,\n \n \t\tfor (i = 0; i < fetch_refspec->nr; i++)\n \t\t\tget_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1);\n+\t} else if (inferred_branches) {\n+\t\t/* Already fully handled above. */\n \t} else if (effective_refmap && effective_refmap->nr) {\n \t\tdie(\"--refmap option is only meaningful with command-line refspec(s)\");\n \t} else {\n@@ -656,6 +677,7 @@ static struct ref *get_ref_map(struct remote *remote,\n \tif (existing_refs_populated)\n \t\thashmap_clear_and_free(&existing_refs, struct refname_hash_entry, ent);\n \n+\trefspec_clear(&inferred_rs);\n \treturn ref_map;\n }\n \n@@ -1960,15 +1982,30 @@ static int do_fetch(struct transport *transport,\n \t\trefspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);\n \t} else {\n \t\tstruct branch *branch = branch_get(NULL);\n-\n-\t\tif (transport->remote->fetch.nr) {\n+\t\tint tracks_this_remote = branch && branch_has_merge_config(branch) &&\n+\t\t\t!strcmp(branch->remote_name, transport->remote->name);\n+\t\tstruct refspec *effective_refmap = refmap.nr ? &refmap :\n+\t\t\t&transport->remote->refmap;\n+\t\tint inferred_branches = !transport->remote->fetch.nr &&\n+\t\t\teffective_refmap->nr;\n+\n+\t\tif (inferred_branches) {\n+\t\t\tstruct string_list tracked = STRING_LIST_INIT_DUP;\n+\t\t\tstruct string_list_item *item;\n+\n+\t\t\tbranches_tracking_remote(transport->remote, &tracked);\n+\t\t\tfor_each_string_list_item(item, &tracked)\n+\t\t\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n+\t\t\t\t\t    item->string);\n+\t\t\tstring_list_clear(&tracked, 0);\n+\t\t} else if (transport->remote->fetch.nr) {\n \t\t\trefspec_ref_prefixes(&transport->remote->fetch,\n \t\t\t\t\t     &transport_ls_refs_options.ref_prefixes);\n-\t\t\tif (follow_remote_head != FOLLOW_REMOTE_NEVER)\n-\t\t\t\tdo_set_head = 1;\n \t\t}\n-\t\tif (branch && branch_has_merge_config(branch) &&\n-\t\t    !strcmp(branch->remote_name, transport->remote->name)) {\n+\t\tif ((transport->remote->fetch.nr || inferred_branches) &&\n+\t\t    follow_remote_head != FOLLOW_REMOTE_NEVER)\n+\t\t\tdo_set_head = 1;\n+\t\tif (tracks_this_remote) {\n \t\t\tint i;\n \t\t\tfor (i = 0; i < branch->merge_nr; i++) {\n \t\t\t\tstrvec_push(&transport_ls_refs_options.ref_prefixes,\n@@ -2009,6 +2046,7 @@ static int do_fetch(struct transport *transport,\n \n \tref_map = get_ref_map(transport->remote, remote_refs, rs,\n \t\t\t      tags, &autotags);\n+\n \tif (!update_head_ok)\n \t\tcheck_not_current_branch(ref_map);\n \ndiff --git a/remote.c b/remote.c\nindex 017cd9d13e..a5deb9b929 100644\n--- a/remote.c\n+++ b/remote.c\n@@ -1884,6 +1884,35 @@ int branch_merge_matches(struct branch *branch,\n \treturn refname_match(branch->merge[i]->src, refname);\n }\n \n+struct branches_tracking_remote_cb_data {\n+\tstruct remote *remote;\n+\tstruct string_list *tracked;\n+};\n+\n+static int add_if_tracking_remote(const struct reference *ref, void *cb_data)\n+{\n+\tstruct branches_tracking_remote_cb_data *data = cb_data;\n+\tstruct branch *branch;\n+\n+\tbranch = branch_get(ref->name);\n+\tif (!branch_has_merge_config(branch) ||\n+\t    strcmp(branch->remote_name, data->remote->name))\n+\t\treturn 0;\n+\n+\tfor (int i = 0; i < branch->merge_nr; i++)\n+\t\tstring_list_insert(data->tracked, branch->merge[i]->src);\n+\n+\treturn 0;\n+}\n+\n+void branches_tracking_remote(struct remote *remote, struct string_list *tracked)\n+{\n+\tstruct branches_tracking_remote_cb_data data = { remote, tracked };\n+\n+\trefs_for_each_branch_ref(get_main_ref_store(the_repository),\n+\t\t\t\t  add_if_tracking_remote, &data);\n+}\n+\n __attribute__((format (printf,2,3)))\n static char *error_buf(struct strbuf *err, const char *fmt, ...)\n {\ndiff --git a/remote.h b/remote.h\nindex ac485a584d..da6c3ef53a 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -359,6 +359,9 @@ int branch_has_merge_config(struct branch *branch);\n \n int branch_merge_matches(struct branch *, int n, const char *);\n \n+/* fills tracked with the refname of every local branch's upstream on remote */\n+void branches_tracking_remote(struct remote *remote, struct string_list *tracked);\n+\n /* list of the remote in a group as configured */\n struct remote_group_data {\n \tconst char *name;\ndiff --git a/t/meson.build b/t/meson.build\nindex f65eb04684..e287a6b947 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -730,6 +730,7 @@ integration_tests = [\n   't5582-fetch-negative-refspec.sh',\n   't5583-push-branches.sh',\n   't5584-http-429-retry.sh',\n+  't5586-fetch-refmap.sh',\n   't5600-clone-fail-cleanup.sh',\n   't5601-clone.sh',\n   't5602-clone-remote-exec.sh',\ndiff --git a/t/t5586-fetch-refmap.sh b/t/t5586-fetch-refmap.sh\nnew file mode 100755\nindex 0000000000..b81fc48cbe\n--- /dev/null\n+++ b/t/t5586-fetch-refmap.sh\n@@ -0,0 +1,119 @@\n+#!/bin/sh\n+\n+test_description='\"git fetch\" with a remote.<name>.refmap but no remote.<name>.fetch\n+\n+When a remote has a refmap configured but no fetch refspec, a\n+refspec-less fetch infers what to fetch from the local branches whose\n+@{upstream} is on that remote.\n+'\n+\n+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main\n+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup' '\n+\ttest_commit main-1 &&\n+\ttest_commit main-2 &&\n+\tgit checkout -b side main-1 &&\n+\ttest_commit side-1 &&\n+\tgit checkout -b next main-1 &&\n+\ttest_commit next-1 &&\n+\tgit checkout main\n+'\n+\n+test_expect_success 'clone shallow and single-branch, then add a second remote' '\n+\tgit clone --no-local --depth=1 --branch main --single-branch . client &&\n+\t(\n+\t\tcd client &&\n+\t\tgit remote add upstream .. &&\n+\t\ttest_might_fail git config unset remote.upstream.fetch &&\n+\t\tgit config remote.upstream.refmap \\\n+\t\t\t\"+refs/heads/*:refs/remotes/upstream/*\"\n+\t)\n+'\n+\n+test_expect_success 'a bare fetch needs nothing until a branch is tracked' '\n+\t(\n+\t\tcd client &&\n+\t\tgit fetch upstream &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n+\t\ttest_must_be_empty actual\n+\t)\n+'\n+\n+test_expect_success 'an explicit one-time fetch lets a branch be tracked' '\n+\t(\n+\t\tcd client &&\n+\t\tgit fetch upstream main &&\n+\t\tgit branch --set-upstream-to=upstream/main &&\n+\t\ttest_cmp_config upstream branch.main.remote &&\n+\t\ttest_cmp_config refs/heads/main branch.main.merge\n+\t)\n+'\n+\n+test_expect_success 'a branch checked out from a one-time fetch is kept updated by later plain fetches' '\n+\t(\n+\t\tcd client &&\n+\t\tgit fetch upstream side:refs/remotes/upstream/side &&\n+\t\tgit branch side-topic upstream/side\n+\t) &&\n+\tgit checkout side &&\n+\ttest_commit side-2 &&\n+\tgit checkout main &&\n+\t(\n+\t\tcd client &&\n+\t\tgit fetch upstream &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\trefs/remotes/upstream/side\n+\t\tEOF\n+\t\ttest_cmp expect actual &&\n+\t\tgit rev-parse refs/remotes/upstream/side >actual-oid &&\n+\t\tgit -C .. rev-parse side >expect-oid &&\n+\t\ttest_cmp expect-oid actual-oid\n+\t)\n+'\n+\n+test_expect_success 'a second branch tracking the same upstream branch does not fetch it twice' '\n+\t(\n+\t\tcd client &&\n+\t\tgit branch side-topic-2 upstream/side &&\n+\t\tgit fetch upstream &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\trefs/remotes/upstream/side\n+\t\tEOF\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'a branch tracking a different remote is not fetched from upstream' '\n+\t(\n+\t\tcd client &&\n+\t\tgit remote add other .. &&\n+\t\tgit fetch other next:refs/remotes/other/next &&\n+\t\tgit branch next-topic other/next &&\n+\t\tgit fetch upstream &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\trefs/remotes/upstream/side\n+\t\tEOF\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'git remote show does not choke on a refmap-only remote' '\n+\t(\n+\t\tcd client &&\n+\t\tgit remote show upstream\n+\t)\n+'\n+\n+test_done\n-- \ngitgitgadget\n\n"},{"id":"554099","messageId":"3abcc8915b4aa199d95a4eeb39a57de2fc491d3d.1791102684.git.gitgitgadget@gmail.com","threadId":"66353","inReplyTo":"pull.2412.v6.git.git.1791102684.gitgitgadget@gmail.com","subject":"[PATCH v6 3/4] remote: add \"git remote add --limited-fetch\"","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-04T08:31:23Z","receivedAt":"2026-10-04T08:31:32Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nA remote added the ordinary way tracks every branch it has, via a\nwildcard remote.<name>.fetch refspec. That is wasteful for a remote\nwhose history is only worth following for the branches actually in\nuse locally, and it can make \"git fetch\" negotiate history for\nbranches nobody asked for.\n\nGive \"git remote add\" a --limited-fetch option that sets up\nremote.<name>.refmap instead of remote.<name>.fetch, so that\n\"git fetch <name>\" only fetches the branches already tracked, as\ndescribed in the previous commit. It is rejected together with\n-t/--track or --mirror, since those already say explicitly what to\nfetch.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/git-remote.adoc |  8 +++++++-\n builtin/remote.c              | 28 ++++++++++++++++++++++------\n t/t5505-remote.sh             | 16 ++++++++++++++++\n 3 files changed, 45 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/git-remote.adoc b/Documentation/git-remote.adoc\nindex eaae30aa88..4255f8b3e6 100644\n--- a/Documentation/git-remote.adoc\n+++ b/Documentation/git-remote.adoc\n@@ -10,7 +10,7 @@ SYNOPSIS\n --------\n [synopsis]\n git remote [-v | --verbose]\n-git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] <name> <URL>\n+git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=(fetch|push)] [--[no-]limited-fetch] <name> <URL>\n git remote rename [--[no-]progress] <old> <new>\n git remote remove <name>\n git remote set-head <name> (-a | --auto | -d | --delete | <branch>)\n@@ -70,6 +70,12 @@ the `refs/remotes/<name>/` namespace, a refspec to track only _<branch>_\n is created.  You can give more than one `-t <branch>` to track\n multiple branches without grabbing all branches.\n +\n+With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec\n+that tracks all branches, `remote.<name>.refmap` is set up so that a\n+refspec-less `git fetch <name>` only fetches branches our local branches\n+are built on. See the `--refmap` entry in linkgit:git-fetch[1] for\n+details.\n++\n With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set\n up to point at remote's _<master>_ branch. See also the set-head command.\n +\ndiff --git a/builtin/remote.c b/builtin/remote.c\nindex de989ea3ba..f036dd5d6f 100644\n--- a/builtin/remote.c\n+++ b/builtin/remote.c\n@@ -179,6 +179,7 @@ static int add(int argc, const char **argv, const char *prefix,\n {\n \tint fetch = 0, fetch_tags = TAGS_DEFAULT;\n \tunsigned mirror = MIRROR_NONE;\n+\tint limited_fetch = -1; /* unspecified */\n \tstruct string_list track = STRING_LIST_INIT_NODUP;\n \tconst char *master = NULL;\n \tstruct remote *remote;\n@@ -198,6 +199,8 @@ static int add(int argc, const char **argv, const char *prefix,\n \t\tOPT_CALLBACK_F(0, \"mirror\", &mirror, \"(push|fetch)\",\n \t\t\tN_(\"set up remote as a mirror to push to or fetch from\"),\n \t\t\tPARSE_OPT_OPTARG | PARSE_OPT_COMP_ARG, parse_mirror_opt),\n+\t\tOPT_BOOL(0, \"limited-fetch\", &limited_fetch,\n+\t\t\tN_(\"fetch only the branches we build on, instead of every branch\")),\n \t\tOPT_END()\n \t};\n \n@@ -211,6 +214,10 @@ static int add(int argc, const char **argv, const char *prefix,\n \t\tdie(_(\"specifying a master branch makes no sense with --mirror\"));\n \tif (mirror && !(mirror & MIRROR_FETCH) && track.nr)\n \t\tdie(_(\"specifying branches to track makes sense only with fetch mirrors\"));\n+\tif (limited_fetch == 1 && track.nr)\n+\t\tdie(_(\"--limited-fetch does not make sense with -t/--track\"));\n+\tif (limited_fetch == 1 && mirror)\n+\t\tdie(_(\"--limited-fetch does not make sense with --mirror\"));\n \n \tname = argv[0];\n \turl = argv[1];\n@@ -230,13 +237,22 @@ static int add(int argc, const char **argv, const char *prefix,\n \trepo_config_set(the_repository, buf.buf, url);\n \n \tif (!mirror || mirror & MIRROR_FETCH) {\n+\t\tint use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&\n+\t\t\tlimited_fetch == 1;\n+\n \t\tstrbuf_reset(&buf);\n-\t\tstrbuf_addf(&buf, \"remote.%s.fetch\", name);\n-\t\tif (track.nr == 0)\n-\t\t\tstring_list_append(&track, \"*\");\n-\t\tfor (size_t i = 0; i < track.nr; i++) {\n-\t\t\tadd_branch(buf.buf, track.items[i].string,\n-\t\t\t\t   name, mirror, &buf2);\n+\t\tif (use_limited_fetch) {\n+\t\t\tstrbuf_addf(&buf, \"remote.%s.refmap\", name);\n+\t\t\tstrbuf_reset(&buf2);\n+\t\t\tstrbuf_addf(&buf2, \"+refs/heads/*:refs/remotes/%s/*\", name);\n+\t\t\trepo_config_set(the_repository, buf.buf, buf2.buf);\n+\t\t} else {\n+\t\t\tstrbuf_addf(&buf, \"remote.%s.fetch\", name);\n+\t\t\tif (track.nr == 0)\n+\t\t\t\tstring_list_append(&track, \"*\");\n+\t\t\tfor (size_t i = 0; i < track.nr; i++)\n+\t\t\t\tadd_branch(buf.buf, track.items[i].string,\n+\t\t\t\t\t   name, mirror, &buf2);\n \t\t}\n \t}\n \ndiff --git a/t/t5505-remote.sh b/t/t5505-remote.sh\nindex 5fbfcb0848..0168d5abfe 100755\n--- a/t/t5505-remote.sh\n+++ b/t/t5505-remote.sh\n@@ -137,6 +137,22 @@ test_expect_success 'filters are listed by git remote -v only' '\n \ttest_grep ! \"\\[blob:none\\]\" out\n '\n \n+test_expect_success '--limited-fetch works in a full repository too' '\n+\ttest_when_finished \"rm -rf full-add\" &&\n+\tgit clone --no-local one full-add &&\n+\t(\n+\t\tcd full-add &&\n+\t\tgit remote add --limited-fetch upstream ../two &&\n+\t\ttest_cmp_config \"+refs/heads/*:refs/remotes/upstream/*\" \\\n+\t\t\tremote.upstream.refmap &&\n+\t\ttest_must_fail git config get remote.upstream.fetch\n+\t)\n+'\n+\n+test_expect_success '--limited-fetch conflicts with -t' '\n+\ttest_must_fail git remote add --limited-fetch -t main upstream ../two\n+'\n+\n test_expect_success 'check remote-tracking' '\n \t(\n \t\tcd test &&\n-- \ngitgitgadget\n\n"},{"id":"554098","messageId":"84d192445c2636b618e86e632ee8439574aa0058.1791102684.git.gitgitgadget@gmail.com","threadId":"66353","inReplyTo":"pull.2412.v6.git.git.1791102684.gitgitgadget@gmail.com","subject":"[PATCH v6 4/4] remote: default to --limited-fetch in a shallow repository","fromName":"Harald Nordgren via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2026-10-04T08:31:24Z","receivedAt":"2026-10-04T08:31:33Z","isPatch":true,"body":"From: Harald Nordgren <haraldnordgren@gmail.com>\n\nAdding a second remote to a shallow, single-branch clone used to\nstill fetch every branch that remote has, since \"git remote add\"\nalways set up a wildcard remote.<name>.fetch refspec regardless of\nhow shallow the repository already was. That defeats the purpose of\nhaving cloned shallow and single-branch in the first place, and can\nmake a plain \"git fetch\" on that remote hang or take a very long\ntime on a repository with many branches.\n\nTurn --limited-fetch on by default when the repository is already\nshallow and neither -t/--track nor --mirror was given, so that\nadding a remote there does not by itself commit to following every\nbranch it has. --no-limited-fetch keeps the previous behavior for\nwhoever wants it.\n\nSigned-off-by: Harald Nordgren <haraldnordgren@gmail.com>\n---\n Documentation/git-remote.adoc |  8 ++++-\n builtin/remote.c              |  5 ++-\n t/t5505-remote.sh             | 60 +++++++++++++++++++++++++++++++++++\n 3 files changed, 71 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/git-remote.adoc b/Documentation/git-remote.adoc\nindex 4255f8b3e6..80f8a4a183 100644\n--- a/Documentation/git-remote.adoc\n+++ b/Documentation/git-remote.adoc\n@@ -52,6 +52,11 @@ Add a remote named _<name>_ for the repository at\n _<URL>_.  The command `git fetch <name>` can then be used to create and\n update remote-tracking branches `<name>/<branch>`.\n +\n+If the repository is already a shallow repository (see linkgit:git-clone[1]\n+`--depth`) and neither `-t`, `--mirror` nor `--no-limited-fetch` is given,\n+`--limited-fetch` is turned on by default, so that `git fetch <name>` does\n+not need to negotiate history for every branch the remote has.\n++\n With `-f` option, `git fetch <name>` is run immediately after\n the remote information is set up.\n +\n@@ -74,7 +79,8 @@ With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec\n that tracks all branches, `remote.<name>.refmap` is set up so that a\n refspec-less `git fetch <name>` only fetches branches our local branches\n are built on. See the `--refmap` entry in linkgit:git-fetch[1] for\n-details.\n+details. `--no-limited-fetch` explicitly disables this, overriding the\n+shallow-repository default described above.\n +\n With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set\n up to point at remote's _<master>_ branch. See also the set-head command.\ndiff --git a/builtin/remote.c b/builtin/remote.c\nindex f036dd5d6f..0cf1126976 100644\n--- a/builtin/remote.c\n+++ b/builtin/remote.c\n@@ -16,6 +16,7 @@\n #include \"rebase.h\"\n #include \"refs.h\"\n #include \"refspec.h\"\n+#include \"shallow.h\"\n #include \"odb.h\"\n #include \"strvec.h\"\n #include \"commit-reach.h\"\n@@ -238,7 +239,9 @@ static int add(int argc, const char **argv, const char *prefix,\n \n \tif (!mirror || mirror & MIRROR_FETCH) {\n \t\tint use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 &&\n-\t\t\tlimited_fetch == 1;\n+\t\t\t(limited_fetch == 1 ||\n+\t\t\t (limited_fetch == -1 &&\n+\t\t\t  is_repository_shallow(the_repository)));\n \n \t\tstrbuf_reset(&buf);\n \t\tif (use_limited_fetch) {\ndiff --git a/t/t5505-remote.sh b/t/t5505-remote.sh\nindex 0168d5abfe..f3b5905e9e 100755\n--- a/t/t5505-remote.sh\n+++ b/t/t5505-remote.sh\n@@ -137,6 +137,66 @@ test_expect_success 'filters are listed by git remote -v only' '\n \ttest_grep ! \"\\[blob:none\\]\" out\n '\n \n+test_expect_success 'add remote -t keeps an explicit refspec in a shallow repository' '\n+\ttest_when_finished \"rm -rf shallow-add\" &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\tone shallow-add &&\n+\t(\n+\t\tcd shallow-add &&\n+\t\tgit remote add -t main upstream ../two &&\n+\t\ttest_cmp_config \"+refs/heads/main:refs/remotes/upstream/main\" \\\n+\t\t\tremote.upstream.fetch\n+\t)\n+'\n+\n+test_expect_success 'add remote keeps the wildcard refspec in a full repository' '\n+\ttest_when_finished \"rm -rf full-add\" &&\n+\tgit clone --no-local one full-add &&\n+\t(\n+\t\tcd full-add &&\n+\t\tgit remote add upstream ../two &&\n+\t\ttest_cmp_config \"+refs/heads/*:refs/remotes/upstream/*\" \\\n+\t\t\tremote.upstream.fetch\n+\t)\n+'\n+\n+test_expect_success 'a remote added in a shallow repository defaults to --limited-fetch' '\n+\ttest_when_finished \"rm -rf shallow-add\" &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\tone shallow-add &&\n+\t(\n+\t\tcd shallow-add &&\n+\t\tgit remote add upstream ../two &&\n+\t\ttest_cmp_config \"+refs/heads/*:refs/remotes/upstream/*\" \\\n+\t\t\tremote.upstream.refmap &&\n+\t\ttest_must_fail git config get remote.upstream.fetch &&\n+\t\tgit fetch upstream main &&\n+\t\tgit branch --set-upstream-to=upstream/main &&\n+\t\ttest_cmp_config upstream branch.main.remote &&\n+\t\ttest_cmp_config refs/heads/main branch.main.merge &&\n+\t\tgit fetch upstream &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/upstream >actual &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\trefs/remotes/upstream/HEAD\n+\t\trefs/remotes/upstream/main\n+\t\tEOF\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '--no-limited-fetch overrides the shallow-repository default' '\n+\ttest_when_finished \"rm -rf shallow-add\" &&\n+\tgit clone --no-local --depth=1 --branch main --single-branch \\\n+\t\tone shallow-add &&\n+\t(\n+\t\tcd shallow-add &&\n+\t\tgit remote add --no-limited-fetch upstream ../two &&\n+\t\ttest_cmp_config \"+refs/heads/*:refs/remotes/upstream/*\" \\\n+\t\t\tremote.upstream.fetch &&\n+\t\ttest_must_fail git config get remote.upstream.refmap\n+\t)\n+'\n+\n test_expect_success '--limited-fetch works in a full repository too' '\n \ttest_when_finished \"rm -rf full-add\" &&\n \tgit clone --no-local one full-add &&\n-- \ngitgitgadget\n"},{"id":"554119","messageId":"xmqqv77hs7ut.fsf@gitster.g","threadId":"66353","inReplyTo":"pull.2412.v6.git.git.1791102684.gitgitgadget@gmail.com","subject":"Re: [PATCH v6 0/4] fetch: avoid fetching every branch of a new remote in a shallow repo","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-10-04T17:17:46Z","receivedAt":"2026-10-04T17:17:49Z","isPatch":true,"body":"\"Harald Nordgren via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> Avoid fetching every branch of a new remote in a shallow repo.\n>\n> Changes in v6:\n>\n>  * Remove leftover reference to deleted default-branch logic in commit\n>    message.\n\nThanks.  I think this is becoming much better, but I see one glitch\nand one design question, for which I do not yet know the right\nanswer.\n\nBefore going there, since one of the test scripts added by this\nseries is called 'fetch refmap', we should have a test or two to\ncheck its more basic use.\n\nWhen the user configures remote.origin.refmap, the command should\nbehave as if --refmap were given on the command line, even when the\nrepository does not yet have a local branch that builds on anything\nfrom the remote.  Attached is my attempt to do so.  It does multiple\nthings in a single block, which we may want to split up, but I am\nsending it here to illustrate what we might want to test and, more\nimportantly, to present a scenario that exposes both the design\nquestion and the glitch.\n\nThe early part of the scenario goes like this:\n\n * We create a new repository and add \"..\" as a remote.\n * We remove remote.origin.fetch and set up remote.origin.refmap.\n * When we run \"git fetch origin\", nothing is fetched because\n   nothing yet builds on what we would fetch from them.\n\nIf you try to run this with [1/4] alone, however, it errors out with\n\"fatal: --refmap option is only meaningful with command-line\nrefspec\", which is suboptimal when triggered by a configuration\nvariable.  Even though our design says that remote.*.refmap makes\nthe command behave as if the user gave '--refmap' on the command\nline, applying that rule here is a bit too strict.\n\nFortunately, this is rectified later in the series when we begin\ntracking which of our local branches build on what we get from them.\nEven when the number of branches to fetch is zero, meaning we should\npretend no command-line refspec was given with --refmap, we no\nlonger get the same error, which is good.\n\nThe second part of the scenario explicitly specifies what to fetch\non the command line and verifies that we fetch exactly that.\n\nThen there is the last part, where the desired behavior is unclear.\nWhat should happen if the remote.origin.* configuration defines both\nfetch and refmap?  How would we explain our choice to the users?  I\ndo not have a good answer to this design question.\n\nAs for the glitch, the last part of the test below dies with the\n\"fatal: --refmap option is only meaningful...\" message when run with\nthe current patchset.  We might decide to error out if both are set.\nAlternatively, we could ignore .refmap and use .fetch, or ignore\n.fetch and use .refmap.  Whatever we decide, the \"fatal: --refmap\noption is only meaningful...\" error is not the right message to show\nin this situation.\n\nThoughts?\n\n\n t/t5586-fetch-refmap.sh | 29 +++++++++++++++++++++++++++++\n 1 file changed, 29 insertions(+)\n\ndiff --git c/t/t5586-fetch-refmap.sh w/t/t5586-fetch-refmap.sh\nindex b81fc48cbe..30a8d79c16 100755\n--- c/t/t5586-fetch-refmap.sh\n+++ w/t/t5586-fetch-refmap.sh\n@@ -22,6 +22,35 @@ test_expect_success 'setup' '\n \tgit checkout main\n '\n \n+test_expect_success 'remote.<name>.refmap without tracking (baseline)' '\n+\ttest_when_finished \"rm -fr fetch-refmap-baseline\" &&\n+\tgit init fetch-refmap-baseline &&\n+\t(\n+\t\tcd fetch-refmap-baseline &&\n+\t\tgit remote add origin ../ &&\n+\n+\t\t# without fetch refspec, but with fetch refmap\n+\t\tgit config --unset-all remote.origin.fetch &&\n+\t\tgit config remote.origin.refmap \"+refs/heads/*:refs/remotes/origin/*\" &&\n+\n+\t\t# nothing tracked, nothing fetched, no error\n+\t\tgit fetch origin 2>error &&\n+\t\ttest_grep ! \"fatal: --refmap option is only meaningful\" error &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/ >actual &&\n+\t\ttest_line_count = 0 actual &&\n+\n+\t\t# nothing tracked, explicit ref on the command line\n+\t\tgit fetch origin main &&\n+\t\tgit for-each-ref --format=\"%(refname)\" refs/remotes/ >actual &&\n+\t\techo refs/remotes/origin/main >expect &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\t# what should happen when we have both refmap and refspec?\n+\t\tgit config remote.origin.fetch \"+refs/heads/*:refs/remotes/origin/*\" &&\n+\t\tgit fetch origin\n+\t)\n+'\n+\n test_expect_success 'clone shallow and single-branch, then add a second remote' '\n \tgit clone --no-local --depth=1 --branch main --single-branch . client &&\n \t(\n"},{"id":"554128","messageId":"CAHwyqnXz+acRBytu9tWL+RzsKnzTZyZCnzzQQhEFjvWT5cgoww@mail.gmail.com","threadId":"66353","inReplyTo":"xmqqv77hs7ut.fsf@gitster.g","subject":"Re: [PATCH v6 0/4] fetch: avoid fetching every branch of a new remote in a shallow repo","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-10-04T19:51:01Z","receivedAt":"2026-10-04T19:51:40Z","isPatch":true,"body":"> If you try to run this with [1/4] alone, however, it errors out with\n> \"fatal: --refmap option is only meaningful with command-line\n> refspec\", which is suboptimal when triggered by a configuration\n> variable.  Even though our design says that remote.*.refmap makes\n> the command behave as if the user gave '--refmap' on the command\n> line, applying that rule here is a bit too strict.\n\nThanks for pointing that out. How did you find that?\n\n> Then there is the last part, where the desired behavior is unclear.\n> What should happen if the remote.origin.* configuration defines both\n> fetch and refmap?  How would we explain our choice to the users?  I\n> do not have a good answer to this design question.\n\nI think that when both `remote.<name>.fetch` and\n`remote.<name>.refmap` are configured, remote.<name>.fetch can win.\n\n> +test_expect_success 'remote.<name>.refmap without tracking (baseline)' '\n> +       test_when_finished \"rm -fr fetch-refmap-baseline\" &&\n> +       git init fetch-refmap-baseline &&\n> +       (\n> +               cd fetch-refmap-baseline &&\n> +               git remote add origin ../ &&\n> +\n> +               # without fetch refspec, but with fetch refmap\n> +               git config --unset-all remote.origin.fetch &&\n> +               git config remote.origin.refmap \"+refs/heads/*:refs/remotes/origin/*\" &&\n> +\n> +               # nothing tracked, nothing fetched, no error\n> +               git fetch origin 2>error &&\n> +               test_grep ! \"fatal: --refmap option is only meaningful\" error &&\n> +               git for-each-ref --format=\"%(refname)\" refs/remotes/ >actual &&\n> +               test_line_count = 0 actual &&\n> +\n> +               # nothing tracked, explicit ref on the command line\n> +               git fetch origin main &&\n> +               git for-each-ref --format=\"%(refname)\" refs/remotes/ >actual &&\n> +               echo refs/remotes/origin/main >expect &&\n> +               test_cmp expect actual &&\n> +\n> +               # what should happen when we have both refmap and refspec?\n> +               git config remote.origin.fetch \"+refs/heads/*:refs/remotes/origin/*\" &&\n> +               git fetch origin\n> +       )\n\nThanks for providing this!\n\n\n\nHarald\n"},{"id":"554167","messageId":"xmqqv77gnxyg.fsf@gitster.g","threadId":"66353","inReplyTo":"CAHwyqnXz+acRBytu9tWL+RzsKnzTZyZCnzzQQhEFjvWT5cgoww@mail.gmail.com","subject":"Re: [PATCH v6 0/4] fetch: avoid fetching every branch of a new remote in a shallow repo","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-10-05T12:17:27Z","receivedAt":"2026-10-05T12:17:30Z","isPatch":true,"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n>> If you try to run this with [1/4] alone, however, it errors out with\n>> \"fatal: --refmap option is only meaningful with command-line\n>> refspec\", which is suboptimal when triggered by a configuration\n>> variable.  Even though our design says that remote.*.refmap makes\n>> the command behave as if the user gave '--refmap' on the command\n>> line, applying that rule here is a bit too strict.\n>\n> Thanks for pointing that out. How did you find that?\n\nBy thinking the process through while writing these very basic\ntests.\n\nI think this topic textually interacts with Collin's followRemoteHAD\nwork.  I tried to be careful while resolving the conflicts when I\nmerged both to 'seen', but please double check the result when I\npush it out.\n"},{"id":"554207","messageId":"CAHwyqnXSmQ_E4u6S_dTVHe82E6ErqQAHpOUDcUZBGyLRbne-Pg@mail.gmail.com","threadId":"66353","inReplyTo":"xmqqv77gnxyg.fsf@gitster.g","subject":"Re: [PATCH v6 0/4] fetch: avoid fetching every branch of a new remote in a shallow repo","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-10-05T18:09:30Z","receivedAt":"2026-10-05T18:09:30Z","isPatch":true,"body":"> I think this topic textually interacts with Collin's followRemoteHAD\n> work.  I tried to be careful while resolving the conflicts when I\n> merged both to 'seen', but please double check the result when I\n> push it out.\n\nI range-diffed against 'seen' and everything looks normal.\n\nI have a new version ready to send out, do you want me to hold off?\n\n\nHarald\n\n"}]}