From: "Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Phillip Wood <phillip.wood123@gmail.com>,
"D. Ben Knoble" <ben.knoble@gmail.com>,
Harald Nordgren <haraldnordgren@gmail.com>
Subject: [PATCH v4 0/4] fetch: avoid fetching every branch of a new remote in a shallow repo
Date: Tue, 29 Sep 2026 09:19:54 +0000 [thread overview]
Message-ID: <pull.2412.v4.git.git.1790673598.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2412.git.git.1789829246437.gitgitgadget@gmail.com>
Avoid fetching every branch of a new remote in a shallow repo.
Changes in v4:
* Removed the automatic default-branch fetch. A fresh remote fetches
nothing until you track a branch explicitly.
* Fixed fetch report showing "new ref HEAD" instead of "new branch "
* Reworded remote..refmap docs and commit message.
Changes in v3:
* Replace the special ":"/"+:" fetch refspec with remote.<name>.refmap,
reusing git's existing --refmap mechanism instead of inventing new
refspec syntax.
* Split the change into 4 commits.
Changes in v2:
* Replaced the opt-in fetch.shallow config entirely with a new special
fetch refspec (+:) that git remote add now defaults new remotes to in a
shallow repository. The new refspec fetches whichever branches any local
branch tracks at that remote, plus the remote's default branch.
Harald Nordgren (4):
fetch: add remote.<name>.refmap
fetch: infer branches to fetch from a refmap-only remote
remote: add "git remote add --limited-fetch"
remote: default to --limited-fetch in a shallow repository
Documentation/config/remote.adoc | 8 +++
Documentation/fetch-options.adoc | 5 ++
Documentation/git-remote.adoc | 14 +++-
builtin/fetch.c | 60 +++++++++++++---
builtin/remote.c | 31 ++++++--
remote.c | 41 ++++++++++-
remote.h | 9 +++
t/meson.build | 1 +
t/t5505-remote.sh | 76 ++++++++++++++++++++
t/t5510-fetch.sh | 17 +++++
t/t5585-fetch-refmap.sh | 119 +++++++++++++++++++++++++++++++
11 files changed, 363 insertions(+), 18 deletions(-)
create mode 100755 t/t5585-fetch-refmap.sh
base-commit: a018953688f1b10bddf91bff8747068f5f4746a4
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2412%2FHaraldNordgren%2Ffetch-shallow-narrow-refspec-v4
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2412/HaraldNordgren/fetch-shallow-narrow-refspec-v4
Pull-Request: https://github.com/git/git/pull/2412
Range-diff vs v3:
1: b04c00b974 ! 1: d48a7004e4 fetch: add remote.<name>.refmap
@@ Commit message
Add a per-remote config variable, remote.<name>.refmap, that provides
the default value for --refmap the same way remote.<name>.fetch
- already provides the default refspecs to fetch. It only takes effect
- when there is something explicit to fetch, on the command line or via
- remote.<name>.fetch, matching how --refmap itself already behaves.
+ already provides the default refspecs to fetch. Like --refmap itself,
+ it only maps refs that are actually being fetched, so it has nothing
+ to do when there is nothing explicit to fetch, on the command line or
+ via remote.<name>.fetch.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
@@ Documentation/config/remote.adoc: remote.<name>.fetch::
+remote.<name>.refmap::
+ The default value of the `--refmap` option for linkgit:git-fetch[1].
-+ Only takes effect when the fetch names what to fetch explicitly,
-+ either on the command line or via `remote.<name>.fetch`. See the
-+ `--refmap` entry in linkgit:git-fetch[1].
++ Used to map remote refs being fetched to remote-tracking refs to
++ store. See the `--refmap` entry in linkgit:git-fetch[1].
+
remote.<name>.push::
The default set of "refspec" for linkgit:git-push[1]. See
2: 4ec508a223 ! 2: 45b26e2bb2 fetch: infer branches to fetch from a refmap-only remote
@@ Commit message
says where to put fetched refs, not what to fetch.
Make that case infer what to fetch: the local branches whose
- @{upstream} is already on that remote, plus the remote's default
- branch, which is always included so it is available even before
- anything is set up to track it. This lets a remote be configured to
- fetch only the branches actually in use, without listing them by
- hand in remote.<name>.fetch, and without needing to touch the
- command line every time.
+ @{upstream} is already on that remote. This lets a remote be
+ configured to fetch only the branches actually in use, without
+ listing them by hand in remote.<name>.fetch, and without needing to
+ touch the command line every time.
Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
## Documentation/config/remote.adoc ##
@@ Documentation/config/remote.adoc: remote.<name>.fetch::
-
remote.<name>.refmap::
The default value of the `--refmap` option for linkgit:git-fetch[1].
-- Only takes effect when the fetch names what to fetch explicitly,
-- either on the command line or via `remote.<name>.fetch`. See the
-- `--refmap` entry in linkgit:git-fetch[1].
-+ If `remote.<name>.fetch` is not set either, a refspec-less fetch
-+ infers what to fetch from local branches built on this remote,
-+ instead of fetching every branch it has. See the `--refmap` entry
-+ in linkgit:git-fetch[1].
+ Used to map remote refs being fetched to remote-tracking refs to
+- store. See the `--refmap` entry in linkgit:git-fetch[1].
++ store. If `remote.<name>.fetch` is not set either, a refspec-less
++ fetch infers what to fetch from local branches built on this
++ remote, instead of fetching every branch it has. See the
++ `--refmap` entry in linkgit:git-fetch[1].
remote.<name>.push::
The default set of "refspec" for linkgit:git-push[1]. See
@@ Documentation/fetch-options.adoc: endif::git-pull[]
+When a refmap is active (from `--refmap` or `remote.<name>.refmap`) but
+there is nothing to fetch, neither on the command line nor from
+`remote.<name>.fetch`, Git infers what to fetch from the local branches
-+whose `@{upstream}` is on that remote, plus the remote's default branch,
-+which is always included so that it is available even before anything
-+is set up to track it.
++whose `@{upstream}` is on that remote.
`-t`::
`--tags`::
## builtin/fetch.c ##
-@@ builtin/fetch.c: static void filter_prefetch_refspec(struct refspec *rs)
- static struct ref *get_ref_map(struct remote *remote,
- const struct ref *remote_refs,
- struct refspec *rs,
-- int tags, int *autotags)
-+ int tags, int *autotags,
-+ char **bootstrap_head_branch)
- {
- int i;
- struct ref *rm;
@@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
struct ref **tail = &ref_map;
struct refspec *effective_refmap =
@@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
+ effective_refmap && effective_refmap->nr) {
+ struct string_list tracked = STRING_LIST_INIT_DUP;
+ struct string_list_item *item;
-+ struct ref *head;
-+ char *default_branch = NULL;
-+ char *default_branch_dst = NULL;
+
+ branches_tracking_remote(remote, &tracked);
+ for_each_string_list_item(item, &tracked)
+ refspec_append(&inferred_rs, item->string);
-+
-+ /* Always fetch the default branch too, as "HEAD". */
-+ head = get_remote_ref(remote_refs, "HEAD");
-+ if (head && head->symref && *head->symref)
-+ default_branch = xstrdup(head->symref);
-+ free_one_ref(head);
-+
-+ for (i = 0; default_branch && !default_branch_dst &&
-+ !string_list_has_string(&tracked, default_branch) &&
-+ i < effective_refmap->nr; i++) {
-+ struct refspec_item *map = &effective_refmap->items[i];
-+
-+ if (map->pattern)
-+ match_refname_with_pattern(map->src, default_branch,
-+ map->dst, &default_branch_dst);
-+ else if (!strcmp(map->src, default_branch))
-+ default_branch_dst = xstrdup(map->dst);
-+ }
-+
-+ if (default_branch_dst) {
-+ struct refspec_item head_item = { .force = 1 };
-+
-+ head_item.src = xstrdup("HEAD");
-+ head_item.dst = default_branch_dst;
-+ get_fetch_map(remote_refs, &head_item, &tail, 1);
-+ free(head_item.src);
-+ }
-+
-+ if (default_branch && bootstrap_head_branch) {
-+ const char *branch_name = default_branch;
-+ skip_prefix(branch_name, "refs/heads/", &branch_name);
-+ *bootstrap_head_branch = xstrdup(branch_name);
-+ }
-+
-+ free(default_branch);
-+ free(default_branch_dst);
+ string_list_clear(&tracked, 0);
+
+ rs = &inferred_rs;
@@ builtin/fetch.c: static struct ref *get_ref_map(struct remote *remote,
return ref_map;
}
-@@ builtin/fetch.c: static void warn_set_head(const char *remote, const char *head_name,
- }
-
- static int set_head(const struct ref *remote_refs, struct remote *remote,
-- int follow_remote_head)
-+ int follow_remote_head, const char *known_head_branch)
- {
- int result = 0, create_only, baremirror, was_detached;
- struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
- b_local_head = STRBUF_INIT;
- const char *no_warn_branch = remote->no_warn_branch;
- char *head_name = NULL;
-- struct ref *ref, *matches;
-+ struct ref *ref, *matches = NULL;
- struct ref *fetch_map = NULL, **fetch_map_tail = &fetch_map;
- struct refspec_item refspec = {
- .force = 0,
-@@ builtin/fetch.c: static int set_head(const struct ref *remote_refs, struct remote *remote,
- struct string_list heads = STRING_LIST_INIT_DUP;
- struct ref_store *refs = get_main_ref_store(the_repository);
-
-- get_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);
-- matches = guess_remote_head(find_ref_by_name(remote_refs, "HEAD"),
-- fetch_map, REMOTE_GUESS_HEAD_ALL);
-- for (ref = matches; ref; ref = ref->next) {
-- string_list_append(&heads, strip_refshead(ref->name));
-- }
-+ if (known_head_branch) {
-+ head_name = xstrdup(known_head_branch);
-+ } else {
-+ get_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);
-+ matches = guess_remote_head(find_ref_by_name(remote_refs, "HEAD"),
-+ fetch_map, REMOTE_GUESS_HEAD_ALL);
-+ for (ref = matches; ref; ref = ref->next) {
-+ string_list_append(&heads, strip_refshead(ref->name));
-+ }
-
-- if (!heads.nr)
-- result = 1;
-- else if (heads.nr > 1)
-- result = 1;
-- else
-- head_name = xstrdup(heads.items[0].string);
-+ if (!heads.nr)
-+ result = 1;
-+ else if (heads.nr > 1)
-+ result = 1;
-+ else
-+ head_name = xstrdup(heads.items[0].string);
-+ }
-
- if (!head_name)
- goto cleanup;
-@@ builtin/fetch.c: static int do_fetch(struct transport *transport,
- struct strmap rejected_refs = STRMAP_INIT;
- int summary_width = 0;
- int follow_remote_head;
-+ char *bootstrap_head_branch = NULL;
-
- if (tags == TAGS_DEFAULT) {
- if (transport->remote->fetch_tags == 2)
@@ builtin/fetch.c: static int do_fetch(struct transport *transport,
refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);
} else {
@@ builtin/fetch.c: static int do_fetch(struct transport *transport,
+ strvec_push(&transport_ls_refs_options.ref_prefixes,
+ item->string);
+ string_list_clear(&tracked, 0);
-+ strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
+ } else if (transport->remote->fetch.nr) {
refspec_ref_prefixes(&transport->remote->fetch,
&transport_ls_refs_options.ref_prefixes);
@@ builtin/fetch.c: static int do_fetch(struct transport *transport,
for (i = 0; i < branch->merge_nr; i++) {
strvec_push(&transport_ls_refs_options.ref_prefixes,
@@ builtin/fetch.c: static int do_fetch(struct transport *transport,
- transport_ls_refs_options_release(&transport_ls_refs_options);
ref_map = get_ref_map(transport->remote, remote_refs, rs,
-- tags, &autotags);
-+ tags, &autotags, &bootstrap_head_branch);
+ tags, &autotags);
+
if (!update_head_ok)
check_not_current_branch(ref_map);
-@@ builtin/fetch.c: static int do_fetch(struct transport *transport,
- * Way too many cases where this can go wrong so let's just
- * ignore errors and fail silently for now.
- */
-- set_head(remote_refs, transport->remote, follow_remote_head);
-+ set_head(remote_refs, transport->remote, follow_remote_head,
-+ bootstrap_head_branch);
- }
-
- cleanup:
-+ free(bootstrap_head_branch);
- /*
- * When using batched updates, we want to commit the non-rejected
- * updates and also handle the rejections.
## remote.c ##
@@ remote.c: int branch_merge_matches(struct branch *branch,
@@ t/t5585-fetch-refmap.sh (new)
+
+When a remote has a refmap configured but no fetch refspec, a
+refspec-less fetch infers what to fetch from the local branches whose
-+@{upstream} is on that remote, plus the default branch of that remote,
-+which is always included so that it is available even before anything
-+is set up to track it.
++@{upstream} is on that remote.
+'
+
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
@@ t/t5585-fetch-refmap.sh (new)
+ )
+'
+
-+test_expect_success 'fetching the new remote does not need every branch it has' '
++test_expect_success 'a bare fetch needs nothing until a branch is tracked' '
+ (
+ cd client &&
+ git fetch upstream &&
+ git for-each-ref --format="%(refname)" refs/remotes/upstream >actual &&
-+ cat >expect <<-\EOF &&
-+ refs/remotes/upstream/HEAD
-+ refs/remotes/upstream/main
-+ EOF
-+ test_cmp expect actual
++ test_must_be_empty actual
+ )
+'
+
-+test_expect_success 'set-upstream-to now resolves right after that first fetch' '
++test_expect_success 'an explicit one-time fetch lets a branch be tracked' '
+ (
+ cd client &&
-+ git branch --set-upstream-to=upstream &&
++ git fetch upstream main &&
++ git branch --set-upstream-to=upstream/main &&
+ test_cmp_config upstream branch.main.remote &&
+ test_cmp_config refs/heads/main branch.main.merge
+ )
3: 604e584956 ! 3: e2f072c254 remote: add "git remote add --limited-fetch"
@@ Documentation/git-remote.adoc: the `refs/remotes/<name>/` namespace, a refspec t
+With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec
+that tracks all branches, `remote.<name>.refmap` is set up so that a
+refspec-less `git fetch <name>` only fetches branches our local branches
-+are built on, plus the remote's default branch. See the `--refmap` entry
-+in linkgit:git-fetch[1] for details.
++are built on. See the `--refmap` entry in linkgit:git-fetch[1] for
++details.
++
With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set
up to point at remote's _<master>_ branch. See also the set-head command.
4: 31462e9445 ! 4: 43b9711a2c remote: default to --limited-fetch in a shallow repository
@@ Documentation/git-remote.adoc: Add a remote named _<name>_ for the repository at
@@ Documentation/git-remote.adoc: With `--limited-fetch` option, instead of a `remote.<name>.fetch` refspec
that tracks all branches, `remote.<name>.refmap` is set up so that a
refspec-less `git fetch <name>` only fetches branches our local branches
- are built on, plus the remote's default branch. See the `--refmap` entry
--in linkgit:git-fetch[1] for details.
-+in linkgit:git-fetch[1] for details. `--no-limited-fetch` explicitly
-+disables this, overriding the shallow-repository default described above.
+ are built on. See the `--refmap` entry in linkgit:git-fetch[1] for
+-details.
++details. `--no-limited-fetch` explicitly disables this, overriding the
++shallow-repository default described above.
+
With `-m <master>` option, a symbolic-ref `refs/remotes/<name>/HEAD` is set
up to point at remote's _<master>_ branch. See also the set-head command.
@@ t/t5505-remote.sh: test_expect_success 'filters are listed by git remote -v only
+ test_cmp_config "+refs/heads/*:refs/remotes/upstream/*" \
+ remote.upstream.refmap &&
+ test_must_fail git config get remote.upstream.fetch &&
++ git fetch upstream main &&
++ git branch --set-upstream-to=upstream/main &&
++ test_cmp_config upstream branch.main.remote &&
++ test_cmp_config refs/heads/main branch.main.merge &&
+ git fetch upstream &&
+ git for-each-ref --format="%(refname)" refs/remotes/upstream >actual &&
+ cat >expect <<-\EOF &&
+ refs/remotes/upstream/HEAD
+ refs/remotes/upstream/main
+ EOF
-+ test_cmp expect actual &&
-+ git branch --set-upstream-to=upstream &&
-+ test_cmp_config upstream branch.main.remote &&
-+ test_cmp_config refs/heads/main branch.main.merge
++ test_cmp expect actual
+ )
+'
+
--
gitgitgadget
next prev parent reply other threads:[~2026-09-29 9:20 UTC|newest]
Thread overview: 55+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-19 14:47 [PATCH] fetch: add config to avoid fetching every branch in shallow repo Harald Nordgren via GitGitGadget
2026-09-21 13:28 ` Phillip Wood
2026-09-21 21:45 ` Harald Nordgren
2026-09-22 13:00 ` Harald Nordgren
2026-09-22 14:53 ` Phillip Wood
2026-09-22 15:37 ` Harald Nordgren
2026-09-23 15:14 ` Phillip Wood
2026-09-22 17:11 ` Junio C Hamano
2026-09-22 21:40 ` Harald Nordgren
2026-09-23 15:19 ` Phillip Wood
2026-09-23 15:34 ` Junio C Hamano
2026-09-23 16:55 ` D. Ben Knoble
2026-09-23 19:50 ` Junio C Hamano
2026-09-24 17:10 ` D. Ben Knoble
2026-09-24 18:02 ` Junio C Hamano
2026-09-23 20:35 ` [PATCH v2] fetch: avoid fetching every branch of a new remote in a " Harald Nordgren via GitGitGadget
2026-09-23 21:38 ` Junio C Hamano
2026-09-25 10:49 ` [PATCH v3 0/4] " Harald Nordgren via GitGitGadget
2026-09-25 10:49 ` [PATCH v3 1/4] fetch: add remote.<name>.refmap Harald Nordgren via GitGitGadget
2026-09-25 22:38 ` Junio C Hamano
2026-09-25 10:50 ` [PATCH v3 2/4] fetch: infer branches to fetch from a refmap-only remote Harald Nordgren via GitGitGadget
2026-09-25 23:26 ` Junio C Hamano
2026-09-25 10:50 ` [PATCH v3 3/4] remote: add "git remote add --limited-fetch" Harald Nordgren via GitGitGadget
2026-09-25 10:50 ` [PATCH v3 4/4] remote: default to --limited-fetch in a shallow repository Harald Nordgren via GitGitGadget
2026-09-29 9:19 ` Harald Nordgren via GitGitGadget [this message]
2026-09-29 9:19 ` [PATCH v4 1/4] fetch: add remote.<name>.refmap Harald Nordgren via GitGitGadget
2026-09-29 9:19 ` [PATCH v4 2/4] fetch: infer branches to fetch from a refmap-only remote Harald Nordgren via GitGitGadget
2026-09-29 9:27 ` Harald Nordgren
2026-09-29 20:17 ` Junio C Hamano
2026-09-29 9:19 ` [PATCH v4 3/4] remote: add "git remote add --limited-fetch" Harald Nordgren via GitGitGadget
2026-09-29 9:19 ` [PATCH v4 4/4] remote: default to --limited-fetch in a shallow repository Harald Nordgren via GitGitGadget
2026-09-29 19:36 ` [PATCH v4 0/4] fetch: avoid fetching every branch of a new remote in a shallow repo Junio C Hamano
2026-10-02 7:13 ` [PATCH v5 " Harald Nordgren via GitGitGadget
2026-10-02 7:13 ` [PATCH v5 1/4] fetch: add remote.<name>.refmap Harald Nordgren via GitGitGadget
2026-10-02 7:13 ` [PATCH v5 2/4] fetch: infer branches to fetch from a refmap-only remote Harald Nordgren via GitGitGadget
2026-10-02 7:13 ` [PATCH v5 3/4] remote: add "git remote add --limited-fetch" Harald Nordgren via GitGitGadget
2026-10-02 16:28 ` Junio C Hamano
2026-10-02 7:13 ` [PATCH v5 4/4] remote: default to --limited-fetch in a shallow repository Harald Nordgren via GitGitGadget
2026-10-04 8:31 ` [PATCH v6 0/4] fetch: avoid fetching every branch of a new remote in a shallow repo Harald Nordgren via GitGitGadget
2026-10-04 8:31 ` [PATCH v6 1/4] fetch: add remote.<name>.refmap Harald Nordgren via GitGitGadget
2026-10-04 8:31 ` [PATCH v6 2/4] fetch: infer branches to fetch from a refmap-only remote Harald Nordgren via GitGitGadget
2026-10-04 8:31 ` [PATCH v6 3/4] remote: add "git remote add --limited-fetch" Harald Nordgren via GitGitGadget
2026-10-04 8:31 ` [PATCH v6 4/4] remote: default to --limited-fetch in a shallow repository Harald Nordgren via GitGitGadget
2026-10-04 17:17 ` [PATCH v6 0/4] fetch: avoid fetching every branch of a new remote in a shallow repo Junio C Hamano
2026-10-04 19:51 ` Harald Nordgren
2026-10-05 12:17 ` Junio C Hamano
2026-10-05 18:09 ` Harald Nordgren
2026-10-07 21:56 ` [PATCH v7 " Harald Nordgren via GitGitGadget
2026-10-07 21:56 ` [PATCH v7 1/4] fetch: add remote.<name>.refmap Harald Nordgren via GitGitGadget
2026-10-07 21:56 ` [PATCH v7 2/4] fetch: infer branches to fetch from a refmap-only remote Harald Nordgren via GitGitGadget
2026-10-08 16:59 ` Junio C Hamano
2026-10-09 7:05 ` Harald Nordgren
2026-10-09 8:09 ` Harald Nordgren
2026-10-07 21:56 ` [PATCH v7 3/4] remote: add "git remote add --limited-fetch" Harald Nordgren via GitGitGadget
2026-10-07 21:56 ` [PATCH v7 4/4] remote: default to --limited-fetch in a shallow repository Harald Nordgren via GitGitGadget
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=pull.2412.v4.git.git.1790673598.gitgitgadget@gmail.com \
--to=gitgitgadget@gmail.com \
--cc=ben.knoble@gmail.com \
--cc=git@vger.kernel.org \
--cc=haraldnordgren@gmail.com \
--cc=phillip.wood123@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox