[PATCH v13 1/6] fetch: add remote.<name>.fetchRefmap
flat view
WARM1d
From: Harald Nordgren via GitGitGadget <hidden>
Date: 2026-10-30 10:52:41
Subsystem:
documentation, the rest · Maintainers:
Jonathan Corbet, Linus Torvalds
Revision v13 (11 revisions archived).
Revisions (11)
From: Harald Nordgren <redacted> Add a per-remote config variable, remote.<name>.fetchRefmap, that provides the default value for --refmap the same way remote.<name>.fetch already provides the default refspecs to fetch. Unlike passing --refmap on the command line, which requires an explicit refspec to go with it, configuring remote.<name>.fetchRefmap on its own does not change what a refspec-less fetch does: it falls back to the same defaults as a remote with no fetchRefmap at all. When remote.<name>.fetch is also configured, it still decides what to fetch, and the refmap maps what it fetches. This lets a remote name only a few branches and store them all through one refmap: [remote "cabal"] fetch = master fetch = next fetch = seen fetchRefmap = +refs/heads/*:refs/remotes/cabal/* Looking up the remote-tracking branch for a remote branch, as @{upstream} and "git branch --track" do, falls back to the refmap when remote.<name>.fetch has no match, so that these work for such branches too. A branch excluded by a negative refspec in remote.<name>.fetch stays excluded. Signed-off-by: Harald Nordgren <redacted> --- Documentation/config/remote.adoc | 7 ++ Documentation/fetch-options.adoc | 3 + builtin/fetch.c | 16 +++- remote.c | 20 +++- remote.h | 6 ++ t/t5510-fetch.sh | 151 +++++++++++++++++++++++++++++++ 6 files changed, 200 insertions(+), 3 deletions(-)
diff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc
index 3a20d0f752..236ccca93e 100644
--- a/Documentation/config/remote.adoc
+++ b/Documentation/config/remote.adoc@@ -33,6 +33,13 @@ remote.<name>.fetch:: The default set of "refspec" for linkgit:git-fetch[1]. See linkgit:git-fetch[1]. +remote.<name>.fetchRefmap:: + The default value of the `--refmap` option, used to map refs being + fetched to remote-tracking refs to store, the same way + `remote.<name>.fetch` does for an explicit refspec. + `remote.<name>.fetch`, if also configured, still decides what is + fetched. See the `--refmap` entry in linkgit:git-fetch[1]. + remote.<name>.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 47dea1de8e..c2101a7b39 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.<name>.refmap` provides the default value for this option, the +same way `remote.<name>.fetch` provides the default refspecs to fetch. `-t`:: `--tags`::
diff --git a/builtin/fetch.c b/builtin/fetch.c
index 1f76ae6242..305934f5f4 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c@@ -518,6 +518,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.<name>.fetchRefmap. + */ + struct refspec *effective_refmap = + refmap.nr ? &refmap : remote ? &remote->refmap : NULL; /* opportunistically-updated references: */ struct ref *orefs = NULL, **oref_tail = &orefs;
@@ -528,6 +534,8 @@ static struct ref *get_ref_map(struct remote *remote, filter_prefetch_refspec(rs); if (remote) filter_prefetch_refspec(&remote->fetch); + if (effective_refmap) + filter_prefetch_refspec(effective_refmap); if (rs->nr) { struct refspec *fetch_refspec;
@@ -561,8 +569,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;
@@ -597,6 +605,10 @@ static struct ref *get_ref_map(struct remote *remote, if (has_merge && !strcmp(branch->remote_name, remote->name)) add_merge_config(&ref_map, remote_refs, branch, &tail); + + for (i = 0; i < remote->refmap.nr; i++) + get_fetch_map(ref_map, &remote->refmap.items[i], + &oref_tail, 1); } else if (!prefetch) { ref_map = get_remote_ref(remote_refs, "HEAD"); if (!ref_map)
diff --git a/remote.c b/remote.c
index 49ee2ea318..9acab2e01a 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, "fetchrefmap")) { + 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))
@@ -995,7 +1003,17 @@ 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); + char **result = refspec->src ? &refspec->dst : &refspec->src; + + if (remote->fetch.nr && !refspec_find_match(&remote->fetch, refspec)) + return 0; + if (refspec_find_match(&remote->refmap, refspec)) + return -1; + if (refname_matches_negative_refspec_item(refspec->src, &remote->fetch)) { + FREE_AND_NULL(*result); + return -1; + } + return 0; } 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 17c0be499c..b5eba020ad 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh@@ -927,6 +927,157 @@ test_expect_success 'explicit --refmap option overrides remote.*.fetch' ' ) ' +test_expect_success 'remote.*.fetchRefmap acts like --refmap on the command line' ' + test_when_finished "git -C three config --unset remote.origin.fetchRefmap" && + git branch -f side && + git -C three config remote.origin.fetchRefmap \ + "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.<name>.fetchRefmap 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.fetchRefmap "+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.<name>.fetch + # wins: a refspec-less fetch follows it as usual, and + # remote.<name>.fetchRefmap plays no part in deciding what to + # fetch, only in mapping what is fetched. + 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 'remote.<name>.fetchRefmap stores branches named in remote.<name>.fetch' ' + test_when_finished "rm -fr fetch-refmap-upstream downstream" && + git init -b master fetch-refmap-upstream && + test_commit -C fetch-refmap-upstream base && + git -C fetch-refmap-upstream branch next && + git -C fetch-refmap-upstream branch seen && + git -C fetch-refmap-upstream branch other && + git init downstream && + ( + cd downstream && + git remote add cabal ../fetch-refmap-upstream && + git config --unset-all remote.cabal.fetch && + git config --add remote.cabal.fetch master && + git config --add remote.cabal.fetch next && + git config --add remote.cabal.fetch seen && + git config remote.cabal.fetchRefmap "+refs/heads/*:refs/remotes/cabal/*" && + git fetch cabal && + check_fetched_refs <<-\EOF && + refs/remotes/cabal/HEAD + refs/remotes/cabal/master + refs/remotes/cabal/next + refs/remotes/cabal/seen + EOF + git branch --track topic cabal/next && + echo refs/remotes/cabal/next >expect && + git rev-parse --symbolic-full-name topic@{upstream} >actual && + test_cmp expect actual + ) +' + +test_expect_success 'remote.<name>.fetchRefmap stores an explicit-dst remote.<name>.fetch entry in both places' ' + test_when_finished "rm -fr fetch-refmap-upstream downstream" && + git init -b master fetch-refmap-upstream && + test_commit -C fetch-refmap-upstream base && + git init downstream && + ( + cd downstream && + git remote add cabal ../fetch-refmap-upstream && + git config --unset-all remote.cabal.fetch && + git config --add remote.cabal.fetch \ + "+refs/heads/master:refs/remotes/custom/master" && + git config remote.cabal.fetchRefmap "+refs/heads/*:refs/remotes/cabal/*" && + git fetch cabal && + check_fetched_refs <<-\EOF + refs/remotes/cabal/HEAD + refs/remotes/cabal/master + refs/remotes/custom/master + EOF + ) +' + +test_expect_success 'remote.<name>.fetchRefmap tracks a branch outside remote.<name>.fetch' ' + test_when_finished "rm -fr fetch-refmap-upstream downstream" && + git init -b main fetch-refmap-upstream && + test_commit -C fetch-refmap-upstream base && + git -C fetch-refmap-upstream branch other && + git init downstream && + ( + cd downstream && + git remote add -t main origin ../fetch-refmap-upstream && + git config remote.origin.fetchRefmap "+refs/heads/*:refs/remotes/origin/*" && + git fetch origin other && + git branch --track topic origin/other && + test_cmp_config origin branch.topic.remote && + test_cmp_config refs/heads/other branch.topic.merge && + echo refs/remotes/origin/other >expect && + git rev-parse --symbolic-full-name topic@{upstream} >actual && + test_cmp expect actual + ) +' + +test_expect_success 'remote.<name>.fetchRefmap does not track a branch excluded by remote.<name>.fetch' ' + test_when_finished "rm -fr fetch-refmap-upstream downstream" && + git init -b main fetch-refmap-upstream && + test_commit -C fetch-refmap-upstream base && + git -C fetch-refmap-upstream branch secret && + git init downstream && + ( + cd downstream && + git remote add origin ../fetch-refmap-upstream && + git config --add remote.origin.fetch "^refs/heads/secret" && + git config remote.origin.fetchRefmap "+refs/heads/*:refs/remotes/origin/*" && + git fetch origin secret && + git rev-parse --verify refs/remotes/origin/secret && + test_must_fail git branch --track topic origin/secret 2>err && + test_grep "not a branch" err + ) +' + test_expect_success 'explicitly empty --refmap option disables remote.*.fetch' ' git branch -f side && (
--
gitgitgadget