{"thread":{"id":"65759","subject":"followRemoteHEAD management question","startedAt":"2026-06-05T16:31:31Z","lastAt":"2026-06-19T09:48:35Z","messageCount":38,"participants":["Matt Hunter","Jeff King","Bence Ferdinandy","Junio C Hamano"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"544802","messageId":"DJ19CI50W6UH.17QLIBNTXBWXU@lfurio.us","threadId":"65759","inReplyTo":null,"subject":"followRemoteHEAD management question","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-05T16:31:30Z","receivedAt":"2026-06-05T16:31:31Z","isPatch":false,"body":"Hello git list,\n\nIn the past, I've preferred to run 'git remote set-head <name> -d' when\nsetting up a new repository, since I generally have an awareness of what\nthe remote default branch is, and I don't like seeing them in branch\nlistings or git-log annotations.  They are especially noisy to me if I\nhave multiple remotes.  It's possible this config is ill-advised - I\nwould love to be educated if so...\n\nHowever, since b7f7d16562c3 (fetch: add configuration for set_head\nbehaviour), these changes are undone by every 'git fetch'.\n\nThe topic mentioned above (merged in a1f34d595503) adds a new\nconfiguration key 'remote.<name>.followRemoteHEAD'.  I'm assuming that\nthe intended use for followRemoteHEAD is really only in local /\nper-repository config, since trying to apply it to my personal\n.gitconfig has some odd behavior.\n\nThe <name> in the key template does not accept a wildcard, so I must\nlist out each of the common remote names I use across different\nrepositories.  Since many of my repos don't actually have remotes\nestablished for all of these names, they pick up a kind of half-baked\ndefinition for each of them as git performs its config parsing.  For\ninstance, a name will appear under 'git remote -v', but it won't\nhave any actual properties configured.\n\nI'd like to add a line to my config somewhere that can globally restore\nthe old behavior in this context, eg:\n\n    git config --global remote.*.followRemoteHEAD never\n\ninstead of adding individual entries to each project's .git/config.\n\nIs there another solution in place I've missed?  If not, would there be\nany opposition to a new key like 'remote.followRemoteHEAD' which serves\nto provide a default value for any remote that doesn't have its own\n'remote.<name>.followRemoteHEAD' key?\n\nI've started scouting out changes to make for such a patch.  It's not\nready yet, but I figured I would throw this question out in case an easy\nanswer can save the effort.\n\nThanks\n"},{"id":"544983","messageId":"20260608234946.GB358144@coredump.intra.peff.net","threadId":"65759","inReplyTo":"DJ19CI50W6UH.17QLIBNTXBWXU@lfurio.us","subject":"Re: followRemoteHEAD management question","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-06-08T23:49:46Z","receivedAt":"2026-06-08T23:49:48Z","isPatch":false,"body":"On Fri, Jun 05, 2026 at 12:31:30PM -0400, Matt Hunter wrote:\n\n> In the past, I've preferred to run 'git remote set-head <name> -d' when\n> setting up a new repository, since I generally have an awareness of what\n> the remote default branch is, and I don't like seeing them in branch\n> listings or git-log annotations.  They are especially noisy to me if I\n> have multiple remotes.  It's possible this config is ill-advised - I\n> would love to be educated if so...\n\nNo, it's perfectly reasonable. Being able to refer to \"origin\" to mean\n\"origin/HEAD\" is sometimes handy, but if you don't use it, there's no\nreason to set up the symref in the first place.\n\n> However, since b7f7d16562c3 (fetch: add configuration for set_head\n> behaviour), these changes are undone by every 'git fetch'.\n> \n> The topic mentioned above (merged in a1f34d595503) adds a new\n> configuration key 'remote.<name>.followRemoteHEAD'.  I'm assuming that\n> the intended use for followRemoteHEAD is really only in local /\n> per-repository config, since trying to apply it to my personal\n> .gitconfig has some odd behavior.\n\nI think this is a gap in the new feature's implementation. It added\nper-remote config, but there is no global config to fall back to (e.g.,\nthe way that remote.*.prune falls back to fetch.prune). There should be\na fetch.followRemoteHEAD option (or perhaps remote.followRemoteHEAD).\n\n> The <name> in the key template does not accept a wildcard, so I must\n> list out each of the common remote names I use across different\n> repositories.  Since many of my repos don't actually have remotes\n> established for all of these names, they pick up a kind of half-baked\n> definition for each of them as git performs its config parsing.  For\n> instance, a name will appear under 'git remote -v', but it won't\n> have any actual properties configured.\n\nYes, this is a common problem with the remote-config namespace. Defining\n_any_ key makes the remote \"exist\", even without a defined url, but that\nisn't usually the intent.  But we can't distinguish that from the case\nwhere you really do want to define a remote without a url (in which case\nthe url is the name of the remote).\n\n> Is there another solution in place I've missed?  If not, would there be\n> any opposition to a new key like 'remote.followRemoteHEAD' which serves\n> to provide a default value for any remote that doesn't have its own\n> 'remote.<name>.followRemoteHEAD' key?\n> \n> I've started scouting out changes to make for such a patch.  It's not\n> ready yet, but I figured I would throw this question out in case an easy\n> answer can save the effort.\n\nI think you are on the right track. I can see arguments for or against\nputting it in fetch.* or remote.*, so you'll have to pick one. ;)\n\n-Peff\n"},{"id":"545224","messageId":"DJ5XE9HC5YNY.33U8AG1GX6ZP0@lfurio.us","threadId":"65759","inReplyTo":"20260608234946.GB358144@coredump.intra.peff.net","subject":"Re: followRemoteHEAD management question","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-11T04:12:54Z","receivedAt":"2026-06-11T04:12:59Z","isPatch":false,"body":"On Mon Jun 8, 2026 at 7:49 PM EDT, Jeff King wrote:\n>> \n>> The topic mentioned above (merged in a1f34d595503) adds a new\n>> configuration key 'remote.<name>.followRemoteHEAD'.  I'm assuming that\n>> the intended use for followRemoteHEAD is really only in local /\n>> per-repository config, since trying to apply it to my personal\n>> .gitconfig has some odd behavior.\n>\n> I think this is a gap in the new feature's implementation. It added\n> per-remote config, but there is no global config to fall back to (e.g.,\n> the way that remote.*.prune falls back to fetch.prune). There should be\n> a fetch.followRemoteHEAD option (or perhaps remote.followRemoteHEAD).\n\nEarlier on while working on this, I actually settled on\nfetch.followRemoteHEAD instead, taking example from the prune setting.\nThanks for the confirmation.\n\n>> The <name> in the key template does not accept a wildcard, so I must\n>> list out each of the common remote names I use across different\n>> repositories.  Since many of my repos don't actually have remotes\n>> established for all of these names, they pick up a kind of half-baked\n>> definition for each of them as git performs its config parsing.  For\n>> instance, a name will appear under 'git remote -v', but it won't\n>> have any actual properties configured.\n>\n> Yes, this is a common problem with the remote-config namespace. Defining\n> _any_ key makes the remote \"exist\", even without a defined url, but that\n> isn't usually the intent.  But we can't distinguish that from the case\n> where you really do want to define a remote without a url (in which case\n> the url is the name of the remote).\n\nI had no idea a remote like that was supported.  Interesting.\n\n>> Is there another solution in place I've missed?  If not, would there be\n>> any opposition to a new key like 'remote.followRemoteHEAD' which serves\n>> to provide a default value for any remote that doesn't have its own\n>> 'remote.<name>.followRemoteHEAD' key?\n>> \n>> I've started scouting out changes to make for such a patch.  It's not\n>> ready yet, but I figured I would throw this question out in case an easy\n>> answer can save the effort.\n>\n> I think you are on the right track. I can see arguments for or against\n> putting it in fetch.* or remote.*, so you'll have to pick one. ;)\n\nAs stated, I think putting it in fetch.* is more consistent.  I'd be\ncurious to hear arguments the other way.\n\nAs for another design decision: I'm leaning toward omitting support for\nthe \"warn-if-not-$branch\" value in fetch.followRemoteHEAD.\n\nMy take on that option as-documented is that it serves more as an\nacknowledgment from the user that \"yes, I understand that origin has\npointed HEAD at foo, please only warn me if it changes\" as opposed to the\nuser expressing that the branch \"foo\" is in some way special to them.\n\nThis interpretation feels very remote-dependent and doesn't make sense in\nthe context of a default catch-all value to me.\n\nThanks for the feedback!\n"},{"id":"545226","messageId":"20260611060123.GA2187173@coredump.intra.peff.net","threadId":"65759","inReplyTo":"DJ5XE9HC5YNY.33U8AG1GX6ZP0@lfurio.us","subject":"Re: followRemoteHEAD management question","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2026-06-11T06:01:23Z","receivedAt":"2026-06-11T06:01:31Z","isPatch":false,"body":"On Thu, Jun 11, 2026 at 12:12:54AM -0400, Matt Hunter wrote:\n\n> > Yes, this is a common problem with the remote-config namespace. Defining\n> > _any_ key makes the remote \"exist\", even without a defined url, but that\n> > isn't usually the intent.  But we can't distinguish that from the case\n> > where you really do want to define a remote without a url (in which case\n> > the url is the name of the remote).\n> \n> I had no idea a remote like that was supported.  Interesting.\n\nI suspect it is more of an emergent property than something that was\ncarefully designed, but after so many years I'd hesitate to change it\n(at least without a big warning and deprecation period).\n\n> > I think you are on the right track. I can see arguments for or against\n> > putting it in fetch.* or remote.*, so you'll have to pick one. ;)\n> \n> As stated, I think putting it in fetch.* is more consistent.  I'd be\n> curious to hear arguments the other way.\n\nMy initial thought is that it might affect clone as well as fetch. But I\nguess this feature does not kick in for clone, as it has its own logic\nfor handling the remote-tracking HEAD. Though arguably it should be\npossible to configure it not to create one in the first place.\n\n> As for another design decision: I'm leaning toward omitting support for\n> the \"warn-if-not-$branch\" value in fetch.followRemoteHEAD.\n> \n> My take on that option as-documented is that it serves more as an\n> acknowledgment from the user that \"yes, I understand that origin has\n> pointed HEAD at foo, please only warn me if it changes\" as opposed to the\n> user expressing that the branch \"foo\" is in some way special to them.\n> \n> This interpretation feels very remote-dependent and doesn't make sense in\n> the context of a default catch-all value to me.\n\nAgreed. I can't think of a reason you'd want it in the global option.\nAnd if we're wrong, it is easy to add support later (versus adding it\nnow, finding out that it creates awkward corner cases, and then having\nthe backwards-incompatible change of ripping it out).\n\n-Peff\n"},{"id":"545313","messageId":"DJ6IBPYNOTTY.3QKEZQ28P713V@ferdinandy.com","threadId":"65759","inReplyTo":"20260611060123.GA2187173@coredump.intra.peff.net","subject":"Re: followRemoteHEAD management question","fromName":"Bence Ferdinandy","fromEmail":"bence@ferdinandy.com","sentAt":"2026-06-11T20:36:58Z","receivedAt":"2026-06-11T20:37:24Z","isPatch":false,"body":"On Thu Jun 11, 2026 at 08:01, Jeff King <peff@peff.net> wrote:\n>\n> My initial thought is that it might affect clone as well as fetch. But I\n> guess this feature does not kick in for clone, as it has its own logic\n> for handling the remote-tracking HEAD. Though arguably it should be\n> possible to configure it not to create one in the first place.\n\nIf memory serves well clone has set the remote/HEAD well before this and\nI think it indeed uses a different mechanism/logic.\n\n>\n>> As for another design decision: I'm leaning toward omitting support for\n>> the \"warn-if-not-$branch\" value in fetch.followRemoteHEAD.\n>> \n>> My take on that option as-documented is that it serves more as an\n>> acknowledgment from the user that \"yes, I understand that origin has\n>> pointed HEAD at foo, please only warn me if it changes\" as opposed to the\n>> user expressing that the branch \"foo\" is in some way special to them.\n\nYes, that was the reasoning. So I also agree on not adding it to global. \n\nBit late to the party, but happy to review/test patches if they come.\n\nBest,\nBence\n"},{"id":"545330","messageId":"20260612055947.1499497-1-m@lfurio.us","threadId":"65759","inReplyTo":"DJ19CI50W6UH.17QLIBNTXBWXU@lfurio.us","subject":"[PATCH 0/7] Introduce fetch.followRemoteHEAD config option","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-12T05:55:36Z","receivedAt":"2026-06-12T06:00:02Z","isPatch":true,"body":"git-fetch presently offers some useful ways to control how remote HEAD\nsymbolic-refs are (or aren't) updated when fetching from remote\nrepositories.  Namely this is done via the\n'remote.<name>.followRemoteHEAD' configuration option.\n\nHowever, this option can be somewhat painful to use if you prefer a\ndefault other than the \"create\" option, and often work with multiple\ndifferent remote repositories.\n\nThis series introduces the option 'fetch.followRemoteHEAD', which\nprovides a configurable default in place of per-remote settings.\n\n'fetch.followRemoteHEAD' functions exactly the same as the original\noption, except that it doesn't allow warning suppression via\n'warn-if-not-$branch'.  Given that different remotes will vary their\nHEAD and set of branches independently, setting a false-positive\nglobally in this way doesn't make logical sense.\n\nWhile it is not mentioned by any of the patches in this series, note\nalso that the behavior introduced by 012bc566bad7 (remote set-head: set\nfollowRemoteHEAD to \"warn\" if \"always\") is unaffected by this series,\nand this feature continues to work for only the\n'remote.<name>.followRemoteHEAD' option.\n\nMatt Hunter (7):\n  fetch: fixup set_head advice for warn-if-not-branch\n  doc: explain fetchRemoteHEADWarn advice\n  t5510: cleanup remote in followRemoteHEAD dangling ref test\n  fetch: rename function report_set_head\n  fetch: refactor do_fetch handling of followRemoteHEAD\n  fetch: add configuration option fetch.followRemoteHEAD\n  fetch: fixup a misaligned comment\n\n Documentation/config/advice.adoc |   4 ++\n Documentation/config/fetch.adoc  |  19 ++++++\n Documentation/config/remote.adoc |  21 +++---\n builtin/fetch.c                  |  52 +++++++++++----\n remote.h                         |  14 ++--\n t/t5510-fetch.sh                 | 106 +++++++++++++++++++++++++++++++\n 6 files changed, 186 insertions(+), 30 deletions(-)\n\n\nbase-commit: 1ff279f3404a482a83fb04c7457e41ab26884aea\n-- \n2.54.0\n\n"},{"id":"545331","messageId":"20260612055947.1499497-2-m@lfurio.us","threadId":"65759","inReplyTo":"20260612055947.1499497-1-m@lfurio.us","subject":"[PATCH 1/7] fetch: fixup set_head advice for warn-if-not-branch","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-12T05:55:37Z","receivedAt":"2026-06-12T06:00:05Z","isPatch":true,"body":"Specifying the word 'branch' in the command is not correct - a mismatch\nwith both the implementation in remote.c and the documentation.\n\nSigned-off-by: Matt Hunter <m@lfurio.us>\n---\n builtin/fetch.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex c1d7c672f4e0..82969e230f5a 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1700,7 +1700,7 @@ static void set_head_advice_msg(const char *remote, const char *head_name)\n \tN_(\"Run 'git remote set-head %s %s' to follow the change, or set\\n\"\n \t   \"'remote.%s.followRemoteHEAD' configuration option to a different value\\n\"\n \t   \"if you do not want to see this message. Specifically running\\n\"\n-\t   \"'git config set remote.%s.followRemoteHEAD warn-if-not-branch-%s'\\n\"\n+\t   \"'git config set remote.%s.followRemoteHEAD warn-if-not-%s'\\n\"\n \t   \"will disable the warning until the remote changes HEAD to something else.\");\n \n \tadvise_if_enabled(ADVICE_FETCH_SET_HEAD_WARN, _(message_advice_set_head),\n-- \n2.54.0\n\n"},{"id":"545332","messageId":"20260612055947.1499497-3-m@lfurio.us","threadId":"65759","inReplyTo":"20260612055947.1499497-1-m@lfurio.us","subject":"[PATCH 2/7] doc: explain fetchRemoteHEADWarn advice","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-12T05:55:38Z","receivedAt":"2026-06-12T06:00:08Z","isPatch":true,"body":"When the user sets 'remote.<name>.followRemoteHEAD' to\n'warn[-if-not-$branch]', git-fetch will report when a fetched HEAD\ndisagrees with the locally-configured remote's HEAD.  This additional\nadvice instructs the user how to deal with these warnings, but was\npreviously undocumented in git-config.\n\nSigned-off-by: Matt Hunter <m@lfurio.us>\n---\n Documentation/config/advice.adoc | 4 ++++\n 1 file changed, 4 insertions(+)\n\ndiff --git a/Documentation/config/advice.adoc b/Documentation/config/advice.adoc\nindex 257db5891817..c3c190ba6a4f 100644\n--- a/Documentation/config/advice.adoc\n+++ b/Documentation/config/advice.adoc\n@@ -48,6 +48,10 @@ all advice messages.\n \t\tto create a local branch after the fact.\n \tdiverging::\n \t\tShown when a fast-forward is not possible.\n+\tfetchRemoteHEADWarn::\n+\t\tShown when linkgit:git-fetch[1] reveals that a remote `HEAD`\n+\t\tdiffers from what is set locally and the user has opted into\n+\t\treceiving a warning in this situation.\n \tfetchShowForcedUpdates::\n \t\tShown when linkgit:git-fetch[1] takes a long time\n \t\tto calculate forced updates after ref updates, or to warn\n-- \n2.54.0\n\n"},{"id":"545333","messageId":"20260612055947.1499497-4-m@lfurio.us","threadId":"65759","inReplyTo":"20260612055947.1499497-1-m@lfurio.us","subject":"[PATCH 3/7] t5510: cleanup remote in followRemoteHEAD dangling ref test","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-12T05:55:39Z","receivedAt":"2026-06-12T06:00:09Z","isPatch":true,"body":"A later patch will introduce a new test which closely mirrors this one.\nUpdate this test to remove the 'custom-head' remote it creates.\nOtherwise, the two tests will conflict with each other, as the second\none to execute will fail to create this remote (which already exists,\nthanks to the first test).\n\nSigned-off-by: Matt Hunter <m@lfurio.us>\n---\n t/t5510-fetch.sh | 1 +\n 1 file changed, 1 insertion(+)\n\ndiff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh\nindex eca9a973b5cb..43190630e714 100755\n--- a/t/t5510-fetch.sh\n+++ b/t/t5510-fetch.sh\n@@ -251,6 +251,7 @@ test_expect_success 'followRemoteHEAD does not kick in with refspecs' '\n '\n \n test_expect_success 'followRemoteHEAD create does not overwrite dangling symref' '\n+\ttest_when_finished \"git -C two remote remove custom-head\" &&\n \tgit -C two remote add -m does-not-exist custom-head ../one &&\n \ttest_config -C two remote.custom-head.followRemoteHEAD create &&\n \tgit -C two fetch custom-head &&\n-- \n2.54.0\n\n"},{"id":"545334","messageId":"20260612055947.1499497-5-m@lfurio.us","threadId":"65759","inReplyTo":"20260612055947.1499497-1-m@lfurio.us","subject":"[PATCH 4/7] fetch: rename function report_set_head","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-12T05:55:40Z","receivedAt":"2026-06-12T06:00:11Z","isPatch":true,"body":"Update to the slightly more obvious name 'warn_set_head', which matches\nthe verbiage of the followRemoteHEAD options.\n\nSigned-off-by: Matt Hunter <m@lfurio.us>\n---\n builtin/fetch.c | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 82969e230f5a..9a45e1e7a44d 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1707,7 +1707,7 @@ static void set_head_advice_msg(const char *remote, const char *head_name)\n \t\t\tremote, head_name, remote, remote, head_name);\n }\n \n-static void report_set_head(const char *remote, const char *head_name,\n+static void warn_set_head(const char *remote, const char *head_name,\n \t\t\tstruct strbuf *buf_prev, int updateres) {\n \tstruct strbuf buf_prefix = STRBUF_INIT;\n \tconst char *prev_head = NULL;\n@@ -1787,7 +1787,7 @@ static int set_head(const struct ref *remote_refs, struct remote *remote)\n \tif (verbosity >= 0 &&\n \t\tfollow_remote_head == FOLLOW_REMOTE_WARN &&\n \t\t(!no_warn_branch || strcmp(no_warn_branch, head_name)))\n-\t\treport_set_head(remote->name, head_name, &b_local_head, was_detached);\n+\t\twarn_set_head(remote->name, head_name, &b_local_head, was_detached);\n \n cleanup:\n \tfree(head_name);\n-- \n2.54.0\n\n"},{"id":"545335","messageId":"20260612055947.1499497-6-m@lfurio.us","threadId":"65759","inReplyTo":"20260612055947.1499497-1-m@lfurio.us","subject":"[PATCH 5/7] fetch: refactor do_fetch handling of followRemoteHEAD","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-12T05:55:41Z","receivedAt":"2026-06-12T06:00:13Z","isPatch":true,"body":"Update enum follow_remote_head_settings to include the value\nFOLLOW_REMOTE_UNCONFIGURED as the new zero-initialized value for\nfollowRemoteHEAD.  This will allow us to distinguish between the option\nbeing unset vs. explicitly set to 'create', which is ultimately the\nsystem default.  The unnecessary indentation is removed.\n\nThe do_fetch function is likewise updated to perform its own decision\nmaking to determine the effective followRemoteHEAD mode, falling back to\nthe system default if necessary.  This will enable the next patch to\nintroduce a user-configurable fallback default option.\n\nFunction set_head now accepts this value as an argument rather than only\nconsidering the value defined by the remote.\n\nThe use of the 'warn-if-not-$branch' value is awkward in the context of\na global default option, since the branches will differ between\nindividual remotes.  For this reason, it's left out of this scheme and\nhandling of the no_warn_branch variable is untouched.  Since a\nremote-specific setting for followRemoteHEAD takes priority, we can\nassume that if remote->no_warn_branch is set, then the remote is also\nasserting FOLLOW_REMOTE_WARN as the effective operating mode, and it\nwill be honored by do_fetch.\n\nSigned-off-by: Matt Hunter <m@lfurio.us>\n---\n builtin/fetch.c | 14 ++++++++++----\n remote.h        | 14 ++++++++------\n 2 files changed, 18 insertions(+), 10 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 9a45e1e7a44d..3cc7efdd83a0 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1729,12 +1729,12 @@ static void warn_set_head(const char *remote, const char *head_name,\n \tstrbuf_release(&buf_prefix);\n }\n \n-static int set_head(const struct ref *remote_refs, struct remote *remote)\n+static int set_head(const struct ref *remote_refs, struct remote *remote,\n+\t\t\tint follow_remote_head)\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-\tint follow_remote_head = remote->follow_remote_head;\n \tconst char *no_warn_branch = remote->no_warn_branch;\n \tchar *head_name = NULL;\n \tstruct ref *ref, *matches;\n@@ -1901,6 +1901,7 @@ static int do_fetch(struct transport *transport,\n \tstruct ref_update_display_info_array display_array = { 0 };\n \tstruct strmap rejected_refs = STRMAP_INIT;\n \tint summary_width = 0;\n+\tint follow_remote_head;\n \n \tif (tags == TAGS_DEFAULT) {\n \t\tif (transport->remote->fetch_tags == 2)\n@@ -1916,6 +1917,11 @@ static int do_fetch(struct transport *transport,\n \t\t\tgoto cleanup;\n \t}\n \n+\tif (transport->remote->follow_remote_head)\n+\t\tfollow_remote_head = transport->remote->follow_remote_head;\n+\telse\n+\t\tfollow_remote_head = BUILTIN_FOLLOW_REMOTE_HEAD_DFLT;\n+\n \tif (rs->nr) {\n \t\trefspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);\n \t} else {\n@@ -1924,7 +1930,7 @@ static int do_fetch(struct transport *transport,\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 (transport->remote->follow_remote_head != FOLLOW_REMOTE_NEVER)\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@@ -2131,7 +2137,7 @@ 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);\n+\t\tset_head(remote_refs, transport->remote, follow_remote_head);\n \t}\n \n cleanup:\ndiff --git a/remote.h b/remote.h\nindex 54b17e4b028b..72a54d84ad51 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -62,12 +62,14 @@ struct remote_state {\n void remote_state_clear(struct remote_state *remote_state);\n struct remote_state *remote_state_new(void);\n \n-\tenum follow_remote_head_settings {\n-\t\tFOLLOW_REMOTE_NEVER = -1,\n-\t\tFOLLOW_REMOTE_CREATE = 0,\n-\t\tFOLLOW_REMOTE_WARN = 1,\n-\t\tFOLLOW_REMOTE_ALWAYS = 2,\n-\t};\n+#define BUILTIN_FOLLOW_REMOTE_HEAD_DFLT FOLLOW_REMOTE_CREATE\n+enum follow_remote_head_settings {\n+\tFOLLOW_REMOTE_UNCONFIGURED = 0,\n+\tFOLLOW_REMOTE_NEVER,\n+\tFOLLOW_REMOTE_CREATE,\n+\tFOLLOW_REMOTE_WARN,\n+\tFOLLOW_REMOTE_ALWAYS,\n+};\n \n struct remote {\n \tstruct hashmap_entry ent;\n-- \n2.54.0\n\n"},{"id":"545336","messageId":"20260612055947.1499497-7-m@lfurio.us","threadId":"65759","inReplyTo":"20260612055947.1499497-1-m@lfurio.us","subject":"[PATCH 6/7] fetch: add configuration option fetch.followRemoteHEAD","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-12T05:55:42Z","receivedAt":"2026-06-12T06:00:14Z","isPatch":true,"body":"'fetch.followRemoteHEAD' is added as a generic option used by all\nremotes for which 'remote.<name>.followRemoteHEAD' is undefined.  If\nboth options are undefined, a builtin default of \"create\" is in effect,\nmatching the previous behavior.\n\nAs mentioned in the previous patch, 'fetch.followRemoteHEAD' supports\nall of the values that its 'remote' counterpart does _except_\nwarn-if-not-$branch, due to its tighter coupling to individual remote\nrepositories.\n\nDocumentation and advice messages for both of the followRemoteHEAD\noptions are reworded to better capture the relationship between the two.\n\nThe added tests assert feature parity between the two followRemoteHEAD\noptions, as well as the fact that 'remote.<name>.followRemoteHEAD'\nalways supersedes this new configurable default.\n\nSigned-off-by: Matt Hunter <m@lfurio.us>\n---\n Documentation/config/fetch.adoc  |  19 ++++++\n Documentation/config/remote.adoc |  21 +++----\n builtin/fetch.c                  |  32 ++++++++--\n t/t5510-fetch.sh                 | 105 +++++++++++++++++++++++++++++++\n 4 files changed, 160 insertions(+), 17 deletions(-)\n\ndiff --git a/Documentation/config/fetch.adoc b/Documentation/config/fetch.adoc\nindex 04ac90912d3a..f7de22a34a54 100644\n--- a/Documentation/config/fetch.adoc\n+++ b/Documentation/config/fetch.adoc\n@@ -126,3 +126,22 @@ the new bundle URI.\n The creation token values are chosen by the provider serving the specific\n bundle URI. If you modify the URI at `fetch.bundleURI`, then be sure to\n remove the value for the `fetch.bundleCreationToken` value before fetching.\n+\n+`fetch.followRemoteHEAD`::\n+\tWhen fetching using a default refspec, this option determines how to handle\n+\tdifferences between a fetched remote's `HEAD` and the local\n+\t`remotes/<name>/HEAD` symbolic-ref.  Its value is one of\n++\n+--\n+`create`;;\n+\tCreate `remotes/<name>/HEAD` if a ref exists on the remote, but not locally.\n+\tAn existing symbolic-ref will not be touched.  This is the default value.\n+`warn`;;\n+\tDisplay a warning if the remote advertises a different `HEAD` than what is\n+\tset locally.  Behaves like \"create\" if the local symbolic-ref doesn't exist.\n+`always`;;\n+\tSilently update `remotes/<name>/HEAD` whenever the remote advertises a new\n+\tvalue.\n+`never`;;\n+\tNever create or modify the `remotes/<name>/HEAD` symbolic-ref.\n+--\ndiff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc\nindex eb9c8a3c4884..761bf4ba7d14 100644\n--- a/Documentation/config/remote.adoc\n+++ b/Documentation/config/remote.adoc\n@@ -157,15 +157,12 @@ Blank values signal to ignore all previous values, allowing a reset of\n the list from broader config scenarios.\n \n remote.<name>.followRemoteHEAD::\n-\tHow linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`\n-\twhen fetching using the configured refspecs of a remote.\n-\tThe default value is \"create\", which will create `remotes/<name>/HEAD`\n-\tif it exists on the remote, but not locally; this will not touch an\n-\talready existing local reference. Setting it to \"warn\" will print\n-\ta message if the remote has a different value than the local one;\n-\tin case there is no local reference, it behaves like \"create\".\n-\tA variant on \"warn\" is \"warn-if-not-$branch\", which behaves like\n-\t\"warn\", but if `HEAD` on the remote is `$branch` it will be silent.\n-\tSetting it to \"always\" will silently update `remotes/<name>/HEAD` to\n-\tthe value on the remote.  Finally, setting it to \"never\" will never\n-\tchange or create the local reference.\n+\tWhen fetching this remote using its default refspec, this option determines\n+\thow to handle differences between the remote's `HEAD` and the local\n+\t`remotes/<name>/HEAD` symbolic-ref.  Overrides the setting for\n+\t`fetch.followRemoteHEAD`.  See `fetch.followRemoteHEAD` for a description of\n+\taccepted values.\n++\n+In addition to the values supported by `fetch.followRemoteHEAD`, this option may\n+also take on the value \"warn-if-not-`$branch`\", which behaves like \"warn\", but\n+ignores the warning if the remote's `HEAD` is `remotes/<name>/$branch`.\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 3cc7efdd83a0..a21bb82274d4 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -103,6 +103,7 @@ static struct string_list negotiation_include = STRING_LIST_INIT_NODUP;\n \n struct fetch_config {\n \tenum display_format display_format;\n+\tenum follow_remote_head_settings follow_remote_head;\n \tint all;\n \tint prune;\n \tint prune_tags;\n@@ -173,6 +174,22 @@ static int git_fetch_config(const char *k, const char *v,\n \t\t\t    \"fetch.output\", v);\n \t}\n \n+\tif (!strcmp(k, \"fetch.followremotehead\")) {\n+\t\tif (!v)\n+\t\t\treturn config_error_nonbool(k);\n+\t\telse if (!strcasecmp(v, \"never\"))\n+\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_NEVER;\n+\t\telse if (!strcasecmp(v, \"create\"))\n+\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_CREATE;\n+\t\telse if (!strcasecmp(v, \"warn\"))\n+\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_WARN;\n+\t\telse if (!strcasecmp(v, \"always\"))\n+\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_ALWAYS;\n+\t\telse\n+\t\t\tdie(_(\"invalid value for '%s': '%s'\"),\n+\t\t\t\t\"fetch.followRemoteHEAD\", v);\n+\t}\n+\n \treturn git_default_config(k, v, ctx, cb);\n }\n \n@@ -1697,11 +1714,13 @@ static const char *strip_refshead(const char *name){\n static void set_head_advice_msg(const char *remote, const char *head_name)\n {\n \tconst char message_advice_set_head[] =\n-\tN_(\"Run 'git remote set-head %s %s' to follow the change, or set\\n\"\n-\t   \"'remote.%s.followRemoteHEAD' configuration option to a different value\\n\"\n-\t   \"if you do not want to see this message. Specifically running\\n\"\n-\t   \"'git config set remote.%s.followRemoteHEAD warn-if-not-%s'\\n\"\n-\t   \"will disable the warning until the remote changes HEAD to something else.\");\n+\tN_(\"Run 'git remote set-head %s %s' to follow the change, or modify\\n\"\n+\t   \"either of the 'remote.%s.followRemoteHEAD' or 'fetch.followRemoteHEAD'\\n\"\n+\t   \"configuration options to handle the situation differently.\\n\\n\"\n+\n+\t   \"Using this specific option\\n\\n\"\n+\t   \"    git config set remote.%s.followRemoteHEAD warn-if-not-%s\\n\\n\"\n+\t   \"will suppress the warning until the remote changes HEAD to something else.\");\n \n \tadvise_if_enabled(ADVICE_FETCH_SET_HEAD_WARN, _(message_advice_set_head),\n \t\t\tremote, head_name, remote, remote, head_name);\n@@ -1919,6 +1938,8 @@ static int do_fetch(struct transport *transport,\n \n \tif (transport->remote->follow_remote_head)\n \t\tfollow_remote_head = transport->remote->follow_remote_head;\n+\telse if (config->follow_remote_head)\n+\t\tfollow_remote_head = config->follow_remote_head;\n \telse\n \t\tfollow_remote_head = BUILTIN_FOLLOW_REMOTE_HEAD_DFLT;\n \n@@ -2477,6 +2498,7 @@ int cmd_fetch(int argc,\n {\n \tstruct fetch_config config = {\n \t\t.display_format = DISPLAY_FORMAT_FULL,\n+\t\t.follow_remote_head = FOLLOW_REMOTE_UNCONFIGURED,\n \t\t.prune = -1,\n \t\t.prune_tags = -1,\n \t\t.show_forced_updates = 1,\ndiff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh\nindex 43190630e714..6f0ae1bdd798 100755\n--- a/t/t5510-fetch.sh\n+++ b/t/t5510-fetch.sh\n@@ -140,6 +140,16 @@ test_expect_success \"fetch test remote HEAD change\" '\n \t)\n '\n \n+test_expect_success \"fetch test default followRemoteHEAD never\" '\n+\tgit -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&\n+\ttest_config -C two fetch.followRemoteHEAD \"never\" &&\n+\tGIT_TRACE_PACKET=$PWD/trace.out git -C two fetch &&\n+\t# Confirm that we do not even ask for HEAD when we are\n+\t# not going to act on it.\n+\ttest_grep ! \"ref-prefix HEAD\" trace.out &&\n+\ttest_must_fail git -C two rev-parse --verify refs/remotes/origin/HEAD\n+'\n+\n test_expect_success \"fetch test followRemoteHEAD never\" '\n \tgit -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&\n \ttest_config -C two remote.origin.followRemoteHEAD \"never\" &&\n@@ -150,6 +160,21 @@ test_expect_success \"fetch test followRemoteHEAD never\" '\n \ttest_must_fail git -C two rev-parse --verify refs/remotes/origin/HEAD\n '\n \n+test_expect_success \"fetch test default followRemoteHEAD warn no change\" '\n+\tgit -C two rev-parse --verify refs/remotes/origin/other &&\n+\tgit -C two remote set-head origin other &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/HEAD &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/main &&\n+\ttest_config -C two fetch.followRemoteHEAD \"warn\" &&\n+\tgit -C two fetch >output &&\n+\techo \"${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ},\" \\\n+\t\t\"but we have ${SQ}other${SQ} locally.\" >expect &&\n+\ttest_cmp expect output &&\n+\thead=$(git -C two rev-parse refs/remotes/origin/HEAD) &&\n+\tbranch=$(git -C two rev-parse refs/remotes/origin/other) &&\n+\ttest \"z$head\" = \"z$branch\"\n+'\n+\n test_expect_success \"fetch test followRemoteHEAD warn no change\" '\n \tgit -C two rev-parse --verify refs/remotes/origin/other &&\n \tgit -C two remote set-head origin other &&\n@@ -165,6 +190,17 @@ test_expect_success \"fetch test followRemoteHEAD warn no change\" '\n \ttest \"z$head\" = \"z$branch\"\n '\n \n+test_expect_success \"fetch test default followRemoteHEAD warn create\" '\n+\tgit -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&\n+\ttest_config -C two fetch.followRemoteHEAD \"warn\" &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/main &&\n+\toutput=$(git -C two fetch) &&\n+\ttest \"z\" = \"z$output\" &&\n+\thead=$(git -C two rev-parse refs/remotes/origin/HEAD) &&\n+\tbranch=$(git -C two rev-parse refs/remotes/origin/main) &&\n+\ttest \"z$head\" = \"z$branch\"\n+'\n+\n test_expect_success \"fetch test followRemoteHEAD warn create\" '\n \tgit -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&\n \ttest_config -C two remote.origin.followRemoteHEAD \"warn\" &&\n@@ -176,6 +212,18 @@ test_expect_success \"fetch test followRemoteHEAD warn create\" '\n \ttest \"z$head\" = \"z$branch\"\n '\n \n+test_expect_success \"fetch test default followRemoteHEAD warn detached\" '\n+\tgit -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&\n+\tgit -C two update-ref refs/remotes/origin/HEAD HEAD &&\n+\tHEAD=$(git -C two log --pretty=\"%H\") &&\n+\ttest_config -C two fetch.followRemoteHEAD \"warn\" &&\n+\tgit -C two fetch >output &&\n+\techo \"${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ},\" \\\n+\t\t\"but we have a detached HEAD pointing to\" \\\n+\t\t\"${SQ}${HEAD}${SQ} locally.\" >expect &&\n+\ttest_cmp expect output\n+'\n+\n test_expect_success \"fetch test followRemoteHEAD warn detached\" '\n \tgit -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&\n \tgit -C two update-ref refs/remotes/origin/HEAD HEAD &&\n@@ -188,6 +236,19 @@ test_expect_success \"fetch test followRemoteHEAD warn detached\" '\n \ttest_cmp expect output\n '\n \n+test_expect_success \"fetch test default followRemoteHEAD warn quiet\" '\n+\tgit -C two rev-parse --verify refs/remotes/origin/other &&\n+\tgit -C two remote set-head origin other &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/HEAD &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/main &&\n+\ttest_config -C two fetch.followRemoteHEAD \"warn\" &&\n+\toutput=$(git -C two fetch --quiet) &&\n+\ttest \"z\" = \"z$output\" &&\n+\thead=$(git -C two rev-parse refs/remotes/origin/HEAD) &&\n+\tbranch=$(git -C two rev-parse refs/remotes/origin/other) &&\n+\ttest \"z$head\" = \"z$branch\"\n+'\n+\n test_expect_success \"fetch test followRemoteHEAD warn quiet\" '\n \tgit -C two rev-parse --verify refs/remotes/origin/other &&\n \tgit -C two remote set-head origin other &&\n@@ -229,6 +290,18 @@ test_expect_success \"fetch test followRemoteHEAD warn-if-not-branch branch is di\n \ttest \"z$head\" = \"z$branch\"\n '\n \n+test_expect_success \"fetch test default followRemoteHEAD always\" '\n+\tgit -C two rev-parse --verify refs/remotes/origin/other &&\n+\tgit -C two remote set-head origin other &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/HEAD &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/main &&\n+\ttest_config -C two fetch.followRemoteHEAD \"always\" &&\n+\tgit -C two fetch &&\n+\thead=$(git -C two rev-parse refs/remotes/origin/HEAD) &&\n+\tbranch=$(git -C two rev-parse refs/remotes/origin/main) &&\n+\ttest \"z$head\" = \"z$branch\"\n+'\n+\n test_expect_success \"fetch test followRemoteHEAD always\" '\n \tgit -C two rev-parse --verify refs/remotes/origin/other &&\n \tgit -C two remote set-head origin other &&\n@@ -241,6 +314,28 @@ test_expect_success \"fetch test followRemoteHEAD always\" '\n \ttest \"z$head\" = \"z$branch\"\n '\n \n+test_expect_success 'per-remote followRemoteHEAD takes priority over fetch default' '\n+\tgit -C two rev-parse --verify refs/remotes/origin/other &&\n+\tgit -C two remote set-head origin other &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/HEAD &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/main &&\n+\ttest_config -C two fetch.followRemoteHEAD \"never\" &&\n+\ttest_config -C two remote.origin.followRemoteHEAD \"always\" &&\n+\tgit -C two fetch &&\n+\thead=$(git -C two rev-parse refs/remotes/origin/HEAD) &&\n+\tbranch=$(git -C two rev-parse refs/remotes/origin/main) &&\n+\ttest \"z$head\" = \"z$branch\"\n+'\n+\n+test_expect_success 'default followRemoteHEAD does not kick in with refspecs' '\n+\tgit -C two remote set-head origin other &&\n+\ttest_config -C two fetch.followRemoteHEAD always &&\n+\tgit -C two fetch origin refs/heads/main:refs/remotes/origin/main &&\n+\techo refs/remotes/origin/other >expect &&\n+\tgit -C two symbolic-ref refs/remotes/origin/HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'followRemoteHEAD does not kick in with refspecs' '\n \tgit -C two remote set-head origin other &&\n \ttest_config -C two remote.origin.followRemoteHEAD always &&\n@@ -250,6 +345,16 @@ test_expect_success 'followRemoteHEAD does not kick in with refspecs' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'default followRemoteHEAD create does not overwrite dangling symref' '\n+\ttest_when_finished \"git -C two remote remove custom-head\" &&\n+\tgit -C two remote add -m does-not-exist custom-head ../one &&\n+\ttest_config -C two fetch.followRemoteHEAD create &&\n+\tgit -C two fetch custom-head &&\n+\techo refs/remotes/custom-head/does-not-exist >expect &&\n+\tgit -C two symbolic-ref refs/remotes/custom-head/HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'followRemoteHEAD create does not overwrite dangling symref' '\n \ttest_when_finished \"git -C two remote remove custom-head\" &&\n \tgit -C two remote add -m does-not-exist custom-head ../one &&\n-- \n2.54.0\n\n"},{"id":"545337","messageId":"20260612055947.1499497-8-m@lfurio.us","threadId":"65759","inReplyTo":"20260612055947.1499497-1-m@lfurio.us","subject":"[PATCH 7/7] fetch: fixup a misaligned comment","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-12T05:55:43Z","receivedAt":"2026-06-12T06:00:15Z","isPatch":true,"body":"Signed-off-by: Matt Hunter <m@lfurio.us>\n---\n builtin/fetch.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex a21bb82274d4..911ac8a47221 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1792,7 +1792,7 @@ static int set_head(const struct ref *remote_refs, struct remote *remote,\n \t\tstrbuf_addf(&b_head, \"refs/remotes/%s/HEAD\", remote->name);\n \t\tstrbuf_addf(&b_remote_head, \"refs/remotes/%s/%s\", remote->name, head_name);\n \t}\n-\t\t/* make sure it's valid */\n+\t/* make sure it's valid */\n \tif (!baremirror && !refs_ref_exists(refs, b_remote_head.buf)) {\n \t\tresult = 1;\n \t\tgoto cleanup;\n-- \n2.54.0\n\n"},{"id":"545340","messageId":"DJ6UJPXWOHVB.343AQQB4XEKNM@lfurio.us","threadId":"65759","inReplyTo":"DJ6IBPYNOTTY.3QKEZQ28P713V@ferdinandy.com","subject":"Re: followRemoteHEAD management question","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-12T06:11:38Z","receivedAt":"2026-06-12T06:11:39Z","isPatch":false,"body":"On Thu Jun 11, 2026 at 4:36 PM EDT, Bence Ferdinandy wrote:\n> On Thu Jun 11, 2026 at 08:01, Jeff King <peff@peff.net> wrote:\n>>\n>> My initial thought is that it might affect clone as well as fetch. But I\n>> guess this feature does not kick in for clone, as it has its own logic\n>> for handling the remote-tracking HEAD. Though arguably it should be\n>> possible to configure it not to create one in the first place.\n>\n> If memory serves well clone has set the remote/HEAD well before this and\n> I think it indeed uses a different mechanism/logic.\n\nI'm a little interested to try to look into the clone case as well, but\nI think I'll save it for a later patch series and keep the scope of this\none as it is.\n\n> Bit late to the party, but happy to review/test patches if they come.\n\nGreatly appreciated!\n>\n> Best,\n> Bence\n\nThe first version of my patches went out.  You two are Cc'd on the cover\nletter, but that didn't propagate to the patches themselves, oops.\n"},{"id":"545380","messageId":"DJ74J0G5PUXF.3DVTRHXS0GEG1@lfurio.us","threadId":"65759","inReplyTo":"20260612055947.1499497-7-m@lfurio.us","subject":"Re: [PATCH 6/7] fetch: add configuration option fetch.followRemoteHEAD","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-12T14:00:54Z","receivedAt":"2026-06-12T14:00:55Z","isPatch":true,"body":"On Fri Jun 12, 2026 at 1:55 AM EDT, Matt Hunter wrote:\n> diff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc\n> index eb9c8a3c4884..761bf4ba7d14 100644\n> --- a/Documentation/config/remote.adoc\n> +++ b/Documentation/config/remote.adoc\n> @@ -157,15 +157,12 @@ Blank values signal to ignore all previous values, allowing a reset of\n>  the list from broader config scenarios.\n>  \n>  remote.<name>.followRemoteHEAD::\n> -\tHow linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`\n> -\twhen fetching using the configured refspecs of a remote.\n> -\tThe default value is \"create\", which will create `remotes/<name>/HEAD`\n> -\tif it exists on the remote, but not locally; this will not touch an\n> -\talready existing local reference. Setting it to \"warn\" will print\n> -\ta message if the remote has a different value than the local one;\n> -\tin case there is no local reference, it behaves like \"create\".\n> -\tA variant on \"warn\" is \"warn-if-not-$branch\", which behaves like\n> -\t\"warn\", but if `HEAD` on the remote is `$branch` it will be silent.\n> -\tSetting it to \"always\" will silently update `remotes/<name>/HEAD` to\n> -\tthe value on the remote.  Finally, setting it to \"never\" will never\n> -\tchange or create the local reference.\n> +\tWhen fetching this remote using its default refspec, this option determines\n> +\thow to handle differences between the remote's `HEAD` and the local\n> +\t`remotes/<name>/HEAD` symbolic-ref.  Overrides the setting for\n> +\t`fetch.followRemoteHEAD`.  See `fetch.followRemoteHEAD` for a description of\n> +\taccepted values.\n> ++\n> +In addition to the values supported by `fetch.followRemoteHEAD`, this option may\n> +also take on the value \"warn-if-not-`$branch`\", which behaves like \"warn\", but\n> +ignores the warning if the remote's `HEAD` is `remotes/<name>/$branch`.\n\nIn hindsight, I'm wondering if $branch ought to be stylized as <branch>\nto match the rest of the docs.  Thoughts?\n"},{"id":"545382","messageId":"xmqqik7nj11i.fsf@gitster.g","threadId":"65759","inReplyTo":"20260612055947.1499497-7-m@lfurio.us","subject":"Re: [PATCH 6/7] fetch: add configuration option fetch.followRemoteHEAD","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-12T14:17:29Z","receivedAt":"2026-06-12T14:17:32Z","isPatch":true,"body":"Matt Hunter <m@lfurio.us> writes:\n\nI haven't been following the discussion, so I will not comment on\nthe idea, i.e., if it makes sense to add such a new option and\nconfiguration, but if we were to add such a thing, I have some\ncomments on the mechanics.\n\nBy the way, do not call a \"configuration variable\" a \"configuration option\".\nLet's keep the vocabulary forcused without using random synonyms.\n\n> diff --git a/builtin/fetch.c b/builtin/fetch.c\n> index 3cc7efdd83a0..a21bb82274d4 100644\n> --- a/builtin/fetch.c\n> +++ b/builtin/fetch.c\n> @@ -103,6 +103,7 @@ static struct string_list negotiation_include = STRING_LIST_INIT_NODUP;\n>  \n>  struct fetch_config {\n>  \tenum display_format display_format;\n> +\tenum follow_remote_head_settings follow_remote_head;\n>  \tint all;\n>  \tint prune;\n>  \tint prune_tags;\n> @@ -173,6 +174,22 @@ static int git_fetch_config(const char *k, const char *v,\n>  \t\t\t    \"fetch.output\", v);\n>  \t}\n>  \n> +\tif (!strcmp(k, \"fetch.followremotehead\")) {\n> +\t\tif (!v)\n> +\t\t\treturn config_error_nonbool(k);\n> +\t\telse if (!strcasecmp(v, \"never\"))\n> +\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_NEVER;\n> +\t\telse if (!strcasecmp(v, \"create\"))\n> +\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_CREATE;\n> +\t\telse if (!strcasecmp(v, \"warn\"))\n> +\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_WARN;\n> +\t\telse if (!strcasecmp(v, \"always\"))\n> +\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_ALWAYS;\n> +\t\telse\n> +\t\t\tdie(_(\"invalid value for '%s': '%s'\"),\n> +\t\t\t\t\"fetch.followRemoteHEAD\", v);\n> +\t}\n\nI think these uses of strcasecmp() are unnecessary and actively\nharms end-user experience.  This is especially true because the\nvalue given to remote.<name>.followRemoteHEAD is case sensitive.\n\nInstead of saying \"if you want X to happen, set this variable to\n'create'\", you have to say \"'create', or any other case variations\nthereof like 'CrEAte'\" somehow, for very dubious gain to the end\nusers.  If you use strcmp(), and document only all lowercase forms,\nit would guarantee to avoid confusing a newbie who read the variable\nto be set to 'never' on one blog and 'Never' on another and wonder\nif 'NEVER' would work or not.\n\nAdmittedly values to some existing configuration variables may be\nparsed case insensitively but we should aim to fix the mistake in\nthe longer term, and we should certainly not add more of them.\n\nIs it sensible to die() here?  If you are fetching from somewhere\nwithout keeping a set of remote-tracking branches for it (i.e., a\nsingle shot \"git fetch https://github.com/gitster/git master\"), you\ndo not care what garbage value is in fetch.followRemoteHEAD.\nPerhaps the version of Git that is slightly newer than the version\nthat ships with this patch defined new valid values that this patch\ndoes not know about, and such a user who is doing a single-shot\nfetch may have that setting to help them working with their usual\nnon-single shot repositories, but they use a newer version of Git\nfor such regular work, and they are using slightly old version of\nGit to perform this single-shot fetch.  The point is that the\nconfigured value will *NOT* be used for such a user, and dying only\nbecause this piece of code does not understand the configuration that\nwill not be used is of dubious value.\n"},{"id":"545429","messageId":"DJ7L27FXS2PG.7PMBDY817U4V@lfurio.us","threadId":"65759","inReplyTo":"xmqqik7nj11i.fsf@gitster.g","subject":"Re: [PATCH 6/7] fetch: add configuration option fetch.followRemoteHEAD","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-13T02:58:16Z","receivedAt":"2026-06-13T02:58:17Z","isPatch":true,"body":"On Fri Jun 12, 2026 at 10:17 AM EDT, Junio C Hamano wrote:\n>\n> By the way, do not call a \"configuration variable\" a \"configuration option\".\n> Let's keep the vocabulary forcused without using random synonyms.\n\nNoted.  I can appreciate that the term \"option\" may be better reserved\nfor describing command-line options, to avoid confusion.\n\nIs it safe to assume \"setting\" may be an appropriate alternative to\n\"configuration variable\" in some contexts?\n\n>\n> I think these uses of strcasecmp() are unnecessary and actively\n> harms end-user experience.  This is especially true because the\n> value given to remote.<name>.followRemoteHEAD is case sensitive.\n>\n> [...]\n>\n> Admittedly values to some existing configuration variables may be\n> parsed case insensitively but we should aim to fix the mistake in\n> the longer term, and we should certainly not add more of them.\n\nThanks for clarifying the correct form here.  The use of strcasecmp()\nwas largely to match surrounding context as I assumed it would meet most\npeople's expectations.\n\nI think a detail like this can be especially confusing since it seems\nlike the parsing for config variable **names** generally is\ncase-insensitive.\n\n>\n> Is it sensible to die() here?  If you are fetching from somewhere\n> without keeping a set of remote-tracking branches for it (i.e., a\n> single shot \"git fetch https://github.com/gitster/git master\"), you\n> do not care what garbage value is in fetch.followRemoteHEAD.\n> Perhaps the version of Git that is slightly newer than the version\n> that ships with this patch defined new valid values that this patch\n> does not know about, and such a user who is doing a single-shot\n> fetch may have that setting to help them working with their usual\n> non-single shot repositories, but they use a newer version of Git\n> for such regular work, and they are using slightly old version of\n> Git to perform this single-shot fetch.  The point is that the\n> configured value will *NOT* be used for such a user, and dying only\n> because this piece of code does not understand the configuration that\n> will not be used is of dubious value.\n\nVery good point about forward compatibility.  Agreed that die() is the\nwrong call here.\n\nThe most sensible thing is probably to leave fetch.followRemoteHEAD\nUNCONFIGURED if the value is unrecognized, so we fall back to the\n\"create\" behavior unless the remote in question defines its own\nfollowRemoteHEAD policy.\n\nWill incorporate each of these in the next round, thanks!\n"},{"id":"545700","messageId":"20260616222606.1003521-1-m@lfurio.us","threadId":"65759","inReplyTo":"20260612055947.1499497-1-m@lfurio.us","subject":"[PATCH v2 0/7] Introduce fetch.followRemoteHEAD config variable","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-16T22:25:14Z","receivedAt":"2026-06-16T22:26:32Z","isPatch":true,"body":"git-fetch presently offers some useful ways to control how remote HEAD\nsymbolic-refs are (or aren't) updated when fetching from remote\nrepositories.  Namely this is done via the\n'remote.<name>.followRemoteHEAD' configuration variable.\n\nHowever, this setting can be somewhat painful to use if you prefer a\ndefault other than \"create\" and often work with multiple different\nremote repositories.\n\nThis series introduces the variable 'fetch.followRemoteHEAD', which\nprovides a configurable default in place of per-remote settings.\n\n'fetch.followRemoteHEAD' functions exactly the same as the original\nvariable, except that it doesn't allow warning suppression via\n'warn-if-not-$branch'.  Given that different remotes will vary their\nHEAD and set of branches independently, setting a false-positive\nglobally in this way doesn't make logical sense.\n\nWhile it is not mentioned by any of the patches in this series, note\nalso that the behavior introduced by 012bc566bad7 (remote set-head: set\nfollowRemoteHEAD to \"warn\" if \"always\") is unaffected by this series,\nand this feature continues to work for only the\n'remote.<name>.followRemoteHEAD' variable.\n\n--- \n\nChanges in v2:\n  - Don't die() if the value of fetch.followRemoteHEAD is unrecognized.\n  - Use case-sensitive matching for fetch.followRemoteHEAD values.\n  - Avoid the phrase \"configuration option\".\n  - Minor documentation wording changes.\n  - Link to v1: https://patch.msgid.link/20260612055947.1499497-1-m@lfurio.us\n\nMatt Hunter (7):\n  fetch: fixup set_head advice for warn-if-not-branch\n  doc: explain fetchRemoteHEADWarn advice\n  t5510: cleanup remote in followRemoteHEAD dangling ref test\n  fetch: rename function report_set_head\n  fetch: refactor do_fetch handling of followRemoteHEAD\n  fetch: add configuration variable fetch.followRemoteHEAD\n  fetch: fixup a misaligned comment\n\n Documentation/config/advice.adoc |   4 ++\n Documentation/config/fetch.adoc  |  19 ++++++\n Documentation/config/remote.adoc |  21 +++---\n builtin/fetch.c                  |  49 ++++++++++----\n remote.h                         |  14 ++--\n t/t5510-fetch.sh                 | 106 +++++++++++++++++++++++++++++++\n 6 files changed, 183 insertions(+), 30 deletions(-)\n\nRange-diff against v1:\n1:  779fb9bfc59f = 1:  2106228f7b98 fetch: fixup set_head advice for warn-if-not-branch\n2:  aacc2856bc77 = 2:  b1c58c06e0c7 doc: explain fetchRemoteHEADWarn advice\n3:  f5272eaafbcc = 3:  c1d11e8883e6 t5510: cleanup remote in followRemoteHEAD dangling ref test\n4:  43a17027c13e = 4:  6306c8212fc0 fetch: rename function report_set_head\n5:  c719435d9675 ! 5:  3c7257094686 fetch: refactor do_fetch handling of followRemoteHEAD\n    @@ Commit message\n     \n         Update enum follow_remote_head_settings to include the value\n         FOLLOW_REMOTE_UNCONFIGURED as the new zero-initialized value for\n    -    followRemoteHEAD.  This will allow us to distinguish between the option\n    -    being unset vs. explicitly set to 'create', which is ultimately the\n    -    system default.  The unnecessary indentation is removed.\n    +    followRemoteHEAD.  This will allow us to distinguish between the\n    +    variable being unset vs. explicitly set to 'create', which is ultimately\n    +    the system default.  The unnecessary indentation is removed.\n     \n         The do_fetch function is likewise updated to perform its own decision\n         making to determine the effective followRemoteHEAD mode, falling back to\n         the system default if necessary.  This will enable the next patch to\n    -    introduce a user-configurable fallback default option.\n    +    introduce a user-configurable default.\n     \n    -    Function set_head now accepts this value as an argument rather than only\n    +    Function set_head now accepts the mode as an argument rather than only\n         considering the value defined by the remote.\n     \n         The use of the 'warn-if-not-$branch' value is awkward in the context of\n    -    a global default option, since the branches will differ between\n    -    individual remotes.  For this reason, it's left out of this scheme and\n    -    handling of the no_warn_branch variable is untouched.  Since a\n    -    remote-specific setting for followRemoteHEAD takes priority, we can\n    -    assume that if remote->no_warn_branch is set, then the remote is also\n    -    asserting FOLLOW_REMOTE_WARN as the effective operating mode, and it\n    -    will be honored by do_fetch.\n    +    a global default, since the branches will differ between individual\n    +    remotes.  For this reason, it's left out of this scheme and handling of\n    +    the no_warn_branch variable is untouched.  Since a remote-specific\n    +    value for followRemoteHEAD takes priority, we can assume that if\n    +    remote->no_warn_branch is set, then the remote is also asserting\n    +    FOLLOW_REMOTE_WARN as the effective operating mode, and it will be\n    +    honored by do_fetch.\n     \n         Signed-off-by: Matt Hunter <m@lfurio.us>\n     \n6:  56f6fc8ded2d ! 6:  af9f99b1ceb2 fetch: add configuration option fetch.followRemoteHEAD\n    @@ Metadata\n     Author: Matt Hunter <m@lfurio.us>\n     \n      ## Commit message ##\n    -    fetch: add configuration option fetch.followRemoteHEAD\n    +    fetch: add configuration variable fetch.followRemoteHEAD\n     \n    -    'fetch.followRemoteHEAD' is added as a generic option used by all\n    +    'fetch.followRemoteHEAD' is added as a generic setting used by all\n         remotes for which 'remote.<name>.followRemoteHEAD' is undefined.  If\n    -    both options are undefined, a builtin default of \"create\" is in effect,\n    -    matching the previous behavior.\n    +    both variables are undefined, a builtin default of \"create\" is in\n    +    effect, matching the previous behavior.\n     \n         As mentioned in the previous patch, 'fetch.followRemoteHEAD' supports\n         all of the values that its 'remote' counterpart does _except_\n    @@ Commit message\n         repositories.\n     \n         Documentation and advice messages for both of the followRemoteHEAD\n    -    options are reworded to better capture the relationship between the two.\n    +    variables are reworded to better capture the relationship between the\n    +    two.\n     \n         The added tests assert feature parity between the two followRemoteHEAD\n    -    options, as well as the fact that 'remote.<name>.followRemoteHEAD'\n    +    variables, as well as the fact that 'remote.<name>.followRemoteHEAD'\n         always supersedes this new configurable default.\n     \n    +    Helped-by: Junio C Hamano <gitster@pobox.com>\n         Signed-off-by: Matt Hunter <m@lfurio.us>\n     \n      ## Documentation/config/fetch.adoc ##\n    @@ Documentation/config/fetch.adoc: the new bundle URI.\n      remove the value for the `fetch.bundleCreationToken` value before fetching.\n     +\n     +`fetch.followRemoteHEAD`::\n    -+\tWhen fetching using a default refspec, this option determines how to handle\n    ++\tWhen fetching using a default refspec, this setting determines how to handle\n     +\tdifferences between a fetched remote's `HEAD` and the local\n     +\t`remotes/<name>/HEAD` symbolic-ref.  Its value is one of\n     ++\n    @@ Documentation/config/remote.adoc: Blank values signal to ignore all previous val\n     -\tSetting it to \"always\" will silently update `remotes/<name>/HEAD` to\n     -\tthe value on the remote.  Finally, setting it to \"never\" will never\n     -\tchange or create the local reference.\n    -+\tWhen fetching this remote using its default refspec, this option determines\n    ++\tWhen fetching this remote using its default refspec, this setting determines\n     +\thow to handle differences between the remote's `HEAD` and the local\n    -+\t`remotes/<name>/HEAD` symbolic-ref.  Overrides the setting for\n    ++\t`remotes/<name>/HEAD` symbolic-ref.  Overrides the value of\n     +\t`fetch.followRemoteHEAD`.  See `fetch.followRemoteHEAD` for a description of\n     +\taccepted values.\n     ++\n    -+In addition to the values supported by `fetch.followRemoteHEAD`, this option may\n    -+also take on the value \"warn-if-not-`$branch`\", which behaves like \"warn\", but\n    -+ignores the warning if the remote's `HEAD` is `remotes/<name>/$branch`.\n    ++In addition to the values supported by `fetch.followRemoteHEAD`, this setting\n    ++may also take on the value \"warn-if-not-`$branch`\", which behaves like \"warn\",\n    ++but ignores the warning if the remote's `HEAD` is `remotes/<name>/$branch`.\n     \n      ## builtin/fetch.c ##\n     @@ builtin/fetch.c: static struct string_list negotiation_include = STRING_LIST_INIT_NODUP;\n    @@ builtin/fetch.c: static int git_fetch_config(const char *k, const char *v,\n     +\tif (!strcmp(k, \"fetch.followremotehead\")) {\n     +\t\tif (!v)\n     +\t\t\treturn config_error_nonbool(k);\n    -+\t\telse if (!strcasecmp(v, \"never\"))\n    ++\t\telse if (!strcmp(v, \"never\"))\n     +\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_NEVER;\n    -+\t\telse if (!strcasecmp(v, \"create\"))\n    ++\t\telse if (!strcmp(v, \"create\"))\n     +\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_CREATE;\n    -+\t\telse if (!strcasecmp(v, \"warn\"))\n    ++\t\telse if (!strcmp(v, \"warn\"))\n     +\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_WARN;\n    -+\t\telse if (!strcasecmp(v, \"always\"))\n    ++\t\telse if (!strcmp(v, \"always\"))\n     +\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_ALWAYS;\n    -+\t\telse\n    -+\t\t\tdie(_(\"invalid value for '%s': '%s'\"),\n    -+\t\t\t\t\"fetch.followRemoteHEAD\", v);\n     +\t}\n     +\n      \treturn git_default_config(k, v, ctx, cb);\n    @@ builtin/fetch.c: static const char *strip_refshead(const char *name){\n     -\t   \"will disable the warning until the remote changes HEAD to something else.\");\n     +\tN_(\"Run 'git remote set-head %s %s' to follow the change, or modify\\n\"\n     +\t   \"either of the 'remote.%s.followRemoteHEAD' or 'fetch.followRemoteHEAD'\\n\"\n    -+\t   \"configuration options to handle the situation differently.\\n\\n\"\n    ++\t   \"configuration variables to handle the situation differently.\\n\\n\"\n     +\n    -+\t   \"Using this specific option\\n\\n\"\n    ++\t   \"Using this specific setting\\n\\n\"\n     +\t   \"    git config set remote.%s.followRemoteHEAD warn-if-not-%s\\n\\n\"\n     +\t   \"will suppress the warning until the remote changes HEAD to something else.\");\n      \n7:  5e0bdd0f00b4 = 7:  5c80107f6488 fetch: fixup a misaligned comment\n\nbase-commit: 0fae78c9d55efe705877ea537fe42c59164ccd94\n-- \n2.54.0\n\n"},{"id":"545702","messageId":"20260616222606.1003521-2-m@lfurio.us","threadId":"65759","inReplyTo":"20260616222606.1003521-1-m@lfurio.us","subject":"[PATCH v2 1/7] fetch: fixup set_head advice for warn-if-not-branch","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-16T22:25:15Z","receivedAt":"2026-06-16T22:26:34Z","isPatch":true,"body":"Specifying the word 'branch' in the command is not correct - a mismatch\nwith both the implementation in remote.c and the documentation.\n\nSigned-off-by: Matt Hunter <m@lfurio.us>\n---\n builtin/fetch.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex c1d7c672f4e0..82969e230f5a 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1700,7 +1700,7 @@ static void set_head_advice_msg(const char *remote, const char *head_name)\n \tN_(\"Run 'git remote set-head %s %s' to follow the change, or set\\n\"\n \t   \"'remote.%s.followRemoteHEAD' configuration option to a different value\\n\"\n \t   \"if you do not want to see this message. Specifically running\\n\"\n-\t   \"'git config set remote.%s.followRemoteHEAD warn-if-not-branch-%s'\\n\"\n+\t   \"'git config set remote.%s.followRemoteHEAD warn-if-not-%s'\\n\"\n \t   \"will disable the warning until the remote changes HEAD to something else.\");\n \n \tadvise_if_enabled(ADVICE_FETCH_SET_HEAD_WARN, _(message_advice_set_head),\n-- \n2.54.0\n\n"},{"id":"545701","messageId":"20260616222606.1003521-3-m@lfurio.us","threadId":"65759","inReplyTo":"20260616222606.1003521-1-m@lfurio.us","subject":"[PATCH v2 2/7] doc: explain fetchRemoteHEADWarn advice","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-16T22:25:16Z","receivedAt":"2026-06-16T22:26:36Z","isPatch":true,"body":"When the user sets 'remote.<name>.followRemoteHEAD' to\n'warn[-if-not-$branch]', git-fetch will report when a fetched HEAD\ndisagrees with the locally-configured remote's HEAD.  This additional\nadvice instructs the user how to deal with these warnings, but was\npreviously undocumented in git-config.\n\nSigned-off-by: Matt Hunter <m@lfurio.us>\n---\n Documentation/config/advice.adoc | 4 ++++\n 1 file changed, 4 insertions(+)\n\ndiff --git a/Documentation/config/advice.adoc b/Documentation/config/advice.adoc\nindex 257db5891817..c3c190ba6a4f 100644\n--- a/Documentation/config/advice.adoc\n+++ b/Documentation/config/advice.adoc\n@@ -48,6 +48,10 @@ all advice messages.\n \t\tto create a local branch after the fact.\n \tdiverging::\n \t\tShown when a fast-forward is not possible.\n+\tfetchRemoteHEADWarn::\n+\t\tShown when linkgit:git-fetch[1] reveals that a remote `HEAD`\n+\t\tdiffers from what is set locally and the user has opted into\n+\t\treceiving a warning in this situation.\n \tfetchShowForcedUpdates::\n \t\tShown when linkgit:git-fetch[1] takes a long time\n \t\tto calculate forced updates after ref updates, or to warn\n-- \n2.54.0\n\n"},{"id":"545703","messageId":"20260616222606.1003521-4-m@lfurio.us","threadId":"65759","inReplyTo":"20260616222606.1003521-1-m@lfurio.us","subject":"[PATCH v2 3/7] t5510: cleanup remote in followRemoteHEAD dangling ref test","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-16T22:25:17Z","receivedAt":"2026-06-16T22:26:38Z","isPatch":true,"body":"A later patch will introduce a new test which closely mirrors this one.\nUpdate this test to remove the 'custom-head' remote it creates.\nOtherwise, the two tests will conflict with each other, as the second\none to execute will fail to create this remote (which already exists,\nthanks to the first test).\n\nSigned-off-by: Matt Hunter <m@lfurio.us>\n---\n t/t5510-fetch.sh | 1 +\n 1 file changed, 1 insertion(+)\n\ndiff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh\nindex eca9a973b5cb..43190630e714 100755\n--- a/t/t5510-fetch.sh\n+++ b/t/t5510-fetch.sh\n@@ -251,6 +251,7 @@ test_expect_success 'followRemoteHEAD does not kick in with refspecs' '\n '\n \n test_expect_success 'followRemoteHEAD create does not overwrite dangling symref' '\n+\ttest_when_finished \"git -C two remote remove custom-head\" &&\n \tgit -C two remote add -m does-not-exist custom-head ../one &&\n \ttest_config -C two remote.custom-head.followRemoteHEAD create &&\n \tgit -C two fetch custom-head &&\n-- \n2.54.0\n\n"},{"id":"545704","messageId":"20260616222606.1003521-5-m@lfurio.us","threadId":"65759","inReplyTo":"20260616222606.1003521-1-m@lfurio.us","subject":"[PATCH v2 4/7] fetch: rename function report_set_head","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-16T22:25:18Z","receivedAt":"2026-06-16T22:26:40Z","isPatch":true,"body":"Update to the slightly more obvious name 'warn_set_head', which matches\nthe verbiage of the followRemoteHEAD options.\n\nSigned-off-by: Matt Hunter <m@lfurio.us>\n---\n builtin/fetch.c | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 82969e230f5a..9a45e1e7a44d 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1707,7 +1707,7 @@ static void set_head_advice_msg(const char *remote, const char *head_name)\n \t\t\tremote, head_name, remote, remote, head_name);\n }\n \n-static void report_set_head(const char *remote, const char *head_name,\n+static void warn_set_head(const char *remote, const char *head_name,\n \t\t\tstruct strbuf *buf_prev, int updateres) {\n \tstruct strbuf buf_prefix = STRBUF_INIT;\n \tconst char *prev_head = NULL;\n@@ -1787,7 +1787,7 @@ static int set_head(const struct ref *remote_refs, struct remote *remote)\n \tif (verbosity >= 0 &&\n \t\tfollow_remote_head == FOLLOW_REMOTE_WARN &&\n \t\t(!no_warn_branch || strcmp(no_warn_branch, head_name)))\n-\t\treport_set_head(remote->name, head_name, &b_local_head, was_detached);\n+\t\twarn_set_head(remote->name, head_name, &b_local_head, was_detached);\n \n cleanup:\n \tfree(head_name);\n-- \n2.54.0\n\n"},{"id":"545705","messageId":"20260616222606.1003521-6-m@lfurio.us","threadId":"65759","inReplyTo":"20260616222606.1003521-1-m@lfurio.us","subject":"[PATCH v2 5/7] fetch: refactor do_fetch handling of followRemoteHEAD","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-16T22:25:19Z","receivedAt":"2026-06-16T22:26:43Z","isPatch":true,"body":"Update enum follow_remote_head_settings to include the value\nFOLLOW_REMOTE_UNCONFIGURED as the new zero-initialized value for\nfollowRemoteHEAD.  This will allow us to distinguish between the\nvariable being unset vs. explicitly set to 'create', which is ultimately\nthe system default.  The unnecessary indentation is removed.\n\nThe do_fetch function is likewise updated to perform its own decision\nmaking to determine the effective followRemoteHEAD mode, falling back to\nthe system default if necessary.  This will enable the next patch to\nintroduce a user-configurable default.\n\nFunction set_head now accepts the mode as an argument rather than only\nconsidering the value defined by the remote.\n\nThe use of the 'warn-if-not-$branch' value is awkward in the context of\na global default, since the branches will differ between individual\nremotes.  For this reason, it's left out of this scheme and handling of\nthe no_warn_branch variable is untouched.  Since a remote-specific\nvalue for followRemoteHEAD takes priority, we can assume that if\nremote->no_warn_branch is set, then the remote is also asserting\nFOLLOW_REMOTE_WARN as the effective operating mode, and it will be\nhonored by do_fetch.\n\nSigned-off-by: Matt Hunter <m@lfurio.us>\n---\n builtin/fetch.c | 14 ++++++++++----\n remote.h        | 14 ++++++++------\n 2 files changed, 18 insertions(+), 10 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 9a45e1e7a44d..3cc7efdd83a0 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1729,12 +1729,12 @@ static void warn_set_head(const char *remote, const char *head_name,\n \tstrbuf_release(&buf_prefix);\n }\n \n-static int set_head(const struct ref *remote_refs, struct remote *remote)\n+static int set_head(const struct ref *remote_refs, struct remote *remote,\n+\t\t\tint follow_remote_head)\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-\tint follow_remote_head = remote->follow_remote_head;\n \tconst char *no_warn_branch = remote->no_warn_branch;\n \tchar *head_name = NULL;\n \tstruct ref *ref, *matches;\n@@ -1901,6 +1901,7 @@ static int do_fetch(struct transport *transport,\n \tstruct ref_update_display_info_array display_array = { 0 };\n \tstruct strmap rejected_refs = STRMAP_INIT;\n \tint summary_width = 0;\n+\tint follow_remote_head;\n \n \tif (tags == TAGS_DEFAULT) {\n \t\tif (transport->remote->fetch_tags == 2)\n@@ -1916,6 +1917,11 @@ static int do_fetch(struct transport *transport,\n \t\t\tgoto cleanup;\n \t}\n \n+\tif (transport->remote->follow_remote_head)\n+\t\tfollow_remote_head = transport->remote->follow_remote_head;\n+\telse\n+\t\tfollow_remote_head = BUILTIN_FOLLOW_REMOTE_HEAD_DFLT;\n+\n \tif (rs->nr) {\n \t\trefspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);\n \t} else {\n@@ -1924,7 +1930,7 @@ static int do_fetch(struct transport *transport,\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 (transport->remote->follow_remote_head != FOLLOW_REMOTE_NEVER)\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@@ -2131,7 +2137,7 @@ 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);\n+\t\tset_head(remote_refs, transport->remote, follow_remote_head);\n \t}\n \n cleanup:\ndiff --git a/remote.h b/remote.h\nindex 54b17e4b028b..72a54d84ad51 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -62,12 +62,14 @@ struct remote_state {\n void remote_state_clear(struct remote_state *remote_state);\n struct remote_state *remote_state_new(void);\n \n-\tenum follow_remote_head_settings {\n-\t\tFOLLOW_REMOTE_NEVER = -1,\n-\t\tFOLLOW_REMOTE_CREATE = 0,\n-\t\tFOLLOW_REMOTE_WARN = 1,\n-\t\tFOLLOW_REMOTE_ALWAYS = 2,\n-\t};\n+#define BUILTIN_FOLLOW_REMOTE_HEAD_DFLT FOLLOW_REMOTE_CREATE\n+enum follow_remote_head_settings {\n+\tFOLLOW_REMOTE_UNCONFIGURED = 0,\n+\tFOLLOW_REMOTE_NEVER,\n+\tFOLLOW_REMOTE_CREATE,\n+\tFOLLOW_REMOTE_WARN,\n+\tFOLLOW_REMOTE_ALWAYS,\n+};\n \n struct remote {\n \tstruct hashmap_entry ent;\n-- \n2.54.0\n\n"},{"id":"545706","messageId":"20260616222606.1003521-7-m@lfurio.us","threadId":"65759","inReplyTo":"20260616222606.1003521-1-m@lfurio.us","subject":"[PATCH v2 6/7] fetch: add configuration variable fetch.followRemoteHEAD","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-16T22:25:20Z","receivedAt":"2026-06-16T22:26:47Z","isPatch":true,"body":"'fetch.followRemoteHEAD' is added as a generic setting used by all\nremotes for which 'remote.<name>.followRemoteHEAD' is undefined.  If\nboth variables are undefined, a builtin default of \"create\" is in\neffect, matching the previous behavior.\n\nAs mentioned in the previous patch, 'fetch.followRemoteHEAD' supports\nall of the values that its 'remote' counterpart does _except_\nwarn-if-not-$branch, due to its tighter coupling to individual remote\nrepositories.\n\nDocumentation and advice messages for both of the followRemoteHEAD\nvariables are reworded to better capture the relationship between the\ntwo.\n\nThe added tests assert feature parity between the two followRemoteHEAD\nvariables, as well as the fact that 'remote.<name>.followRemoteHEAD'\nalways supersedes this new configurable default.\n\nHelped-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Matt Hunter <m@lfurio.us>\n---\n Documentation/config/fetch.adoc  |  19 ++++++\n Documentation/config/remote.adoc |  21 +++----\n builtin/fetch.c                  |  29 +++++++--\n t/t5510-fetch.sh                 | 105 +++++++++++++++++++++++++++++++\n 4 files changed, 157 insertions(+), 17 deletions(-)\n\ndiff --git a/Documentation/config/fetch.adoc b/Documentation/config/fetch.adoc\nindex 04ac90912d3a..00435e9a16d9 100644\n--- a/Documentation/config/fetch.adoc\n+++ b/Documentation/config/fetch.adoc\n@@ -126,3 +126,22 @@ the new bundle URI.\n The creation token values are chosen by the provider serving the specific\n bundle URI. If you modify the URI at `fetch.bundleURI`, then be sure to\n remove the value for the `fetch.bundleCreationToken` value before fetching.\n+\n+`fetch.followRemoteHEAD`::\n+\tWhen fetching using a default refspec, this setting determines how to handle\n+\tdifferences between a fetched remote's `HEAD` and the local\n+\t`remotes/<name>/HEAD` symbolic-ref.  Its value is one of\n++\n+--\n+`create`;;\n+\tCreate `remotes/<name>/HEAD` if a ref exists on the remote, but not locally.\n+\tAn existing symbolic-ref will not be touched.  This is the default value.\n+`warn`;;\n+\tDisplay a warning if the remote advertises a different `HEAD` than what is\n+\tset locally.  Behaves like \"create\" if the local symbolic-ref doesn't exist.\n+`always`;;\n+\tSilently update `remotes/<name>/HEAD` whenever the remote advertises a new\n+\tvalue.\n+`never`;;\n+\tNever create or modify the `remotes/<name>/HEAD` symbolic-ref.\n+--\ndiff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc\nindex eb9c8a3c4884..04724bc51628 100644\n--- a/Documentation/config/remote.adoc\n+++ b/Documentation/config/remote.adoc\n@@ -157,15 +157,12 @@ Blank values signal to ignore all previous values, allowing a reset of\n the list from broader config scenarios.\n \n remote.<name>.followRemoteHEAD::\n-\tHow linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`\n-\twhen fetching using the configured refspecs of a remote.\n-\tThe default value is \"create\", which will create `remotes/<name>/HEAD`\n-\tif it exists on the remote, but not locally; this will not touch an\n-\talready existing local reference. Setting it to \"warn\" will print\n-\ta message if the remote has a different value than the local one;\n-\tin case there is no local reference, it behaves like \"create\".\n-\tA variant on \"warn\" is \"warn-if-not-$branch\", which behaves like\n-\t\"warn\", but if `HEAD` on the remote is `$branch` it will be silent.\n-\tSetting it to \"always\" will silently update `remotes/<name>/HEAD` to\n-\tthe value on the remote.  Finally, setting it to \"never\" will never\n-\tchange or create the local reference.\n+\tWhen fetching this remote using its default refspec, this setting determines\n+\thow to handle differences between the remote's `HEAD` and the local\n+\t`remotes/<name>/HEAD` symbolic-ref.  Overrides the value of\n+\t`fetch.followRemoteHEAD`.  See `fetch.followRemoteHEAD` for a description of\n+\taccepted values.\n++\n+In addition to the values supported by `fetch.followRemoteHEAD`, this setting\n+may also take on the value \"warn-if-not-`$branch`\", which behaves like \"warn\",\n+but ignores the warning if the remote's `HEAD` is `remotes/<name>/$branch`.\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 3cc7efdd83a0..1375fc4e0547 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -103,6 +103,7 @@ static struct string_list negotiation_include = STRING_LIST_INIT_NODUP;\n \n struct fetch_config {\n \tenum display_format display_format;\n+\tenum follow_remote_head_settings follow_remote_head;\n \tint all;\n \tint prune;\n \tint prune_tags;\n@@ -173,6 +174,19 @@ static int git_fetch_config(const char *k, const char *v,\n \t\t\t    \"fetch.output\", v);\n \t}\n \n+\tif (!strcmp(k, \"fetch.followremotehead\")) {\n+\t\tif (!v)\n+\t\t\treturn config_error_nonbool(k);\n+\t\telse if (!strcmp(v, \"never\"))\n+\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_NEVER;\n+\t\telse if (!strcmp(v, \"create\"))\n+\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_CREATE;\n+\t\telse if (!strcmp(v, \"warn\"))\n+\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_WARN;\n+\t\telse if (!strcmp(v, \"always\"))\n+\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_ALWAYS;\n+\t}\n+\n \treturn git_default_config(k, v, ctx, cb);\n }\n \n@@ -1697,11 +1711,13 @@ static const char *strip_refshead(const char *name){\n static void set_head_advice_msg(const char *remote, const char *head_name)\n {\n \tconst char message_advice_set_head[] =\n-\tN_(\"Run 'git remote set-head %s %s' to follow the change, or set\\n\"\n-\t   \"'remote.%s.followRemoteHEAD' configuration option to a different value\\n\"\n-\t   \"if you do not want to see this message. Specifically running\\n\"\n-\t   \"'git config set remote.%s.followRemoteHEAD warn-if-not-%s'\\n\"\n-\t   \"will disable the warning until the remote changes HEAD to something else.\");\n+\tN_(\"Run 'git remote set-head %s %s' to follow the change, or modify\\n\"\n+\t   \"either of the 'remote.%s.followRemoteHEAD' or 'fetch.followRemoteHEAD'\\n\"\n+\t   \"configuration variables to handle the situation differently.\\n\\n\"\n+\n+\t   \"Using this specific setting\\n\\n\"\n+\t   \"    git config set remote.%s.followRemoteHEAD warn-if-not-%s\\n\\n\"\n+\t   \"will suppress the warning until the remote changes HEAD to something else.\");\n \n \tadvise_if_enabled(ADVICE_FETCH_SET_HEAD_WARN, _(message_advice_set_head),\n \t\t\tremote, head_name, remote, remote, head_name);\n@@ -1919,6 +1935,8 @@ static int do_fetch(struct transport *transport,\n \n \tif (transport->remote->follow_remote_head)\n \t\tfollow_remote_head = transport->remote->follow_remote_head;\n+\telse if (config->follow_remote_head)\n+\t\tfollow_remote_head = config->follow_remote_head;\n \telse\n \t\tfollow_remote_head = BUILTIN_FOLLOW_REMOTE_HEAD_DFLT;\n \n@@ -2477,6 +2495,7 @@ int cmd_fetch(int argc,\n {\n \tstruct fetch_config config = {\n \t\t.display_format = DISPLAY_FORMAT_FULL,\n+\t\t.follow_remote_head = FOLLOW_REMOTE_UNCONFIGURED,\n \t\t.prune = -1,\n \t\t.prune_tags = -1,\n \t\t.show_forced_updates = 1,\ndiff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh\nindex 43190630e714..6f0ae1bdd798 100755\n--- a/t/t5510-fetch.sh\n+++ b/t/t5510-fetch.sh\n@@ -140,6 +140,16 @@ test_expect_success \"fetch test remote HEAD change\" '\n \t)\n '\n \n+test_expect_success \"fetch test default followRemoteHEAD never\" '\n+\tgit -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&\n+\ttest_config -C two fetch.followRemoteHEAD \"never\" &&\n+\tGIT_TRACE_PACKET=$PWD/trace.out git -C two fetch &&\n+\t# Confirm that we do not even ask for HEAD when we are\n+\t# not going to act on it.\n+\ttest_grep ! \"ref-prefix HEAD\" trace.out &&\n+\ttest_must_fail git -C two rev-parse --verify refs/remotes/origin/HEAD\n+'\n+\n test_expect_success \"fetch test followRemoteHEAD never\" '\n \tgit -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&\n \ttest_config -C two remote.origin.followRemoteHEAD \"never\" &&\n@@ -150,6 +160,21 @@ test_expect_success \"fetch test followRemoteHEAD never\" '\n \ttest_must_fail git -C two rev-parse --verify refs/remotes/origin/HEAD\n '\n \n+test_expect_success \"fetch test default followRemoteHEAD warn no change\" '\n+\tgit -C two rev-parse --verify refs/remotes/origin/other &&\n+\tgit -C two remote set-head origin other &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/HEAD &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/main &&\n+\ttest_config -C two fetch.followRemoteHEAD \"warn\" &&\n+\tgit -C two fetch >output &&\n+\techo \"${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ},\" \\\n+\t\t\"but we have ${SQ}other${SQ} locally.\" >expect &&\n+\ttest_cmp expect output &&\n+\thead=$(git -C two rev-parse refs/remotes/origin/HEAD) &&\n+\tbranch=$(git -C two rev-parse refs/remotes/origin/other) &&\n+\ttest \"z$head\" = \"z$branch\"\n+'\n+\n test_expect_success \"fetch test followRemoteHEAD warn no change\" '\n \tgit -C two rev-parse --verify refs/remotes/origin/other &&\n \tgit -C two remote set-head origin other &&\n@@ -165,6 +190,17 @@ test_expect_success \"fetch test followRemoteHEAD warn no change\" '\n \ttest \"z$head\" = \"z$branch\"\n '\n \n+test_expect_success \"fetch test default followRemoteHEAD warn create\" '\n+\tgit -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&\n+\ttest_config -C two fetch.followRemoteHEAD \"warn\" &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/main &&\n+\toutput=$(git -C two fetch) &&\n+\ttest \"z\" = \"z$output\" &&\n+\thead=$(git -C two rev-parse refs/remotes/origin/HEAD) &&\n+\tbranch=$(git -C two rev-parse refs/remotes/origin/main) &&\n+\ttest \"z$head\" = \"z$branch\"\n+'\n+\n test_expect_success \"fetch test followRemoteHEAD warn create\" '\n \tgit -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&\n \ttest_config -C two remote.origin.followRemoteHEAD \"warn\" &&\n@@ -176,6 +212,18 @@ test_expect_success \"fetch test followRemoteHEAD warn create\" '\n \ttest \"z$head\" = \"z$branch\"\n '\n \n+test_expect_success \"fetch test default followRemoteHEAD warn detached\" '\n+\tgit -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&\n+\tgit -C two update-ref refs/remotes/origin/HEAD HEAD &&\n+\tHEAD=$(git -C two log --pretty=\"%H\") &&\n+\ttest_config -C two fetch.followRemoteHEAD \"warn\" &&\n+\tgit -C two fetch >output &&\n+\techo \"${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ},\" \\\n+\t\t\"but we have a detached HEAD pointing to\" \\\n+\t\t\"${SQ}${HEAD}${SQ} locally.\" >expect &&\n+\ttest_cmp expect output\n+'\n+\n test_expect_success \"fetch test followRemoteHEAD warn detached\" '\n \tgit -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&\n \tgit -C two update-ref refs/remotes/origin/HEAD HEAD &&\n@@ -188,6 +236,19 @@ test_expect_success \"fetch test followRemoteHEAD warn detached\" '\n \ttest_cmp expect output\n '\n \n+test_expect_success \"fetch test default followRemoteHEAD warn quiet\" '\n+\tgit -C two rev-parse --verify refs/remotes/origin/other &&\n+\tgit -C two remote set-head origin other &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/HEAD &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/main &&\n+\ttest_config -C two fetch.followRemoteHEAD \"warn\" &&\n+\toutput=$(git -C two fetch --quiet) &&\n+\ttest \"z\" = \"z$output\" &&\n+\thead=$(git -C two rev-parse refs/remotes/origin/HEAD) &&\n+\tbranch=$(git -C two rev-parse refs/remotes/origin/other) &&\n+\ttest \"z$head\" = \"z$branch\"\n+'\n+\n test_expect_success \"fetch test followRemoteHEAD warn quiet\" '\n \tgit -C two rev-parse --verify refs/remotes/origin/other &&\n \tgit -C two remote set-head origin other &&\n@@ -229,6 +290,18 @@ test_expect_success \"fetch test followRemoteHEAD warn-if-not-branch branch is di\n \ttest \"z$head\" = \"z$branch\"\n '\n \n+test_expect_success \"fetch test default followRemoteHEAD always\" '\n+\tgit -C two rev-parse --verify refs/remotes/origin/other &&\n+\tgit -C two remote set-head origin other &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/HEAD &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/main &&\n+\ttest_config -C two fetch.followRemoteHEAD \"always\" &&\n+\tgit -C two fetch &&\n+\thead=$(git -C two rev-parse refs/remotes/origin/HEAD) &&\n+\tbranch=$(git -C two rev-parse refs/remotes/origin/main) &&\n+\ttest \"z$head\" = \"z$branch\"\n+'\n+\n test_expect_success \"fetch test followRemoteHEAD always\" '\n \tgit -C two rev-parse --verify refs/remotes/origin/other &&\n \tgit -C two remote set-head origin other &&\n@@ -241,6 +314,28 @@ test_expect_success \"fetch test followRemoteHEAD always\" '\n \ttest \"z$head\" = \"z$branch\"\n '\n \n+test_expect_success 'per-remote followRemoteHEAD takes priority over fetch default' '\n+\tgit -C two rev-parse --verify refs/remotes/origin/other &&\n+\tgit -C two remote set-head origin other &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/HEAD &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/main &&\n+\ttest_config -C two fetch.followRemoteHEAD \"never\" &&\n+\ttest_config -C two remote.origin.followRemoteHEAD \"always\" &&\n+\tgit -C two fetch &&\n+\thead=$(git -C two rev-parse refs/remotes/origin/HEAD) &&\n+\tbranch=$(git -C two rev-parse refs/remotes/origin/main) &&\n+\ttest \"z$head\" = \"z$branch\"\n+'\n+\n+test_expect_success 'default followRemoteHEAD does not kick in with refspecs' '\n+\tgit -C two remote set-head origin other &&\n+\ttest_config -C two fetch.followRemoteHEAD always &&\n+\tgit -C two fetch origin refs/heads/main:refs/remotes/origin/main &&\n+\techo refs/remotes/origin/other >expect &&\n+\tgit -C two symbolic-ref refs/remotes/origin/HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'followRemoteHEAD does not kick in with refspecs' '\n \tgit -C two remote set-head origin other &&\n \ttest_config -C two remote.origin.followRemoteHEAD always &&\n@@ -250,6 +345,16 @@ test_expect_success 'followRemoteHEAD does not kick in with refspecs' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'default followRemoteHEAD create does not overwrite dangling symref' '\n+\ttest_when_finished \"git -C two remote remove custom-head\" &&\n+\tgit -C two remote add -m does-not-exist custom-head ../one &&\n+\ttest_config -C two fetch.followRemoteHEAD create &&\n+\tgit -C two fetch custom-head &&\n+\techo refs/remotes/custom-head/does-not-exist >expect &&\n+\tgit -C two symbolic-ref refs/remotes/custom-head/HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'followRemoteHEAD create does not overwrite dangling symref' '\n \ttest_when_finished \"git -C two remote remove custom-head\" &&\n \tgit -C two remote add -m does-not-exist custom-head ../one &&\n-- \n2.54.0\n\n"},{"id":"545707","messageId":"20260616222606.1003521-8-m@lfurio.us","threadId":"65759","inReplyTo":"20260616222606.1003521-1-m@lfurio.us","subject":"[PATCH v2 7/7] fetch: fixup a misaligned comment","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-16T22:25:21Z","receivedAt":"2026-06-16T22:26:52Z","isPatch":true,"body":"Signed-off-by: Matt Hunter <m@lfurio.us>\n---\n builtin/fetch.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 1375fc4e0547..d942bf6aa029 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1789,7 +1789,7 @@ static int set_head(const struct ref *remote_refs, struct remote *remote,\n \t\tstrbuf_addf(&b_head, \"refs/remotes/%s/HEAD\", remote->name);\n \t\tstrbuf_addf(&b_remote_head, \"refs/remotes/%s/%s\", remote->name, head_name);\n \t}\n-\t\t/* make sure it's valid */\n+\t/* make sure it's valid */\n \tif (!baremirror && !refs_ref_exists(refs, b_remote_head.buf)) {\n \t\tresult = 1;\n \t\tgoto cleanup;\n-- \n2.54.0\n\n"},{"id":"545710","messageId":"xmqqh5n213bw.fsf@gitster.g","threadId":"65759","inReplyTo":"20260616222606.1003521-1-m@lfurio.us","subject":"Re: [PATCH v2 0/7] Introduce fetch.followRemoteHEAD config variable","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-16T23:18:59Z","receivedAt":"2026-06-16T23:19:02Z","isPatch":true,"body":"Matt Hunter <m@lfurio.us> writes:\n\n> Changes in v2:\n>   - Don't die() if the value of fetch.followRemoteHEAD is unrecognized.\n>   - Use case-sensitive matching for fetch.followRemoteHEAD values.\n>   - Avoid the phrase \"configuration option\".\n>   - Minor documentation wording changes.\n>   - Link to v1: https://patch.msgid.link/20260612055947.1499497-1-m@lfurio.us\n\n>     @@ builtin/fetch.c: static int git_fetch_config(const char *k, const char *v,\n>      +\tif (!strcmp(k, \"fetch.followremotehead\")) {\n>      +\t\tif (!v)\n>      +\t\t\treturn config_error_nonbool(k);\n>     -+\t\telse if (!strcasecmp(v, \"never\"))\n>     ++\t\telse if (!strcmp(v, \"never\"))\n>      +\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_NEVER;\n>     -+\t\telse if (!strcasecmp(v, \"create\"))\n>     ++\t\telse if (!strcmp(v, \"create\"))\n>      +\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_CREATE;\n>     -+\t\telse if (!strcasecmp(v, \"warn\"))\n>     ++\t\telse if (!strcmp(v, \"warn\"))\n>      +\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_WARN;\n>     -+\t\telse if (!strcasecmp(v, \"always\"))\n>     ++\t\telse if (!strcmp(v, \"always\"))\n>      +\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_ALWAYS;\n>     -+\t\telse\n>     -+\t\t\tdie(_(\"invalid value for '%s': '%s'\"),\n>     -+\t\t\t\t\"fetch.followRemoteHEAD\", v);\n>      +\t}\n\nNot dying on an unrecognised value is certainly better than dying,\nbut shouldn't we at least clear fetch_config->follow_remote_head\nto some \"unspecified\" or \"default\" value?  What does the existing\nparser routine for remote.*.followremotehead do?\n\nIdeally,\n\n (1) If the \"fetch\" operation ends up with not needing to consult\n     the value of fetch.followRemoteHEAD at all (e.g., it is a\n     one-shot fetch that updates no remote-tracking hierarchy, or it\n     has a more specific per-remote setting that this variable is\n     meant to serve as a mere fallback), any bogus or unknown value\n     will not get any warning.\n\n (2) If fetch.followRemoteHEAD ends up being _used_, and if it has\n     an unknown value, we should at least warn \"we do not understand\n     what you wrote, 'awlays', and we ignore it\", or die \"we do not\n     understand 'reset', perhaps it is from a future version of Git?\".\n\nI do not think customization based on git_config() callback like the\nabove can easily implement such an ideal semantics.\n\nAnd I suspect that the existing per-remote configuration that this\nvariable is meant to serve as a fallback definition would not work\nin such an ideal way (i.e., even if we are doing one-shot fetch that\ndoes not touch any remote-tracking hierarchies, \"git fetch\" may warn\nif the value is not understood, and when we do need the value, the\ncode would only warn and does not die), so in that sense this new\ncode is not making things _worse_, even though it may be spreading\nthe same badness more widely X-<.\n\nThanks.\n"},{"id":"545757","messageId":"xmqqcxxp1j2t.fsf@gitster.g","threadId":"65759","inReplyTo":"xmqqh5n213bw.fsf@gitster.g","subject":"Re: [PATCH v2 0/7] Introduce fetch.followRemoteHEAD config variable","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-17T11:51:06Z","receivedAt":"2026-06-17T11:51:09Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> ... to some \"unspecified\" or \"default\" value?  What does the existing\n> parser routine for remote.*.followremotehead do?\n>\n> Ideally,\n>\n>  (1) If the \"fetch\" operation ends up with not needing to consult\n>      the value of fetch.followRemoteHEAD at all (e.g., it is a\n>      one-shot fetch that updates no remote-tracking hierarchy, or it\n>      has a more specific per-remote setting that this variable is\n>      meant to serve as a mere fallback), any bogus or unknown value\n>      will not get any warning.\n>\n>  (2) If fetch.followRemoteHEAD ends up being _used_, and if it has\n>      an unknown value, we should at least warn \"we do not understand\n>      what you wrote, 'awlays', and we ignore it\", or die \"we do not\n>      understand 'reset', perhaps it is from a future version of Git?\".\n>\n> I do not think customization based on git_config() callback like the\n> above can easily implement such an ideal semantics.\n>\n> And I suspect that the existing per-remote configuration that this\n> variable is meant to serve as a fallback definition would not work\n> in such an ideal way (i.e., even if we are doing one-shot fetch that\n> does not touch any remote-tracking hierarchies, \"git fetch\" may warn\n> if the value is not understood, and when we do need the value, the\n> code would only warn and does not die), ...\n\nHaving said all that, I do not think it is a blocker for this series\nthat it does not take us into the more ideal direction and still\nmakes a syntax check on a value that will not be used and complains\nto the user.  We may want an in-code NEEDSWORK comment to hint\nfuture developers that they may want to revamp both of the code\npaths for fetch.followRemoteHEAD and remote.*.followremotehead not\nto complain when the values are unneeded and die when the unrecognized\nvalue is needed to continue, though.\n\nOther than that, this looks excellent.  Thanks.\n"},{"id":"545810","messageId":"DJBVYP58YNTU.LQ7VXFIQE84H@lfurio.us","threadId":"65759","inReplyTo":"xmqqcxxp1j2t.fsf@gitster.g","subject":"Re: [PATCH v2 0/7] Introduce fetch.followRemoteHEAD config variable","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-18T04:21:33Z","receivedAt":"2026-06-18T04:21:40Z","isPatch":true,"body":"On Wed Jun 17, 2026 at 7:51 AM EDT, Junio C Hamano wrote:\n>>\n>> Ideally,\n>>\n>>  (1) If the \"fetch\" operation ends up with not needing to consult\n>>      the value of fetch.followRemoteHEAD at all (e.g., it is a\n>>      one-shot fetch that updates no remote-tracking hierarchy, or it\n>>      has a more specific per-remote setting that this variable is\n>>      meant to serve as a mere fallback), any bogus or unknown value\n>>      will not get any warning.\n>>\n>>  (2) If fetch.followRemoteHEAD ends up being _used_, and if it has\n>>      an unknown value, we should at least warn \"we do not understand\n>>      what you wrote, 'awlays', and we ignore it\", or die \"we do not\n>>      understand 'reset', perhaps it is from a future version of Git?\".\n\nThis explanation makes much more sense to me than what you said in your\nresponse to the first iteration.  I believe I understand your vision\nbetter here.\n\n>>\n>> I do not think customization based on git_config() callback like the\n>> above can easily implement such an ideal semantics.\n>>\n>> And I suspect that the existing per-remote configuration that this\n>> variable is meant to serve as a fallback definition would not work\n>> in such an ideal way (i.e., even if we are doing one-shot fetch that\n>> does not touch any remote-tracking hierarchies, \"git fetch\" may warn\n>> if the value is not understood, and when we do need the value, the\n>> code would only warn and does not die), ...\n\nRight.  It seems like the design of the config callback mechanism\ndoesn't work well for the dynamic behavior described in your ideal case.\n\nI've tried to test out a few ideas to make it work, and each one so far\nends up feeling hacky very quickly.\n\n>\n> Having said all that, I do not think it is a blocker for this series\n> that it does not take us into the more ideal direction and still\n> makes a syntax check on a value that will not be used and complains\n> to the user.  We may want an in-code NEEDSWORK comment to hint\n> future developers that they may want to revamp both of the code\n> paths for fetch.followRemoteHEAD and remote.*.followremotehead not\n> to complain when the values are unneeded and die when the unrecognized\n> value is needed to continue, though.\n\nPersonally, even in the case where we can disregard any and all\nfollowRemoteHEAD settings on a one-shot fetch, I don't think die()-ing\non an unrecognized value should be the course of action.\n\nAs you pointed out in your last response to this topic, a future git\nrelease may implement additional choices for followRemoteHEAD.  If a\nuser opts in to this new functionality, but finds themself using an\nolder version of git (for whatever reason), I would still expect the\nfetch operation to continue, just using different semantics for\nfollowRemoteHEAD.\n\nIn fact, the better behavior might be to fall to \"never\" if the user\nasks to do something we don't understand.  In this case, we just emit\nthe warning, continue with fetch, but followRemoteHEAD does nothing -\nnot even create a missing ref.\n\n>\n> Other than that, this looks excellent.  Thanks.\n\nThanks for the great feedback and consideration!\n\nIf you like, I can apply the appropriate NEEDSWORK comment, possibly add\na warning to 'fetch.followRemoteHEAD' parsing (matching the 'remote'\nside), and we can call this good to go for now.\n"},{"id":"545861","messageId":"xmqqmrwrsvey.fsf@gitster.g","threadId":"65759","inReplyTo":"DJBVYP58YNTU.LQ7VXFIQE84H@lfurio.us","subject":"Re: [PATCH v2 0/7] Introduce fetch.followRemoteHEAD config variable","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-06-18T15:47:01Z","receivedAt":"2026-06-18T15:47:04Z","isPatch":true,"body":"\"Matt Hunter\" <m@lfurio.us> writes:\n\n>> Other than that, this looks excellent.  Thanks.\n>\n> Thanks for the great feedback and consideration!\n>\n> If you like, I can apply the appropriate NEEDSWORK comment, possibly add\n> a warning to 'fetch.followRemoteHEAD' parsing (matching the 'remote'\n> side), and we can call this good to go for now.\n\nSounds like a plan.  Thanks.\n"},{"id":"545926","messageId":"20260619094751.2996804-4-m@lfurio.us","threadId":"65759","inReplyTo":"20260619094751.2996804-1-m@lfurio.us","subject":"[PATCH v3 3/8] t5510: cleanup remote in followRemoteHEAD dangling ref test","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-19T09:44:22Z","receivedAt":"2026-06-19T09:48:25Z","isPatch":true,"body":"A later patch will introduce a new test which closely mirrors this one.\nUpdate this test to remove the 'custom-head' remote it creates.\nOtherwise, the two tests will conflict with each other, as the second\none to execute will fail to create this remote (which already exists,\nthanks to the first test).\n\nSigned-off-by: Matt Hunter <m@lfurio.us>\n---\n t/t5510-fetch.sh | 1 +\n 1 file changed, 1 insertion(+)\n\ndiff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh\nindex eca9a973b5cb..43190630e714 100755\n--- a/t/t5510-fetch.sh\n+++ b/t/t5510-fetch.sh\n@@ -251,6 +251,7 @@ test_expect_success 'followRemoteHEAD does not kick in with refspecs' '\n '\n \n test_expect_success 'followRemoteHEAD create does not overwrite dangling symref' '\n+\ttest_when_finished \"git -C two remote remove custom-head\" &&\n \tgit -C two remote add -m does-not-exist custom-head ../one &&\n \ttest_config -C two remote.custom-head.followRemoteHEAD create &&\n \tgit -C two fetch custom-head &&\n-- \n2.54.0\n\n"},{"id":"545927","messageId":"20260619094751.2996804-1-m@lfurio.us","threadId":"65759","inReplyTo":"20260612055947.1499497-1-m@lfurio.us","subject":"[PATCH v3 0/8] Introduce fetch.followRemoteHEAD config variable","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-19T09:44:19Z","receivedAt":"2026-06-19T09:48:25Z","isPatch":true,"body":"git-fetch presently offers some useful ways to control how remote HEAD\nsymbolic-refs are (or aren't) updated when fetching from remote\nrepositories.  Namely this is done via the\n'remote.<name>.followRemoteHEAD' configuration variable.\n\nHowever, this setting can be somewhat painful to use if you prefer a\ndefault other than \"create\" and often work with multiple different\nremote repositories.\n\nThis series introduces the variable 'fetch.followRemoteHEAD', which\nprovides a configurable default in place of per-remote settings.\n\n'fetch.followRemoteHEAD' functions exactly the same as the original\nvariable, except that it doesn't allow warning suppression via\n'warn-if-not-$branch'.  Given that different remotes will vary their\nHEAD and set of branches independently, setting a false-positive\nglobally in this way doesn't make logical sense.\n\nWhile it is not mentioned by any of the patches in this series, note\nalso that the behavior introduced by 012bc566bad7 (remote set-head: set\nfollowRemoteHEAD to \"warn\" if \"always\") is unaffected by this series,\nand this feature continues to work for only the\n'remote.<name>.followRemoteHEAD' variable.\n\n--- \n\nHi Junio,\n\nThe changes we discussed are implemented, but I also included a last\nsecond related fix to control flow of git-fetch config parsing.\n\nSee patch 5/8 (fetch: return 0 on known git_fetch_config),\nas well as a similar line squashed into\n7/8 (fetch: add configuration variable fetch.followRemoteHEAD)\n\nThanks.\n\nChanges in v3:\n  - Produce warning when fetch.followRemoteHEAD is set to a bogus value.\n  - Leave NEEDSWORK comment detailing future improvements.\n  - Avoid calling git_default_config unnecessarily in git-fetch.\n  - Link to v2: https://patch.msgid.link/20260616222606.1003521-1-m@lfurio.us\n\nChanges in v2:\n  - Don't die() if the value of fetch.followRemoteHEAD is unrecognized.\n  - Use case-sensitive matching for fetch.followRemoteHEAD values.\n  - Avoid the phrase \"configuration option\".\n  - Minor documentation wording changes.\n  - Link to v1: https://patch.msgid.link/20260612055947.1499497-1-m@lfurio.us\n\nMatt Hunter (8):\n  fetch: fixup set_head advice for warn-if-not-branch\n  doc: explain fetchRemoteHEADWarn advice\n  t5510: cleanup remote in followRemoteHEAD dangling ref test\n  fetch: rename function report_set_head\n  fetch: return 0 on known git_fetch_config\n  fetch: refactor do_fetch handling of followRemoteHEAD\n  fetch: add configuration variable fetch.followRemoteHEAD\n  fetch: fixup a misaligned comment\n\n Documentation/config/advice.adoc |   4 ++\n Documentation/config/fetch.adoc  |  19 ++++++\n Documentation/config/remote.adoc |  21 +++---\n builtin/fetch.c                  |  62 ++++++++++++++----\n remote.h                         |  14 ++--\n t/t5510-fetch.sh                 | 106 +++++++++++++++++++++++++++++++\n 6 files changed, 196 insertions(+), 30 deletions(-)\n\nRange-diff against v2:\n1:  2106228f7b98 = 1:  48b23e0e2008 fetch: fixup set_head advice for warn-if-not-branch\n2:  b1c58c06e0c7 = 2:  a68e5edf92b7 doc: explain fetchRemoteHEADWarn advice\n3:  c1d11e8883e6 = 3:  bfe7891e6105 t5510: cleanup remote in followRemoteHEAD dangling ref test\n4:  6306c8212fc0 = 4:  8bc1e56dafca fetch: rename function report_set_head\n-:  ------------ > 5:  3568b03adc97 fetch: return 0 on known git_fetch_config\n5:  3c7257094686 = 6:  b6c919d821d0 fetch: refactor do_fetch handling of followRemoteHEAD\n6:  af9f99b1ceb2 ! 7:  dc1e05646887 fetch: add configuration variable fetch.followRemoteHEAD\n    @@ Commit message\n         warn-if-not-$branch, due to its tighter coupling to individual remote\n         repositories.\n     \n    +    This setting interacts with the do_fetch mechanism in the same way as\n    +    the previous does, but there are opportunities for improved\n    +    user-experience discussed in [1].  See the included NEEDSWORK comment as\n    +    well.\n    +\n         Documentation and advice messages for both of the followRemoteHEAD\n         variables are reworded to better capture the relationship between the\n         two.\n    @@ Commit message\n         variables, as well as the fact that 'remote.<name>.followRemoteHEAD'\n         always supersedes this new configurable default.\n     \n    +    [1]: https://lore.kernel.org/git/xmqqh5n213bw.fsf@gitster.g/\n    +\n         Helped-by: Junio C Hamano <gitster@pobox.com>\n         Signed-off-by: Matt Hunter <m@lfurio.us>\n     \n    @@ builtin/fetch.c: static struct string_list negotiation_include = STRING_LIST_INI\n      \tint prune;\n      \tint prune_tags;\n     @@ builtin/fetch.c: static int git_fetch_config(const char *k, const char *v,\n    - \t\t\t    \"fetch.output\", v);\n    + \t\treturn 0;\n      \t}\n      \n     +\tif (!strcmp(k, \"fetch.followremotehead\")) {\n    @@ builtin/fetch.c: static int git_fetch_config(const char *k, const char *v,\n     +\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_WARN;\n     +\t\telse if (!strcmp(v, \"always\"))\n     +\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_ALWAYS;\n    ++\t\telse\n    ++\t\t\twarning(_(\"unrecognized fetch.followRemoteHEAD value '%s' ignored\"), v);\n    ++\t\treturn 0;\n     +\t}\n     +\n      \treturn git_default_config(k, v, ctx, cb);\n    @@ builtin/fetch.c: static const char *strip_refshead(const char *name){\n      \tadvise_if_enabled(ADVICE_FETCH_SET_HEAD_WARN, _(message_advice_set_head),\n      \t\t\tremote, head_name, remote, remote, head_name);\n     @@ builtin/fetch.c: static int do_fetch(struct transport *transport,\n    + \t\t\tgoto cleanup;\n    + \t}\n      \n    ++\t/*\n    ++\t * NEEDSWORK: By the time this function executes, we have already parsed\n    ++\t * all such followRemoteHEAD values from the external configuration,\n    ++\t * potentially emitting warning messages for bogus values.  Ideally, if\n    ++\t * this fetch ends up not needing to consult these values, then git would\n    ++\t * not ever output a value warning. (eg: when pulling from a URL directly -\n    ++\t * rather than a configured remote, or when a remote's followRemoteHEAD\n    ++\t * overrides the fallback fetch setting)\n    ++\t */\n      \tif (transport->remote->follow_remote_head)\n      \t\tfollow_remote_head = transport->remote->follow_remote_head;\n     +\telse if (config->follow_remote_head)\n7:  5c80107f6488 = 8:  f9555a0d5cea fetch: fixup a misaligned comment\n\nbase-commit: 95e20213faefeb95df29277c58ac1980ab68f701\n-- \n2.54.0\n\n"},{"id":"545928","messageId":"20260619094751.2996804-2-m@lfurio.us","threadId":"65759","inReplyTo":"20260619094751.2996804-1-m@lfurio.us","subject":"[PATCH v3 1/8] fetch: fixup set_head advice for warn-if-not-branch","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-19T09:44:20Z","receivedAt":"2026-06-19T09:48:26Z","isPatch":true,"body":"Specifying the word 'branch' in the command is not correct - a mismatch\nwith both the implementation in remote.c and the documentation.\n\nSigned-off-by: Matt Hunter <m@lfurio.us>\n---\n builtin/fetch.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex c1d7c672f4e0..82969e230f5a 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1700,7 +1700,7 @@ static void set_head_advice_msg(const char *remote, const char *head_name)\n \tN_(\"Run 'git remote set-head %s %s' to follow the change, or set\\n\"\n \t   \"'remote.%s.followRemoteHEAD' configuration option to a different value\\n\"\n \t   \"if you do not want to see this message. Specifically running\\n\"\n-\t   \"'git config set remote.%s.followRemoteHEAD warn-if-not-branch-%s'\\n\"\n+\t   \"'git config set remote.%s.followRemoteHEAD warn-if-not-%s'\\n\"\n \t   \"will disable the warning until the remote changes HEAD to something else.\");\n \n \tadvise_if_enabled(ADVICE_FETCH_SET_HEAD_WARN, _(message_advice_set_head),\n-- \n2.54.0\n\n"},{"id":"545929","messageId":"20260619094751.2996804-5-m@lfurio.us","threadId":"65759","inReplyTo":"20260619094751.2996804-1-m@lfurio.us","subject":"[PATCH v3 4/8] fetch: rename function report_set_head","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-19T09:44:23Z","receivedAt":"2026-06-19T09:48:28Z","isPatch":true,"body":"Update to the slightly more obvious name 'warn_set_head', which matches\nthe verbiage of the followRemoteHEAD options.\n\nSigned-off-by: Matt Hunter <m@lfurio.us>\n---\n builtin/fetch.c | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 82969e230f5a..9a45e1e7a44d 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1707,7 +1707,7 @@ static void set_head_advice_msg(const char *remote, const char *head_name)\n \t\t\tremote, head_name, remote, remote, head_name);\n }\n \n-static void report_set_head(const char *remote, const char *head_name,\n+static void warn_set_head(const char *remote, const char *head_name,\n \t\t\tstruct strbuf *buf_prev, int updateres) {\n \tstruct strbuf buf_prefix = STRBUF_INIT;\n \tconst char *prev_head = NULL;\n@@ -1787,7 +1787,7 @@ static int set_head(const struct ref *remote_refs, struct remote *remote)\n \tif (verbosity >= 0 &&\n \t\tfollow_remote_head == FOLLOW_REMOTE_WARN &&\n \t\t(!no_warn_branch || strcmp(no_warn_branch, head_name)))\n-\t\treport_set_head(remote->name, head_name, &b_local_head, was_detached);\n+\t\twarn_set_head(remote->name, head_name, &b_local_head, was_detached);\n \n cleanup:\n \tfree(head_name);\n-- \n2.54.0\n\n"},{"id":"545930","messageId":"20260619094751.2996804-6-m@lfurio.us","threadId":"65759","inReplyTo":"20260619094751.2996804-1-m@lfurio.us","subject":"[PATCH v3 5/8] fetch: return 0 on known git_fetch_config","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-19T09:44:24Z","receivedAt":"2026-06-19T09:48:29Z","isPatch":true,"body":"The git config callback for git-fetch should only forward calls to\ngit_default_config when an unknown key is given.  Prevent this in the\ncase of 'fetch.output' by returning '0', as the other known keys do.\n\nSigned-off-by: Matt Hunter <m@lfurio.us>\n---\n builtin/fetch.c | 1 +\n 1 file changed, 1 insertion(+)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 9a45e1e7a44d..1036e8edbc59 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -171,6 +171,7 @@ static int git_fetch_config(const char *k, const char *v,\n \t\telse\n \t\t\tdie(_(\"invalid value for '%s': '%s'\"),\n \t\t\t    \"fetch.output\", v);\n+\t\treturn 0;\n \t}\n \n \treturn git_default_config(k, v, ctx, cb);\n-- \n2.54.0\n\n"},{"id":"545931","messageId":"20260619094751.2996804-3-m@lfurio.us","threadId":"65759","inReplyTo":"20260619094751.2996804-1-m@lfurio.us","subject":"[PATCH v3 2/8] doc: explain fetchRemoteHEADWarn advice","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-19T09:44:21Z","receivedAt":"2026-06-19T09:48:29Z","isPatch":true,"body":"When the user sets 'remote.<name>.followRemoteHEAD' to\n'warn[-if-not-$branch]', git-fetch will report when a fetched HEAD\ndisagrees with the locally-configured remote's HEAD.  This additional\nadvice instructs the user how to deal with these warnings, but was\npreviously undocumented in git-config.\n\nSigned-off-by: Matt Hunter <m@lfurio.us>\n---\n Documentation/config/advice.adoc | 4 ++++\n 1 file changed, 4 insertions(+)\n\ndiff --git a/Documentation/config/advice.adoc b/Documentation/config/advice.adoc\nindex 257db5891817..c3c190ba6a4f 100644\n--- a/Documentation/config/advice.adoc\n+++ b/Documentation/config/advice.adoc\n@@ -48,6 +48,10 @@ all advice messages.\n \t\tto create a local branch after the fact.\n \tdiverging::\n \t\tShown when a fast-forward is not possible.\n+\tfetchRemoteHEADWarn::\n+\t\tShown when linkgit:git-fetch[1] reveals that a remote `HEAD`\n+\t\tdiffers from what is set locally and the user has opted into\n+\t\treceiving a warning in this situation.\n \tfetchShowForcedUpdates::\n \t\tShown when linkgit:git-fetch[1] takes a long time\n \t\tto calculate forced updates after ref updates, or to warn\n-- \n2.54.0\n\n"},{"id":"545932","messageId":"20260619094751.2996804-7-m@lfurio.us","threadId":"65759","inReplyTo":"20260619094751.2996804-1-m@lfurio.us","subject":"[PATCH v3 6/8] fetch: refactor do_fetch handling of followRemoteHEAD","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-19T09:44:25Z","receivedAt":"2026-06-19T09:48:31Z","isPatch":true,"body":"Update enum follow_remote_head_settings to include the value\nFOLLOW_REMOTE_UNCONFIGURED as the new zero-initialized value for\nfollowRemoteHEAD.  This will allow us to distinguish between the\nvariable being unset vs. explicitly set to 'create', which is ultimately\nthe system default.  The unnecessary indentation is removed.\n\nThe do_fetch function is likewise updated to perform its own decision\nmaking to determine the effective followRemoteHEAD mode, falling back to\nthe system default if necessary.  This will enable the next patch to\nintroduce a user-configurable default.\n\nFunction set_head now accepts the mode as an argument rather than only\nconsidering the value defined by the remote.\n\nThe use of the 'warn-if-not-$branch' value is awkward in the context of\na global default, since the branches will differ between individual\nremotes.  For this reason, it's left out of this scheme and handling of\nthe no_warn_branch variable is untouched.  Since a remote-specific\nvalue for followRemoteHEAD takes priority, we can assume that if\nremote->no_warn_branch is set, then the remote is also asserting\nFOLLOW_REMOTE_WARN as the effective operating mode, and it will be\nhonored by do_fetch.\n\nSigned-off-by: Matt Hunter <m@lfurio.us>\n---\n builtin/fetch.c | 14 ++++++++++----\n remote.h        | 14 ++++++++------\n 2 files changed, 18 insertions(+), 10 deletions(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 1036e8edbc59..ad63ca943c33 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1730,12 +1730,12 @@ static void warn_set_head(const char *remote, const char *head_name,\n \tstrbuf_release(&buf_prefix);\n }\n \n-static int set_head(const struct ref *remote_refs, struct remote *remote)\n+static int set_head(const struct ref *remote_refs, struct remote *remote,\n+\t\t\tint follow_remote_head)\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-\tint follow_remote_head = remote->follow_remote_head;\n \tconst char *no_warn_branch = remote->no_warn_branch;\n \tchar *head_name = NULL;\n \tstruct ref *ref, *matches;\n@@ -1902,6 +1902,7 @@ static int do_fetch(struct transport *transport,\n \tstruct ref_update_display_info_array display_array = { 0 };\n \tstruct strmap rejected_refs = STRMAP_INIT;\n \tint summary_width = 0;\n+\tint follow_remote_head;\n \n \tif (tags == TAGS_DEFAULT) {\n \t\tif (transport->remote->fetch_tags == 2)\n@@ -1917,6 +1918,11 @@ static int do_fetch(struct transport *transport,\n \t\t\tgoto cleanup;\n \t}\n \n+\tif (transport->remote->follow_remote_head)\n+\t\tfollow_remote_head = transport->remote->follow_remote_head;\n+\telse\n+\t\tfollow_remote_head = BUILTIN_FOLLOW_REMOTE_HEAD_DFLT;\n+\n \tif (rs->nr) {\n \t\trefspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes);\n \t} else {\n@@ -1925,7 +1931,7 @@ static int do_fetch(struct transport *transport,\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 (transport->remote->follow_remote_head != FOLLOW_REMOTE_NEVER)\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@@ -2132,7 +2138,7 @@ 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);\n+\t\tset_head(remote_refs, transport->remote, follow_remote_head);\n \t}\n \n cleanup:\ndiff --git a/remote.h b/remote.h\nindex 54b17e4b028b..72a54d84ad51 100644\n--- a/remote.h\n+++ b/remote.h\n@@ -62,12 +62,14 @@ struct remote_state {\n void remote_state_clear(struct remote_state *remote_state);\n struct remote_state *remote_state_new(void);\n \n-\tenum follow_remote_head_settings {\n-\t\tFOLLOW_REMOTE_NEVER = -1,\n-\t\tFOLLOW_REMOTE_CREATE = 0,\n-\t\tFOLLOW_REMOTE_WARN = 1,\n-\t\tFOLLOW_REMOTE_ALWAYS = 2,\n-\t};\n+#define BUILTIN_FOLLOW_REMOTE_HEAD_DFLT FOLLOW_REMOTE_CREATE\n+enum follow_remote_head_settings {\n+\tFOLLOW_REMOTE_UNCONFIGURED = 0,\n+\tFOLLOW_REMOTE_NEVER,\n+\tFOLLOW_REMOTE_CREATE,\n+\tFOLLOW_REMOTE_WARN,\n+\tFOLLOW_REMOTE_ALWAYS,\n+};\n \n struct remote {\n \tstruct hashmap_entry ent;\n-- \n2.54.0\n\n"},{"id":"545933","messageId":"20260619094751.2996804-8-m@lfurio.us","threadId":"65759","inReplyTo":"20260619094751.2996804-1-m@lfurio.us","subject":"[PATCH v3 7/8] fetch: add configuration variable fetch.followRemoteHEAD","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-19T09:44:26Z","receivedAt":"2026-06-19T09:48:33Z","isPatch":true,"body":"'fetch.followRemoteHEAD' is added as a generic setting used by all\nremotes for which 'remote.<name>.followRemoteHEAD' is undefined.  If\nboth variables are undefined, a builtin default of \"create\" is in\neffect, matching the previous behavior.\n\nAs mentioned in the previous patch, 'fetch.followRemoteHEAD' supports\nall of the values that its 'remote' counterpart does _except_\nwarn-if-not-$branch, due to its tighter coupling to individual remote\nrepositories.\n\nThis setting interacts with the do_fetch mechanism in the same way as\nthe previous does, but there are opportunities for improved\nuser-experience discussed in [1].  See the included NEEDSWORK comment as\nwell.\n\nDocumentation and advice messages for both of the followRemoteHEAD\nvariables are reworded to better capture the relationship between the\ntwo.\n\nThe added tests assert feature parity between the two followRemoteHEAD\nvariables, as well as the fact that 'remote.<name>.followRemoteHEAD'\nalways supersedes this new configurable default.\n\n[1]: https://lore.kernel.org/git/xmqqh5n213bw.fsf@gitster.g/\n\nHelped-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Matt Hunter <m@lfurio.us>\n---\n Documentation/config/fetch.adoc  |  19 ++++++\n Documentation/config/remote.adoc |  21 +++----\n builtin/fetch.c                  |  41 ++++++++++--\n t/t5510-fetch.sh                 | 105 +++++++++++++++++++++++++++++++\n 4 files changed, 169 insertions(+), 17 deletions(-)\n\ndiff --git a/Documentation/config/fetch.adoc b/Documentation/config/fetch.adoc\nindex 04ac90912d3a..00435e9a16d9 100644\n--- a/Documentation/config/fetch.adoc\n+++ b/Documentation/config/fetch.adoc\n@@ -126,3 +126,22 @@ the new bundle URI.\n The creation token values are chosen by the provider serving the specific\n bundle URI. If you modify the URI at `fetch.bundleURI`, then be sure to\n remove the value for the `fetch.bundleCreationToken` value before fetching.\n+\n+`fetch.followRemoteHEAD`::\n+\tWhen fetching using a default refspec, this setting determines how to handle\n+\tdifferences between a fetched remote's `HEAD` and the local\n+\t`remotes/<name>/HEAD` symbolic-ref.  Its value is one of\n++\n+--\n+`create`;;\n+\tCreate `remotes/<name>/HEAD` if a ref exists on the remote, but not locally.\n+\tAn existing symbolic-ref will not be touched.  This is the default value.\n+`warn`;;\n+\tDisplay a warning if the remote advertises a different `HEAD` than what is\n+\tset locally.  Behaves like \"create\" if the local symbolic-ref doesn't exist.\n+`always`;;\n+\tSilently update `remotes/<name>/HEAD` whenever the remote advertises a new\n+\tvalue.\n+`never`;;\n+\tNever create or modify the `remotes/<name>/HEAD` symbolic-ref.\n+--\ndiff --git a/Documentation/config/remote.adoc b/Documentation/config/remote.adoc\nindex eb9c8a3c4884..04724bc51628 100644\n--- a/Documentation/config/remote.adoc\n+++ b/Documentation/config/remote.adoc\n@@ -157,15 +157,12 @@ Blank values signal to ignore all previous values, allowing a reset of\n the list from broader config scenarios.\n \n remote.<name>.followRemoteHEAD::\n-\tHow linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`\n-\twhen fetching using the configured refspecs of a remote.\n-\tThe default value is \"create\", which will create `remotes/<name>/HEAD`\n-\tif it exists on the remote, but not locally; this will not touch an\n-\talready existing local reference. Setting it to \"warn\" will print\n-\ta message if the remote has a different value than the local one;\n-\tin case there is no local reference, it behaves like \"create\".\n-\tA variant on \"warn\" is \"warn-if-not-$branch\", which behaves like\n-\t\"warn\", but if `HEAD` on the remote is `$branch` it will be silent.\n-\tSetting it to \"always\" will silently update `remotes/<name>/HEAD` to\n-\tthe value on the remote.  Finally, setting it to \"never\" will never\n-\tchange or create the local reference.\n+\tWhen fetching this remote using its default refspec, this setting determines\n+\thow to handle differences between the remote's `HEAD` and the local\n+\t`remotes/<name>/HEAD` symbolic-ref.  Overrides the value of\n+\t`fetch.followRemoteHEAD`.  See `fetch.followRemoteHEAD` for a description of\n+\taccepted values.\n++\n+In addition to the values supported by `fetch.followRemoteHEAD`, this setting\n+may also take on the value \"warn-if-not-`$branch`\", which behaves like \"warn\",\n+but ignores the warning if the remote's `HEAD` is `remotes/<name>/$branch`.\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex ad63ca943c33..3c8210d1776f 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -103,6 +103,7 @@ static struct string_list negotiation_include = STRING_LIST_INIT_NODUP;\n \n struct fetch_config {\n \tenum display_format display_format;\n+\tenum follow_remote_head_settings follow_remote_head;\n \tint all;\n \tint prune;\n \tint prune_tags;\n@@ -174,6 +175,22 @@ static int git_fetch_config(const char *k, const char *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+\t\telse if (!strcmp(v, \"never\"))\n+\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_NEVER;\n+\t\telse if (!strcmp(v, \"create\"))\n+\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_CREATE;\n+\t\telse if (!strcmp(v, \"warn\"))\n+\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_WARN;\n+\t\telse if (!strcmp(v, \"always\"))\n+\t\t\tfetch_config->follow_remote_head = FOLLOW_REMOTE_ALWAYS;\n+\t\telse\n+\t\t\twarning(_(\"unrecognized fetch.followRemoteHEAD value '%s' ignored\"), v);\n+\t\treturn 0;\n+\t}\n+\n \treturn git_default_config(k, v, ctx, cb);\n }\n \n@@ -1698,11 +1715,13 @@ static const char *strip_refshead(const char *name){\n static void set_head_advice_msg(const char *remote, const char *head_name)\n {\n \tconst char message_advice_set_head[] =\n-\tN_(\"Run 'git remote set-head %s %s' to follow the change, or set\\n\"\n-\t   \"'remote.%s.followRemoteHEAD' configuration option to a different value\\n\"\n-\t   \"if you do not want to see this message. Specifically running\\n\"\n-\t   \"'git config set remote.%s.followRemoteHEAD warn-if-not-%s'\\n\"\n-\t   \"will disable the warning until the remote changes HEAD to something else.\");\n+\tN_(\"Run 'git remote set-head %s %s' to follow the change, or modify\\n\"\n+\t   \"either of the 'remote.%s.followRemoteHEAD' or 'fetch.followRemoteHEAD'\\n\"\n+\t   \"configuration variables to handle the situation differently.\\n\\n\"\n+\n+\t   \"Using this specific setting\\n\\n\"\n+\t   \"    git config set remote.%s.followRemoteHEAD warn-if-not-%s\\n\\n\"\n+\t   \"will suppress the warning until the remote changes HEAD to something else.\");\n \n \tadvise_if_enabled(ADVICE_FETCH_SET_HEAD_WARN, _(message_advice_set_head),\n \t\t\tremote, head_name, remote, remote, head_name);\n@@ -1918,8 +1937,19 @@ static int do_fetch(struct transport *transport,\n \t\t\tgoto cleanup;\n \t}\n \n+\t/*\n+\t * NEEDSWORK: By the time this function executes, we have already parsed\n+\t * all such followRemoteHEAD values from the external configuration,\n+\t * potentially emitting warning messages for bogus values.  Ideally, if\n+\t * this fetch ends up not needing to consult these values, then git would\n+\t * not ever output a value warning. (eg: when pulling from a URL directly -\n+\t * rather than a configured remote, or when a remote's followRemoteHEAD\n+\t * overrides the fallback fetch setting)\n+\t */\n \tif (transport->remote->follow_remote_head)\n \t\tfollow_remote_head = transport->remote->follow_remote_head;\n+\telse if (config->follow_remote_head)\n+\t\tfollow_remote_head = config->follow_remote_head;\n \telse\n \t\tfollow_remote_head = BUILTIN_FOLLOW_REMOTE_HEAD_DFLT;\n \n@@ -2478,6 +2508,7 @@ int cmd_fetch(int argc,\n {\n \tstruct fetch_config config = {\n \t\t.display_format = DISPLAY_FORMAT_FULL,\n+\t\t.follow_remote_head = FOLLOW_REMOTE_UNCONFIGURED,\n \t\t.prune = -1,\n \t\t.prune_tags = -1,\n \t\t.show_forced_updates = 1,\ndiff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh\nindex 43190630e714..6f0ae1bdd798 100755\n--- a/t/t5510-fetch.sh\n+++ b/t/t5510-fetch.sh\n@@ -140,6 +140,16 @@ test_expect_success \"fetch test remote HEAD change\" '\n \t)\n '\n \n+test_expect_success \"fetch test default followRemoteHEAD never\" '\n+\tgit -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&\n+\ttest_config -C two fetch.followRemoteHEAD \"never\" &&\n+\tGIT_TRACE_PACKET=$PWD/trace.out git -C two fetch &&\n+\t# Confirm that we do not even ask for HEAD when we are\n+\t# not going to act on it.\n+\ttest_grep ! \"ref-prefix HEAD\" trace.out &&\n+\ttest_must_fail git -C two rev-parse --verify refs/remotes/origin/HEAD\n+'\n+\n test_expect_success \"fetch test followRemoteHEAD never\" '\n \tgit -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&\n \ttest_config -C two remote.origin.followRemoteHEAD \"never\" &&\n@@ -150,6 +160,21 @@ test_expect_success \"fetch test followRemoteHEAD never\" '\n \ttest_must_fail git -C two rev-parse --verify refs/remotes/origin/HEAD\n '\n \n+test_expect_success \"fetch test default followRemoteHEAD warn no change\" '\n+\tgit -C two rev-parse --verify refs/remotes/origin/other &&\n+\tgit -C two remote set-head origin other &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/HEAD &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/main &&\n+\ttest_config -C two fetch.followRemoteHEAD \"warn\" &&\n+\tgit -C two fetch >output &&\n+\techo \"${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ},\" \\\n+\t\t\"but we have ${SQ}other${SQ} locally.\" >expect &&\n+\ttest_cmp expect output &&\n+\thead=$(git -C two rev-parse refs/remotes/origin/HEAD) &&\n+\tbranch=$(git -C two rev-parse refs/remotes/origin/other) &&\n+\ttest \"z$head\" = \"z$branch\"\n+'\n+\n test_expect_success \"fetch test followRemoteHEAD warn no change\" '\n \tgit -C two rev-parse --verify refs/remotes/origin/other &&\n \tgit -C two remote set-head origin other &&\n@@ -165,6 +190,17 @@ test_expect_success \"fetch test followRemoteHEAD warn no change\" '\n \ttest \"z$head\" = \"z$branch\"\n '\n \n+test_expect_success \"fetch test default followRemoteHEAD warn create\" '\n+\tgit -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&\n+\ttest_config -C two fetch.followRemoteHEAD \"warn\" &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/main &&\n+\toutput=$(git -C two fetch) &&\n+\ttest \"z\" = \"z$output\" &&\n+\thead=$(git -C two rev-parse refs/remotes/origin/HEAD) &&\n+\tbranch=$(git -C two rev-parse refs/remotes/origin/main) &&\n+\ttest \"z$head\" = \"z$branch\"\n+'\n+\n test_expect_success \"fetch test followRemoteHEAD warn create\" '\n \tgit -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&\n \ttest_config -C two remote.origin.followRemoteHEAD \"warn\" &&\n@@ -176,6 +212,18 @@ test_expect_success \"fetch test followRemoteHEAD warn create\" '\n \ttest \"z$head\" = \"z$branch\"\n '\n \n+test_expect_success \"fetch test default followRemoteHEAD warn detached\" '\n+\tgit -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&\n+\tgit -C two update-ref refs/remotes/origin/HEAD HEAD &&\n+\tHEAD=$(git -C two log --pretty=\"%H\") &&\n+\ttest_config -C two fetch.followRemoteHEAD \"warn\" &&\n+\tgit -C two fetch >output &&\n+\techo \"${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ},\" \\\n+\t\t\"but we have a detached HEAD pointing to\" \\\n+\t\t\"${SQ}${HEAD}${SQ} locally.\" >expect &&\n+\ttest_cmp expect output\n+'\n+\n test_expect_success \"fetch test followRemoteHEAD warn detached\" '\n \tgit -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&\n \tgit -C two update-ref refs/remotes/origin/HEAD HEAD &&\n@@ -188,6 +236,19 @@ test_expect_success \"fetch test followRemoteHEAD warn detached\" '\n \ttest_cmp expect output\n '\n \n+test_expect_success \"fetch test default followRemoteHEAD warn quiet\" '\n+\tgit -C two rev-parse --verify refs/remotes/origin/other &&\n+\tgit -C two remote set-head origin other &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/HEAD &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/main &&\n+\ttest_config -C two fetch.followRemoteHEAD \"warn\" &&\n+\toutput=$(git -C two fetch --quiet) &&\n+\ttest \"z\" = \"z$output\" &&\n+\thead=$(git -C two rev-parse refs/remotes/origin/HEAD) &&\n+\tbranch=$(git -C two rev-parse refs/remotes/origin/other) &&\n+\ttest \"z$head\" = \"z$branch\"\n+'\n+\n test_expect_success \"fetch test followRemoteHEAD warn quiet\" '\n \tgit -C two rev-parse --verify refs/remotes/origin/other &&\n \tgit -C two remote set-head origin other &&\n@@ -229,6 +290,18 @@ test_expect_success \"fetch test followRemoteHEAD warn-if-not-branch branch is di\n \ttest \"z$head\" = \"z$branch\"\n '\n \n+test_expect_success \"fetch test default followRemoteHEAD always\" '\n+\tgit -C two rev-parse --verify refs/remotes/origin/other &&\n+\tgit -C two remote set-head origin other &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/HEAD &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/main &&\n+\ttest_config -C two fetch.followRemoteHEAD \"always\" &&\n+\tgit -C two fetch &&\n+\thead=$(git -C two rev-parse refs/remotes/origin/HEAD) &&\n+\tbranch=$(git -C two rev-parse refs/remotes/origin/main) &&\n+\ttest \"z$head\" = \"z$branch\"\n+'\n+\n test_expect_success \"fetch test followRemoteHEAD always\" '\n \tgit -C two rev-parse --verify refs/remotes/origin/other &&\n \tgit -C two remote set-head origin other &&\n@@ -241,6 +314,28 @@ test_expect_success \"fetch test followRemoteHEAD always\" '\n \ttest \"z$head\" = \"z$branch\"\n '\n \n+test_expect_success 'per-remote followRemoteHEAD takes priority over fetch default' '\n+\tgit -C two rev-parse --verify refs/remotes/origin/other &&\n+\tgit -C two remote set-head origin other &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/HEAD &&\n+\tgit -C two rev-parse --verify refs/remotes/origin/main &&\n+\ttest_config -C two fetch.followRemoteHEAD \"never\" &&\n+\ttest_config -C two remote.origin.followRemoteHEAD \"always\" &&\n+\tgit -C two fetch &&\n+\thead=$(git -C two rev-parse refs/remotes/origin/HEAD) &&\n+\tbranch=$(git -C two rev-parse refs/remotes/origin/main) &&\n+\ttest \"z$head\" = \"z$branch\"\n+'\n+\n+test_expect_success 'default followRemoteHEAD does not kick in with refspecs' '\n+\tgit -C two remote set-head origin other &&\n+\ttest_config -C two fetch.followRemoteHEAD always &&\n+\tgit -C two fetch origin refs/heads/main:refs/remotes/origin/main &&\n+\techo refs/remotes/origin/other >expect &&\n+\tgit -C two symbolic-ref refs/remotes/origin/HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'followRemoteHEAD does not kick in with refspecs' '\n \tgit -C two remote set-head origin other &&\n \ttest_config -C two remote.origin.followRemoteHEAD always &&\n@@ -250,6 +345,16 @@ test_expect_success 'followRemoteHEAD does not kick in with refspecs' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'default followRemoteHEAD create does not overwrite dangling symref' '\n+\ttest_when_finished \"git -C two remote remove custom-head\" &&\n+\tgit -C two remote add -m does-not-exist custom-head ../one &&\n+\ttest_config -C two fetch.followRemoteHEAD create &&\n+\tgit -C two fetch custom-head &&\n+\techo refs/remotes/custom-head/does-not-exist >expect &&\n+\tgit -C two symbolic-ref refs/remotes/custom-head/HEAD >actual &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'followRemoteHEAD create does not overwrite dangling symref' '\n \ttest_when_finished \"git -C two remote remove custom-head\" &&\n \tgit -C two remote add -m does-not-exist custom-head ../one &&\n-- \n2.54.0\n\n"},{"id":"545934","messageId":"20260619094751.2996804-9-m@lfurio.us","threadId":"65759","inReplyTo":"20260619094751.2996804-1-m@lfurio.us","subject":"[PATCH v3 8/8] fetch: fixup a misaligned comment","fromName":"Matt Hunter","fromEmail":"m@lfurio.us","sentAt":"2026-06-19T09:44:27Z","receivedAt":"2026-06-19T09:48:35Z","isPatch":true,"body":"Signed-off-by: Matt Hunter <m@lfurio.us>\n---\n builtin/fetch.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 3c8210d1776f..25ab8803a819 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -1793,7 +1793,7 @@ static int set_head(const struct ref *remote_refs, struct remote *remote,\n \t\tstrbuf_addf(&b_head, \"refs/remotes/%s/HEAD\", remote->name);\n \t\tstrbuf_addf(&b_remote_head, \"refs/remotes/%s/%s\", remote->name, head_name);\n \t}\n-\t\t/* make sure it's valid */\n+\t/* make sure it's valid */\n \tif (!baremirror && !refs_ref_exists(refs, b_remote_head.buf)) {\n \t\tresult = 1;\n \t\tgoto cleanup;\n-- \n2.54.0\n\n"}]}