From b69f731e478a6156d260810ef8f994641180be4d Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Sat, 10 Oct 2026 08:02:10 +0000 Subject: [PATCH 1/5] fetch: add remote..refmap Add a per-remote config variable, remote..refmap, that provides the default value for --refmap the same way remote..fetch 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..fetch. A refspec-less fetch with a refmap configured but nothing else to say what to fetch falls back to the same defaults as a remote with no refspec or refmap at all. Only an explicit --refmap given on the command line with no command-line refspec to go with it is still rejected, since that combination is a plain mistake to type, unlike a remote..refmap configured on its own. A remote..fetch that is also configured is used as before. Signed-off-by: Harald Nordgren Signed-off-by: Junio C Hamano --- Documentation/config/remote.adoc | 5 +++ Documentation/fetch-options.adoc | 3 ++ builtin/fetch.c | 10 ++++- remote.c | 12 +++++- remote.h | 6 +++ t/t5510-fetch.sh | 63 ++++++++++++++++++++++++++++++++ 6 files changed, 96 insertions(+), 3 deletions(-) diff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc index 3a20d0f752..103eada406 100644 --- a/Documentation/config/remote.adoc +++ b/Documentation/config/remote.adoc @@ -33,6 +33,11 @@ remote..fetch:: The default set of "refspec" for linkgit:git-fetch[1]. See linkgit:git-fetch[1]. +remote..refmap:: + The default value of the `--refmap` option for 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..push:: The default set of "refspec" for linkgit:git-push[1]. See linkgit:git-push[1]. diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc index 035f780e58..538914bc6e 100644 --- a/Documentation/fetch-options.adoc +++ b/Documentation/fetch-options.adoc @@ -244,6 +244,9 @@ endif::git-pull[] refspecs and rely entirely on the refspecs supplied as command-line arguments. See section on "Configured Remote-tracking Branches" for details. ++ +`remote..refmap` provides the default value for this option, the +same way `remote..fetch` provides the default refspecs to fetch. `-t`:: `--tags`:: diff --git a/builtin/fetch.c b/builtin/fetch.c index 533fdfe7d8..6e89d1a00f 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -509,6 +509,12 @@ static struct ref *get_ref_map(struct remote *remote, struct ref *rm; struct ref *ref_map = NULL; struct ref **tail = &ref_map; + /* + * The --refmap command line option, if given, takes precedence + * over remote..refmap. + */ + struct refspec *effective_refmap = + refmap.nr ? &refmap : remote ? &remote->refmap : NULL; /* opportunistically-updated references: */ struct ref *orefs = NULL, **oref_tail = &orefs; @@ -552,8 +558,8 @@ static struct ref *get_ref_map(struct remote *remote, * by ref_remove_duplicates() in favor of one of these * opportunistic entries with FETCH_HEAD_IGNORE. */ - if (refmap.nr) - fetch_refspec = &refmap; + if (effective_refmap && effective_refmap->nr) + fetch_refspec = effective_refmap; else fetch_refspec = &remote->fetch; diff --git a/remote.c b/remote.c index fe62068463..017cd9d13e 100644 --- a/remote.c +++ b/remote.c @@ -152,6 +152,7 @@ static struct remote *make_remote(struct remote_state *remote_state, ret->name = xstrndup(name, len); refspec_init_push(&ret->push, the_hash_algo); refspec_init_fetch(&ret->fetch, the_hash_algo); + refspec_init_fetch(&ret->refmap, the_hash_algo); string_list_init_dup(&ret->server_options); string_list_init_dup(&ret->negotiation_restrict); string_list_init_dup(&ret->negotiation_include); @@ -176,6 +177,7 @@ static void remote_clear(struct remote *remote) refspec_clear(&remote->push); refspec_clear(&remote->fetch); + refspec_clear(&remote->refmap); free((char *)remote->receivepack); free((char *)remote->uploadpack); @@ -539,6 +541,12 @@ static int handle_config(const char *key, const char *value, return -1; refspec_append(&remote->fetch, v); free(v); + } else if (!strcmp(subkey, "refmap")) { + char *v; + if (git_config_string(&v, key, value)) + return -1; + refspec_append(&remote->refmap, v); + free(v); } else if (!strcmp(subkey, "receivepack")) { char *v; if (git_config_string(&v, key, value)) @@ -988,7 +996,9 @@ void ref_push_report_free(struct ref_push_report *report) int remote_find_tracking(struct remote *remote, struct refspec_item *refspec) { - return refspec_find_match(&remote->fetch, refspec); + if (remote->fetch.nr) + return refspec_find_match(&remote->fetch, refspec); + return refspec_find_match(&remote->refmap, refspec); } static struct ref *alloc_ref_with_prefix(const char *prefix, size_t prefixlen, diff --git a/remote.h b/remote.h index cca02033b9..ac485a584d 100644 --- a/remote.h +++ b/remote.h @@ -90,6 +90,12 @@ struct remote { struct refspec fetch; + /* + * How to map refs fetched without an explicit destination into our + * own namespace, the same as the --refmap command line option. + */ + struct refspec refmap; + /* * The setting for whether to fetch tags (as a separate rule from the * configured refspecs); diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh index a8d38d9176..886dd0ca4b 100755 --- a/t/t5510-fetch.sh +++ b/t/t5510-fetch.sh @@ -927,6 +927,69 @@ test_expect_success 'explicit --refmap option overrides remote.*.fetch' ' ) ' +test_expect_success 'remote.*.refmap acts like --refmap on the command line' ' + test_when_finished "git -C three config --unset remote.origin.refmap" && + git branch -f side && + git -C three config remote.origin.refmap \ + "refs/heads/*:refs/remotes/other/*" && + ( + cd three && + git update-ref refs/remotes/origin/main base-origin-main && + o=$(git rev-parse --verify refs/remotes/origin/main) && + git fetch origin main && + n=$(git rev-parse --verify refs/remotes/origin/main) && + test "$o" = "$n" && + test_must_fail git rev-parse --verify refs/remotes/origin/side && + git rev-parse --verify refs/remotes/other/main + ) +' + +check_fetched_refs () { + git for-each-ref --format="%(refname)" refs/remotes/ >actual && + cat >expect && + test_cmp expect actual +} + +test_expect_success 'remote..refmap without tracking (baseline)' ' + test_when_finished "rm -fr fetch-refmap-baseline fetch-refmap-upstream" && + git init -b main fetch-refmap-upstream && + test_commit -C fetch-refmap-upstream base && + git -C fetch-refmap-upstream branch other && + git init fetch-refmap-baseline && + ( + cd fetch-refmap-baseline && + git remote add origin ../fetch-refmap-upstream && + + # Without fetch refspec, but with fetch refmap. + git config --unset-all remote.origin.fetch && + git config remote.origin.refmap "+refs/heads/*:refs/remotes/origin/*" && + + # Nothing tracked, nothing fetched, no error. + git fetch origin && + check_fetched_refs <<-\EOF && + EOF + + # Nothing tracked, explicit ref on the command line. + git fetch origin main && + check_fetched_refs <<-\EOF && + refs/remotes/origin/main + EOF + + # With both refmap and fetch configured, remote..fetch + # wins: a refspec-less fetch follows it as usual, and + # remote..refmap plays no part in deciding what to + # fetch, only in remapping something that is already being + # fetched by name. + git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" && + git fetch origin && + check_fetched_refs <<-\EOF + refs/remotes/origin/HEAD + refs/remotes/origin/main + refs/remotes/origin/other + EOF + ) +' + test_expect_success 'explicitly empty --refmap option disables remote.*.fetch' ' git branch -f side && ( From 17e46b28137a95a0a1bfd22f9df3292a8699734c Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Sat, 10 Oct 2026 08:02:11 +0000 Subject: [PATCH 2/5] fetch: extract collect_upstream_from_remote() helper When a fetch has no refspec to work from, we fall back to guessing what to fetch: the remote's configured fetch refspec if there is one, and separately, whether the current branch's upstream lives on this remote. The latter is independent of the former on purpose: a narrow refspec (for example from "git remote add -t") can leave the current branch's own upstream uncovered, and we still need to be able to fetch it. That "does this branch build on this remote" check was inlined for just the current branch. Move it into its own function, collect_upstream_from_remote(), so the same logic can be reused for branches other than just the current one. Signed-off-by: Harald Nordgren Signed-off-by: Junio C Hamano --- builtin/fetch.c | 45 +++++++++++++++++++++++++++++---------------- remote.c | 15 +++++++++++++++ remote.h | 9 +++++++++ 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/builtin/fetch.c b/builtin/fetch.c index 6e89d1a00f..85411bad33 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -1962,23 +1962,36 @@ static int do_fetch(struct transport *transport, if (rs->nr) { refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes); - } else { - struct branch *branch = branch_get(NULL); + } else if (transport->remote->fetch.nr) { + struct string_list tracked = STRING_LIST_INIT_DUP; + struct string_list_item *item; - if (transport->remote->fetch.nr) { - refspec_ref_prefixes(&transport->remote->fetch, - &transport_ls_refs_options.ref_prefixes); - if (follow_remote_head != FOLLOW_REMOTE_NEVER) - do_set_head = 1; - } - if (branch && branch_has_merge_config(branch) && - !strcmp(branch->remote_name, transport->remote->name)) { - int i; - for (i = 0; i < branch->merge_nr; i++) { - strvec_push(&transport_ls_refs_options.ref_prefixes, - branch->merge[i]->src); - } - } + refspec_ref_prefixes(&transport->remote->fetch, + &transport_ls_refs_options.ref_prefixes); + if (follow_remote_head != FOLLOW_REMOTE_NEVER) + do_set_head = 1; + + /* + * The configured refspec may not cover the current + * branch's upstream (e.g. a narrowed -t refspec), so + * make sure we can still fetch it regardless. + */ + collect_upstream_from_remote(the_repository, &tracked, + transport->remote, NULL); + for_each_string_list_item(item, &tracked) + strvec_push(&transport_ls_refs_options.ref_prefixes, + item->string); + string_list_clear(&tracked, 0); + } else { + struct string_list tracked = STRING_LIST_INIT_DUP; + struct string_list_item *item; + + collect_upstream_from_remote(the_repository, &tracked, + transport->remote, NULL); + for_each_string_list_item(item, &tracked) + strvec_push(&transport_ls_refs_options.ref_prefixes, + item->string); + string_list_clear(&tracked, 0); /* * If there are no refs specified to fetch, then we just diff --git a/remote.c b/remote.c index 017cd9d13e..8f3bb22edd 100644 --- a/remote.c +++ b/remote.c @@ -1884,6 +1884,21 @@ int branch_merge_matches(struct branch *branch, return refname_match(branch->merge[i]->src, refname); } +void collect_upstream_from_remote(struct repository *repo, + struct string_list *tracked, + struct remote *remote, + const char *refname) +{ + struct branch *branch = repo_branch_get(repo, refname); + + if (!branch_has_merge_config(branch) || + strcmp(branch->remote_name, remote->name)) + return; + + for (int i = 0; i < branch->merge_nr; i++) + string_list_insert(tracked, branch->merge[i]->src); +} + __attribute__((format (printf,2,3))) static char *error_buf(struct strbuf *err, const char *fmt, ...) { diff --git a/remote.h b/remote.h index ac485a584d..7c86c529b6 100644 --- a/remote.h +++ b/remote.h @@ -359,6 +359,15 @@ int branch_has_merge_config(struct branch *branch); int branch_merge_matches(struct branch *, int n, const char *); +/* + * If refname's branch builds on remote, add its upstream on remote to + * tracked. A NULL refname means the current branch. + */ +void collect_upstream_from_remote(struct repository *repo, + struct string_list *tracked, + struct remote *remote, + const char *refname); + /* list of the remote in a group as configured */ struct remote_group_data { const char *name; From b616bf060cbb4648cc0e2a07bb963fef987f94c5 Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Sat, 10 Oct 2026 08:02:12 +0000 Subject: [PATCH 3/5] fetch: infer branches to fetch from a refmap-only remote Configuring remote..refmap without remote..fetch used to make a refspec-less "git fetch " fail with "--refmap option is only meaningful with command-line refspec(s)", since a refmap only 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. This lets a remote be configured to fetch only the branches actually in use, without listing them by hand in remote..fetch, and without needing to touch the command line every time. Signed-off-by: Harald Nordgren Signed-off-by: Junio C Hamano --- Documentation/config/remote.adoc | 5 +- Documentation/fetch-options.adoc | 6 +- builtin/fetch.c | 41 ++++++++++- remote.c | 24 +++++++ remote.h | 4 ++ t/meson.build | 1 + t/t5586-fetch-refmap.sh | 119 +++++++++++++++++++++++++++++++ 7 files changed, 194 insertions(+), 6 deletions(-) create mode 100755 t/t5586-fetch-refmap.sh diff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc index 103eada406..7c52e443a5 100644 --- a/Documentation/config/remote.adoc +++ b/Documentation/config/remote.adoc @@ -36,7 +36,10 @@ remote..fetch:: remote..refmap:: The default value of the `--refmap` option for 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..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..push:: The default set of "refspec" for linkgit:git-push[1]. See diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc index 538914bc6e..b720e014e6 100644 --- a/Documentation/fetch-options.adoc +++ b/Documentation/fetch-options.adoc @@ -245,8 +245,10 @@ endif::git-pull[] command-line arguments. See section on "Configured Remote-tracking Branches" for details. + -`remote..refmap` provides the default value for this option, the -same way `remote..fetch` provides the default refspecs to fetch. +When a refmap is active (from `--refmap` or `remote..refmap`) but +nothing to fetch is specified on the command line, nor is there a +`remote..fetch`, branches from the remote that are used as the +`@{upstream}` of our local branches are fetched. `-t`:: `--tags`:: diff --git a/builtin/fetch.c b/builtin/fetch.c index 85411bad33..2453a150cd 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -515,6 +515,8 @@ static struct ref *get_ref_map(struct remote *remote, */ struct refspec *effective_refmap = refmap.nr ? &refmap : remote ? &remote->refmap : NULL; + struct refspec inferred_rs; + int infer_from_refmap = 0; /* opportunistically-updated references: */ struct ref *orefs = NULL, **oref_tail = &orefs; @@ -522,15 +524,32 @@ static struct ref *get_ref_map(struct remote *remote, struct hashmap existing_refs; int existing_refs_populated = 0; + refspec_init_fetch(&inferred_rs, the_hash_algo); + filter_prefetch_refspec(rs); if (remote) filter_prefetch_refspec(&remote->fetch); + if (!rs->nr && remote && !remote->fetch.nr && + effective_refmap && effective_refmap->nr) { + struct string_list tracked = STRING_LIST_INIT_DUP; + struct string_list_item *item; + + branches_tracking_remote(the_repository, remote, &tracked); + for_each_string_list_item(item, &tracked) + refspec_append(&inferred_rs, item->string); + string_list_clear(&tracked, 0); + + rs = &inferred_rs; + infer_from_refmap = 1; + } + if (rs->nr) { struct refspec *fetch_refspec; for (i = 0; i < rs->nr; i++) { - get_fetch_map(remote_refs, &rs->items[i], &tail, 0); + get_fetch_map(remote_refs, &rs->items[i], &tail, + infer_from_refmap); if (rs->items[i].dst && rs->items[i].dst[0]) *autotags = 1; } @@ -565,6 +584,8 @@ static struct ref *get_ref_map(struct remote *remote, for (i = 0; i < fetch_refspec->nr; i++) get_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1); + } else if (infer_from_refmap) { + /* Already fully handled above. */ } else if (refmap.nr) { die("--refmap option is only meaningful with command-line refspec(s)"); } else { @@ -660,6 +681,7 @@ static struct ref *get_ref_map(struct remote *remote, if (existing_refs_populated) hashmap_clear_and_free(&existing_refs, struct refname_hash_entry, ent); + refspec_clear(&inferred_rs); return ref_map; } @@ -1983,11 +2005,24 @@ static int do_fetch(struct transport *transport, item->string); string_list_clear(&tracked, 0); } else { + /* + * The --refmap command line option, if given, takes + * precedence over remote..refmap. + */ + struct refspec *effective_refmap = refmap.nr ? &refmap : + &transport->remote->refmap; struct string_list tracked = STRING_LIST_INIT_DUP; struct string_list_item *item; - collect_upstream_from_remote(the_repository, &tracked, - transport->remote, NULL); + if (effective_refmap->nr) { + branches_tracking_remote(the_repository, + transport->remote, &tracked); + if (follow_remote_head != FOLLOW_REMOTE_NEVER) + do_set_head = 1; + } else { + collect_upstream_from_remote(the_repository, &tracked, + transport->remote, NULL); + } for_each_string_list_item(item, &tracked) strvec_push(&transport_ls_refs_options.ref_prefixes, item->string); diff --git a/remote.c b/remote.c index 8f3bb22edd..f50a7bc854 100644 --- a/remote.c +++ b/remote.c @@ -1899,6 +1899,30 @@ void collect_upstream_from_remote(struct repository *repo, string_list_insert(tracked, branch->merge[i]->src); } +struct branches_tracking_remote_cb_data { + struct repository *repo; + struct remote *remote; + struct string_list *tracked; +}; + +static int add_if_tracking_remote(const struct reference *ref, void *cb_data) +{ + struct branches_tracking_remote_cb_data *data = cb_data; + + collect_upstream_from_remote(data->repo, data->tracked, data->remote, + ref->name); + return 0; +} + +void branches_tracking_remote(struct repository *repo, struct remote *remote, + struct string_list *tracked) +{ + struct branches_tracking_remote_cb_data data = { repo, remote, tracked }; + + refs_for_each_branch_ref(get_main_ref_store(repo), + add_if_tracking_remote, &data); +} + __attribute__((format (printf,2,3))) static char *error_buf(struct strbuf *err, const char *fmt, ...) { diff --git a/remote.h b/remote.h index 7c86c529b6..62265346d3 100644 --- a/remote.h +++ b/remote.h @@ -368,6 +368,10 @@ void collect_upstream_from_remote(struct repository *repo, struct remote *remote, const char *refname); +/* fills tracked with the refname of every local branch's upstream on remote */ +void branches_tracking_remote(struct repository *repo, struct remote *remote, + struct string_list *tracked); + /* list of the remote in a group as configured */ struct remote_group_data { const char *name; diff --git a/t/meson.build b/t/meson.build index 3ca7b27104..a1c7465c2b 100644 --- a/t/meson.build +++ b/t/meson.build @@ -728,6 +728,7 @@ integration_tests = [ 't5582-fetch-negative-refspec.sh', 't5583-push-branches.sh', 't5584-http-429-retry.sh', + 't5586-fetch-refmap.sh', 't5600-clone-fail-cleanup.sh', 't5601-clone.sh', 't5602-clone-remote-exec.sh', diff --git a/t/t5586-fetch-refmap.sh b/t/t5586-fetch-refmap.sh new file mode 100755 index 0000000000..b81fc48cbe --- /dev/null +++ b/t/t5586-fetch-refmap.sh @@ -0,0 +1,119 @@ +#!/bin/sh + +test_description='"git fetch" with a remote..refmap but no remote..fetch + +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. +' + +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME + +. ./test-lib.sh + +test_expect_success 'setup' ' + test_commit main-1 && + test_commit main-2 && + git checkout -b side main-1 && + test_commit side-1 && + git checkout -b next main-1 && + test_commit next-1 && + git checkout main +' + +test_expect_success 'clone shallow and single-branch, then add a second remote' ' + git clone --no-local --depth=1 --branch main --single-branch . client && + ( + cd client && + git remote add upstream .. && + test_might_fail git config unset remote.upstream.fetch && + git config remote.upstream.refmap \ + "+refs/heads/*:refs/remotes/upstream/*" + ) +' + +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 && + test_must_be_empty actual + ) +' + +test_expect_success 'an explicit one-time fetch lets a branch be tracked' ' + ( + cd client && + 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 + ) +' + +test_expect_success 'a branch checked out from a one-time fetch is kept updated by later plain fetches' ' + ( + cd client && + git fetch upstream side:refs/remotes/upstream/side && + git branch side-topic upstream/side + ) && + git checkout side && + test_commit side-2 && + git checkout main && + ( + 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 + refs/remotes/upstream/side + EOF + test_cmp expect actual && + git rev-parse refs/remotes/upstream/side >actual-oid && + git -C .. rev-parse side >expect-oid && + test_cmp expect-oid actual-oid + ) +' + +test_expect_success 'a second branch tracking the same upstream branch does not fetch it twice' ' + ( + cd client && + git branch side-topic-2 upstream/side && + git fetch upstream && + git for-each-ref --format="%(refname)" refs/remotes/upstream >actual && + cat >expect <<-\EOF && + refs/remotes/upstream/HEAD + refs/remotes/upstream/main + refs/remotes/upstream/side + EOF + test_cmp expect actual + ) +' + +test_expect_success 'a branch tracking a different remote is not fetched from upstream' ' + ( + cd client && + git remote add other .. && + git fetch other next:refs/remotes/other/next && + git branch next-topic other/next && + git fetch upstream && + git for-each-ref --format="%(refname)" refs/remotes/upstream >actual && + cat >expect <<-\EOF && + refs/remotes/upstream/HEAD + refs/remotes/upstream/main + refs/remotes/upstream/side + EOF + test_cmp expect actual + ) +' + +test_expect_success 'git remote show does not choke on a refmap-only remote' ' + ( + cd client && + git remote show upstream + ) +' + +test_done From ee8f93969c76de94b3f0c75da3dcce6669b4b77c Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Sat, 10 Oct 2026 08:02:13 +0000 Subject: [PATCH 4/5] remote: add "git remote add --limited-fetch" A remote added the ordinary way tracks every branch it has, via a wildcard remote..fetch refspec. That is wasteful for a remote whose history is only worth following for the branches actually in use locally, and it can make "git fetch" negotiate history for branches nobody asked for. Give "git remote add" a --limited-fetch option that sets up remote..refmap instead of remote..fetch, so that "git fetch " only fetches the branches already tracked, as described in the previous commit. It is rejected together with -t/--track or --mirror, since those already say explicitly what to fetch. Signed-off-by: Harald Nordgren Signed-off-by: Junio C Hamano --- Documentation/git-remote.adoc | 8 +++++++- builtin/remote.c | 28 ++++++++++++++++++++++------ t/t5505-remote.sh | 16 ++++++++++++++++ 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/Documentation/git-remote.adoc b/Documentation/git-remote.adoc index eaae30aa88..4255f8b3e6 100644 --- a/Documentation/git-remote.adoc +++ b/Documentation/git-remote.adoc @@ -10,7 +10,7 @@ SYNOPSIS -------- [synopsis] git remote [-v | --verbose] -git remote add [-t ] [-m ] [-f] [--[no-]tags] [--mirror=(fetch|push)] +git remote add [-t ] [-m ] [-f] [--[no-]tags] [--mirror=(fetch|push)] [--[no-]limited-fetch] git remote rename [--[no-]progress] git remote remove git remote set-head (-a | --auto | -d | --delete | ) @@ -70,6 +70,12 @@ the `refs/remotes//` namespace, a refspec to track only __ is created. You can give more than one `-t ` to track multiple branches without grabbing all branches. + +With `--limited-fetch` option, instead of a `remote..fetch` refspec +that tracks all branches, `remote..refmap` is set up so that a +refspec-less `git fetch ` only fetches branches our local branches +are built on. See the `--refmap` entry in linkgit:git-fetch[1] for +details. ++ With `-m ` option, a symbolic-ref `refs/remotes//HEAD` is set up to point at remote's __ branch. See also the set-head command. + diff --git a/builtin/remote.c b/builtin/remote.c index de989ea3ba..f036dd5d6f 100644 --- a/builtin/remote.c +++ b/builtin/remote.c @@ -179,6 +179,7 @@ static int add(int argc, const char **argv, const char *prefix, { int fetch = 0, fetch_tags = TAGS_DEFAULT; unsigned mirror = MIRROR_NONE; + int limited_fetch = -1; /* unspecified */ struct string_list track = STRING_LIST_INIT_NODUP; const char *master = NULL; struct remote *remote; @@ -198,6 +199,8 @@ static int add(int argc, const char **argv, const char *prefix, OPT_CALLBACK_F(0, "mirror", &mirror, "(push|fetch)", N_("set up remote as a mirror to push to or fetch from"), PARSE_OPT_OPTARG | PARSE_OPT_COMP_ARG, parse_mirror_opt), + OPT_BOOL(0, "limited-fetch", &limited_fetch, + N_("fetch only the branches we build on, instead of every branch")), OPT_END() }; @@ -211,6 +214,10 @@ static int add(int argc, const char **argv, const char *prefix, die(_("specifying a master branch makes no sense with --mirror")); if (mirror && !(mirror & MIRROR_FETCH) && track.nr) die(_("specifying branches to track makes sense only with fetch mirrors")); + if (limited_fetch == 1 && track.nr) + die(_("--limited-fetch does not make sense with -t/--track")); + if (limited_fetch == 1 && mirror) + die(_("--limited-fetch does not make sense with --mirror")); name = argv[0]; url = argv[1]; @@ -230,13 +237,22 @@ static int add(int argc, const char **argv, const char *prefix, repo_config_set(the_repository, buf.buf, url); if (!mirror || mirror & MIRROR_FETCH) { + int use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 && + limited_fetch == 1; + strbuf_reset(&buf); - strbuf_addf(&buf, "remote.%s.fetch", name); - if (track.nr == 0) - string_list_append(&track, "*"); - for (size_t i = 0; i < track.nr; i++) { - add_branch(buf.buf, track.items[i].string, - name, mirror, &buf2); + if (use_limited_fetch) { + strbuf_addf(&buf, "remote.%s.refmap", name); + strbuf_reset(&buf2); + strbuf_addf(&buf2, "+refs/heads/*:refs/remotes/%s/*", name); + repo_config_set(the_repository, buf.buf, buf2.buf); + } else { + strbuf_addf(&buf, "remote.%s.fetch", name); + if (track.nr == 0) + string_list_append(&track, "*"); + for (size_t i = 0; i < track.nr; i++) + add_branch(buf.buf, track.items[i].string, + name, mirror, &buf2); } } diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh index 5fbfcb0848..0168d5abfe 100755 --- a/t/t5505-remote.sh +++ b/t/t5505-remote.sh @@ -137,6 +137,22 @@ test_expect_success 'filters are listed by git remote -v only' ' test_grep ! "\[blob:none\]" out ' +test_expect_success '--limited-fetch works in a full repository too' ' + test_when_finished "rm -rf full-add" && + git clone --no-local one full-add && + ( + cd full-add && + git remote add --limited-fetch upstream ../two && + test_cmp_config "+refs/heads/*:refs/remotes/upstream/*" \ + remote.upstream.refmap && + test_must_fail git config get remote.upstream.fetch + ) +' + +test_expect_success '--limited-fetch conflicts with -t' ' + test_must_fail git remote add --limited-fetch -t main upstream ../two +' + test_expect_success 'check remote-tracking' ' ( cd test && From 2b6e751fc043613fee90ee788faa8dbef190c4a8 Mon Sep 17 00:00:00 2001 From: Harald Nordgren Date: Sat, 10 Oct 2026 08:02:14 +0000 Subject: [PATCH 5/5] remote: default to --limited-fetch in a shallow repository Adding a second remote to a shallow, single-branch clone used to still fetch every branch that remote has, since "git remote add" always set up a wildcard remote..fetch refspec regardless of how shallow the repository already was. That defeats the purpose of having cloned shallow and single-branch in the first place, and can make a plain "git fetch" on that remote hang or take a very long time on a repository with many branches. Turn --limited-fetch on by default when the repository is already shallow and neither -t/--track nor --mirror was given, so that adding a remote there does not by itself commit to following every branch it has. --no-limited-fetch keeps the previous behavior for whoever wants it. Signed-off-by: Harald Nordgren Signed-off-by: Junio C Hamano --- Documentation/git-remote.adoc | 8 ++++- builtin/remote.c | 5 ++- t/t5505-remote.sh | 60 +++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/Documentation/git-remote.adoc b/Documentation/git-remote.adoc index 4255f8b3e6..80f8a4a183 100644 --- a/Documentation/git-remote.adoc +++ b/Documentation/git-remote.adoc @@ -52,6 +52,11 @@ Add a remote named __ for the repository at __. The command `git fetch ` can then be used to create and update remote-tracking branches `/`. + +If the repository is already a shallow repository (see linkgit:git-clone[1] +`--depth`) and neither `-t`, `--mirror` nor `--no-limited-fetch` is given, +`--limited-fetch` is turned on by default, so that `git fetch ` does +not need to negotiate history for every branch the remote has. ++ With `-f` option, `git fetch ` is run immediately after the remote information is set up. + @@ -74,7 +79,8 @@ With `--limited-fetch` option, instead of a `remote..fetch` refspec that tracks all branches, `remote..refmap` is set up so that a refspec-less `git fetch ` only fetches branches our local branches 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 ` option, a symbolic-ref `refs/remotes//HEAD` is set up to point at remote's __ branch. See also the set-head command. diff --git a/builtin/remote.c b/builtin/remote.c index f036dd5d6f..0cf1126976 100644 --- a/builtin/remote.c +++ b/builtin/remote.c @@ -16,6 +16,7 @@ #include "rebase.h" #include "refs.h" #include "refspec.h" +#include "shallow.h" #include "odb.h" #include "strvec.h" #include "commit-reach.h" @@ -238,7 +239,9 @@ static int add(int argc, const char **argv, const char *prefix, if (!mirror || mirror & MIRROR_FETCH) { int use_limited_fetch = mirror == MIRROR_NONE && track.nr == 0 && - limited_fetch == 1; + (limited_fetch == 1 || + (limited_fetch == -1 && + is_repository_shallow(the_repository))); strbuf_reset(&buf); if (use_limited_fetch) { diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh index 0168d5abfe..f3b5905e9e 100755 --- a/t/t5505-remote.sh +++ b/t/t5505-remote.sh @@ -137,6 +137,66 @@ test_expect_success 'filters are listed by git remote -v only' ' test_grep ! "\[blob:none\]" out ' +test_expect_success 'add remote -t keeps an explicit refspec in a shallow repository' ' + test_when_finished "rm -rf shallow-add" && + git clone --no-local --depth=1 --branch main --single-branch \ + one shallow-add && + ( + cd shallow-add && + git remote add -t main upstream ../two && + test_cmp_config "+refs/heads/main:refs/remotes/upstream/main" \ + remote.upstream.fetch + ) +' + +test_expect_success 'add remote keeps the wildcard refspec in a full repository' ' + test_when_finished "rm -rf full-add" && + git clone --no-local one full-add && + ( + cd full-add && + git remote add upstream ../two && + test_cmp_config "+refs/heads/*:refs/remotes/upstream/*" \ + remote.upstream.fetch + ) +' + +test_expect_success 'a remote added in a shallow repository defaults to --limited-fetch' ' + test_when_finished "rm -rf shallow-add" && + git clone --no-local --depth=1 --branch main --single-branch \ + one shallow-add && + ( + cd shallow-add && + git remote add upstream ../two && + 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 + ) +' + +test_expect_success '--no-limited-fetch overrides the shallow-repository default' ' + test_when_finished "rm -rf shallow-add" && + git clone --no-local --depth=1 --branch main --single-branch \ + one shallow-add && + ( + cd shallow-add && + git remote add --no-limited-fetch upstream ../two && + test_cmp_config "+refs/heads/*:refs/remotes/upstream/*" \ + remote.upstream.fetch && + test_must_fail git config get remote.upstream.refmap + ) +' + test_expect_success '--limited-fetch works in a full repository too' ' test_when_finished "rm -rf full-add" && git clone --no-local one full-add &&