fetch: add configuration variable fetch.followRemoteHEAD

'fetch.followRemoteHEAD' is added as a generic setting used by all remotes for which 'remote.<name>.followRemoteHEAD' is undefined. If both variables are undefined, a builtin default of "create" is in effect, matching the previous behavior. As mentioned in the previous patch, 'fetch.followRemoteHEAD' supports all of the values that its 'remote' counterpart does _except_ warn-if-not-$branch, due to its tighter coupling to individual remote repositories. This setting interacts with the do_fetch mechanism in the same way as the previous does, but there are opportunities for improved user-experience discussed in [1]. See the included NEEDSWORK comment as well. Documentation and advice messages for both of the followRemoteHEAD variables are reworded to better capture the relationship between the two. The added tests assert feature parity between the two followRemoteHEAD variables, as well as the fact that 'remote.<name>.followRemoteHEAD' always supersedes this new configurable default. [1]: https://lore.kernel.org/git/xmqqh5n213bw.fsf@gitster.g/ Helped-by: Junio C Hamano <gitster@pobox.com> Signed-off-by: Matt Hunter <m@lfurio.us> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Matt Hunter committed Jun 19, 2026 at 05:44 UTC 7d00999b10579708fc3c2512cb0a55eff4e7565f
4 files changed +169 -17
Documentation/config/fetch.adoc
+19
@@ -126,3 +126,22 @@ the new bundle URI.
126 The creation token values are chosen by the provider serving the specific
127 bundle URI. If you modify the URI at `fetch.bundleURI`, then be sure to
128 remove the value for the `fetch.bundleCreationToken` value before fetching.
129 +
130 +`fetch.followRemoteHEAD`::
131 + When fetching using a default refspec, this setting determines how to handle
132 + differences between a fetched remote's `HEAD` and the local
133 + `remotes/<name>/HEAD` symbolic-ref. Its value is one of
134 ++
135 +--
136 +`create`;;
137 + Create `remotes/<name>/HEAD` if a ref exists on the remote, but not locally.
138 + An existing symbolic-ref will not be touched. This is the default value.
139 +`warn`;;
140 + Display a warning if the remote advertises a different `HEAD` than what is
141 + set locally. Behaves like "create" if the local symbolic-ref doesn't exist.
142 +`always`;;
143 + Silently update `remotes/<name>/HEAD` whenever the remote advertises a new
144 + value.
145 +`never`;;
146 + Never create or modify the `remotes/<name>/HEAD` symbolic-ref.
147 +--
Documentation/config/remote.adoc
+9 -12
@@ -157,15 +157,12 @@ Blank values signal to ignore all previous values, allowing a reset of
157 the list from broader config scenarios.
158
159 remote.<name>.followRemoteHEAD::
160 - How linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`
161 - when fetching using the configured refspecs of a remote.
162 - The default value is "create", which will create `remotes/<name>/HEAD`
163 - if it exists on the remote, but not locally; this will not touch an
164 - already existing local reference. Setting it to "warn" will print
165 - a message if the remote has a different value than the local one;
166 - in case there is no local reference, it behaves like "create".
167 - A variant on "warn" is "warn-if-not-$branch", which behaves like
168 - "warn", but if `HEAD` on the remote is `$branch` it will be silent.
169 - Setting it to "always" will silently update `remotes/<name>/HEAD` to
170 - the value on the remote. Finally, setting it to "never" will never
171 - change or create the local reference.
160 + When fetching this remote using its default refspec, this setting determines
161 + how to handle differences between the remote's `HEAD` and the local
162 + `remotes/<name>/HEAD` symbolic-ref. Overrides the value of
163 + `fetch.followRemoteHEAD`. See `fetch.followRemoteHEAD` for a description of
164 + accepted values.
165 ++
166 +In addition to the values supported by `fetch.followRemoteHEAD`, this setting
167 +may also take on the value "warn-if-not-`$branch`", which behaves like "warn",
168 +but ignores the warning if the remote's `HEAD` is `remotes/<name>/$branch`.
builtin/fetch.c
+36 -5
@@ -103,6 +103,7 @@ static struct string_list negotiation_include = STRING_LIST_INIT_NODUP;
103
104 struct fetch_config {
105 enum display_format display_format;
106 + enum follow_remote_head_settings follow_remote_head;
107 int all;
108 int prune;
109 int prune_tags;
@@ -174,6 +175,22 @@ static int git_fetch_config(const char *k, const char *v,
175 return 0;
176 }
177
178 + if (!strcmp(k, "fetch.followremotehead")) {
179 + if (!v)
180 + return config_error_nonbool(k);
181 + else if (!strcmp(v, "never"))
182 + fetch_config->follow_remote_head = FOLLOW_REMOTE_NEVER;
183 + else if (!strcmp(v, "create"))
184 + fetch_config->follow_remote_head = FOLLOW_REMOTE_CREATE;
185 + else if (!strcmp(v, "warn"))
186 + fetch_config->follow_remote_head = FOLLOW_REMOTE_WARN;
187 + else if (!strcmp(v, "always"))
188 + fetch_config->follow_remote_head = FOLLOW_REMOTE_ALWAYS;
189 + else
190 + warning(_("unrecognized fetch.followRemoteHEAD value '%s' ignored"), v);
191 + return 0;
192 + }
193 +
194 return git_default_config(k, v, ctx, cb);
195 }
196
@@ -1698,11 +1715,13 @@ static const char *strip_refshead(const char *name){
1715 static void set_head_advice_msg(const char *remote, const char *head_name)
1716 {
1717 const char message_advice_set_head[] =
1701 - N_("Run 'git remote set-head %s %s' to follow the change, or set\n"
1702 - "'remote.%s.followRemoteHEAD' configuration option to a different value\n"
1703 - "if you do not want to see this message. Specifically running\n"
1704 - "'git config set remote.%s.followRemoteHEAD warn-if-not-%s'\n"
1705 - "will disable the warning until the remote changes HEAD to something else.");
1718 + N_("Run 'git remote set-head %s %s' to follow the change, or modify\n"
1719 + "either of the 'remote.%s.followRemoteHEAD' or 'fetch.followRemoteHEAD'\n"
1720 + "configuration variables to handle the situation differently.\n\n"
1721 +
1722 + "Using this specific setting\n\n"
1723 + " git config set remote.%s.followRemoteHEAD warn-if-not-%s\n\n"
1724 + "will suppress the warning until the remote changes HEAD to something else.");
1725
1726 advise_if_enabled(ADVICE_FETCH_SET_HEAD_WARN, _(message_advice_set_head),
1727 remote, head_name, remote, remote, head_name);
@@ -1918,8 +1937,19 @@ static int do_fetch(struct transport *transport,
1937 goto cleanup;
1938 }
1939
1940 + /*
1941 + * NEEDSWORK: By the time this function executes, we have already parsed
1942 + * all such followRemoteHEAD values from the external configuration,
1943 + * potentially emitting warning messages for bogus values. Ideally, if
1944 + * this fetch ends up not needing to consult these values, then git would
1945 + * not ever output a value warning. (eg: when pulling from a URL directly -
1946 + * rather than a configured remote, or when a remote's followRemoteHEAD
1947 + * overrides the fallback fetch setting)
1948 + */
1949 if (transport->remote->follow_remote_head)
1950 follow_remote_head = transport->remote->follow_remote_head;
1951 + else if (config->follow_remote_head)
1952 + follow_remote_head = config->follow_remote_head;
1953 else
1954 follow_remote_head = BUILTIN_FOLLOW_REMOTE_HEAD_DFLT;
1955
@@ -2478,6 +2508,7 @@ int cmd_fetch(int argc,
2508 {
2509 struct fetch_config config = {
2510 .display_format = DISPLAY_FORMAT_FULL,
2511 + .follow_remote_head = FOLLOW_REMOTE_UNCONFIGURED,
2512 .prune = -1,
2513 .prune_tags = -1,
2514 .show_forced_updates = 1,
t/t5510-fetch.sh
+105
@@ -140,6 +140,16 @@ test_expect_success "fetch test remote HEAD change" '
140 )
141 '
142
143 +test_expect_success "fetch test default followRemoteHEAD never" '
144 + git -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&
145 + test_config -C two fetch.followRemoteHEAD "never" &&
146 + GIT_TRACE_PACKET=$PWD/trace.out git -C two fetch &&
147 + # Confirm that we do not even ask for HEAD when we are
148 + # not going to act on it.
149 + test_grep ! "ref-prefix HEAD" trace.out &&
150 + test_must_fail git -C two rev-parse --verify refs/remotes/origin/HEAD
151 +'
152 +
153 test_expect_success "fetch test followRemoteHEAD never" '
154 git -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&
155 test_config -C two remote.origin.followRemoteHEAD "never" &&
@@ -150,6 +160,21 @@ test_expect_success "fetch test followRemoteHEAD never" '
160 test_must_fail git -C two rev-parse --verify refs/remotes/origin/HEAD
161 '
162
163 +test_expect_success "fetch test default followRemoteHEAD warn no change" '
164 + git -C two rev-parse --verify refs/remotes/origin/other &&
165 + git -C two remote set-head origin other &&
166 + git -C two rev-parse --verify refs/remotes/origin/HEAD &&
167 + git -C two rev-parse --verify refs/remotes/origin/main &&
168 + test_config -C two fetch.followRemoteHEAD "warn" &&
169 + git -C two fetch >output &&
170 + echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
171 + "but we have ${SQ}other${SQ} locally." >expect &&
172 + test_cmp expect output &&
173 + head=$(git -C two rev-parse refs/remotes/origin/HEAD) &&
174 + branch=$(git -C two rev-parse refs/remotes/origin/other) &&
175 + test "z$head" = "z$branch"
176 +'
177 +
178 test_expect_success "fetch test followRemoteHEAD warn no change" '
179 git -C two rev-parse --verify refs/remotes/origin/other &&
180 git -C two remote set-head origin other &&
@@ -165,6 +190,17 @@ test_expect_success "fetch test followRemoteHEAD warn no change" '
190 test "z$head" = "z$branch"
191 '
192
193 +test_expect_success "fetch test default followRemoteHEAD warn create" '
194 + git -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&
195 + test_config -C two fetch.followRemoteHEAD "warn" &&
196 + git -C two rev-parse --verify refs/remotes/origin/main &&
197 + output=$(git -C two fetch) &&
198 + test "z" = "z$output" &&
199 + head=$(git -C two rev-parse refs/remotes/origin/HEAD) &&
200 + branch=$(git -C two rev-parse refs/remotes/origin/main) &&
201 + test "z$head" = "z$branch"
202 +'
203 +
204 test_expect_success "fetch test followRemoteHEAD warn create" '
205 git -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&
206 test_config -C two remote.origin.followRemoteHEAD "warn" &&
@@ -176,6 +212,18 @@ test_expect_success "fetch test followRemoteHEAD warn create" '
212 test "z$head" = "z$branch"
213 '
214
215 +test_expect_success "fetch test default followRemoteHEAD warn detached" '
216 + git -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&
217 + git -C two update-ref refs/remotes/origin/HEAD HEAD &&
218 + HEAD=$(git -C two log --pretty="%H") &&
219 + test_config -C two fetch.followRemoteHEAD "warn" &&
220 + git -C two fetch >output &&
221 + echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
222 + "but we have a detached HEAD pointing to" \
223 + "${SQ}${HEAD}${SQ} locally." >expect &&
224 + test_cmp expect output
225 +'
226 +
227 test_expect_success "fetch test followRemoteHEAD warn detached" '
228 git -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&
229 git -C two update-ref refs/remotes/origin/HEAD HEAD &&
@@ -188,6 +236,19 @@ test_expect_success "fetch test followRemoteHEAD warn detached" '
236 test_cmp expect output
237 '
238
239 +test_expect_success "fetch test default followRemoteHEAD warn quiet" '
240 + git -C two rev-parse --verify refs/remotes/origin/other &&
241 + git -C two remote set-head origin other &&
242 + git -C two rev-parse --verify refs/remotes/origin/HEAD &&
243 + git -C two rev-parse --verify refs/remotes/origin/main &&
244 + test_config -C two fetch.followRemoteHEAD "warn" &&
245 + output=$(git -C two fetch --quiet) &&
246 + test "z" = "z$output" &&
247 + head=$(git -C two rev-parse refs/remotes/origin/HEAD) &&
248 + branch=$(git -C two rev-parse refs/remotes/origin/other) &&
249 + test "z$head" = "z$branch"
250 +'
251 +
252 test_expect_success "fetch test followRemoteHEAD warn quiet" '
253 git -C two rev-parse --verify refs/remotes/origin/other &&
254 git -C two remote set-head origin other &&
@@ -229,6 +290,18 @@ test_expect_success "fetch test followRemoteHEAD warn-if-not-branch branch is di
290 test "z$head" = "z$branch"
291 '
292
293 +test_expect_success "fetch test default followRemoteHEAD always" '
294 + git -C two rev-parse --verify refs/remotes/origin/other &&
295 + git -C two remote set-head origin other &&
296 + git -C two rev-parse --verify refs/remotes/origin/HEAD &&
297 + git -C two rev-parse --verify refs/remotes/origin/main &&
298 + test_config -C two fetch.followRemoteHEAD "always" &&
299 + git -C two fetch &&
300 + head=$(git -C two rev-parse refs/remotes/origin/HEAD) &&
301 + branch=$(git -C two rev-parse refs/remotes/origin/main) &&
302 + test "z$head" = "z$branch"
303 +'
304 +
305 test_expect_success "fetch test followRemoteHEAD always" '
306 git -C two rev-parse --verify refs/remotes/origin/other &&
307 git -C two remote set-head origin other &&
@@ -241,6 +314,28 @@ test_expect_success "fetch test followRemoteHEAD always" '
314 test "z$head" = "z$branch"
315 '
316
317 +test_expect_success 'per-remote followRemoteHEAD takes priority over fetch default' '
318 + git -C two rev-parse --verify refs/remotes/origin/other &&
319 + git -C two remote set-head origin other &&
320 + git -C two rev-parse --verify refs/remotes/origin/HEAD &&
321 + git -C two rev-parse --verify refs/remotes/origin/main &&
322 + test_config -C two fetch.followRemoteHEAD "never" &&
323 + test_config -C two remote.origin.followRemoteHEAD "always" &&
324 + git -C two fetch &&
325 + head=$(git -C two rev-parse refs/remotes/origin/HEAD) &&
326 + branch=$(git -C two rev-parse refs/remotes/origin/main) &&
327 + test "z$head" = "z$branch"
328 +'
329 +
330 +test_expect_success 'default followRemoteHEAD does not kick in with refspecs' '
331 + git -C two remote set-head origin other &&
332 + test_config -C two fetch.followRemoteHEAD always &&
333 + git -C two fetch origin refs/heads/main:refs/remotes/origin/main &&
334 + echo refs/remotes/origin/other >expect &&
335 + git -C two symbolic-ref refs/remotes/origin/HEAD >actual &&
336 + test_cmp expect actual
337 +'
338 +
339 test_expect_success 'followRemoteHEAD does not kick in with refspecs' '
340 git -C two remote set-head origin other &&
341 test_config -C two remote.origin.followRemoteHEAD always &&
@@ -250,6 +345,16 @@ test_expect_success 'followRemoteHEAD does not kick in with refspecs' '
345 test_cmp expect actual
346 '
347
348 +test_expect_success 'default followRemoteHEAD create does not overwrite dangling symref' '
349 + test_when_finished "git -C two remote remove custom-head" &&
350 + git -C two remote add -m does-not-exist custom-head ../one &&
351 + test_config -C two fetch.followRemoteHEAD create &&
352 + git -C two fetch custom-head &&
353 + echo refs/remotes/custom-head/does-not-exist >expect &&
354 + git -C two symbolic-ref refs/remotes/custom-head/HEAD >actual &&
355 + test_cmp expect actual
356 +'
357 +
358 test_expect_success 'followRemoteHEAD create does not overwrite dangling symref' '
359 test_when_finished "git -C two remote remove custom-head" &&
360 git -C two remote add -m does-not-exist custom-head ../one &&