threads / rfc / 62096

RFC v2, 2 partsset remote/HEAD with fetch

Subject: [RFC PATCH v2 0/2] set remote/HEAD with fetch

## tl;dr

258 messages between Sep 10, 2024 and Jan 6, 2025. Diffs are folded; open one to read it.

replies: 257people: 11as markdown or json

Bence Ferdinandy· Sep 10, 2024, 20:37 UTC · lore

Sorry for the noise, I managed to badly mess up receipients in the previous thread so v2 is just fixing that ...

Hi,

these two patches attempt to solve the issue raised in https://lore.kernel.org/git/D3HBD7C1FR14.74FL1Q1S9UCB@ferdinandy.com/

As a quick summary: `clone` sets `refs/remotes/[remote]/HEAD` while going init -> remote add -> fetch does not, one has to manually run `remote set-head -a [remote]`.

The first patch adds a `--set-head` flag to `fetch` and `remote update` which runs `remote set-head -a` for us. Unfortunately, with the current behaviour of set-head this will always print a message, even though a no-op fetch doesn't print anything, and I think this is also confusing for people who do not care about remote/HEAD, so the second patch removes the print if `set-head -a` is a no-op (and actually makes it into a no-op, instead of just idempotent).

Another way could of course be duplicating some of the code from remote set-head in fetch.c instead of calling directly, but it didn't look like an anti-pattern in the code-base and it felt the best way to insure identical behaviour between a `git fetch --all --set-head` and a `git fetch --all && git remote | xargs -i git remote set-head -a {}`.

What is missing for sure is:
- documentation
- tests (if needed)
- settings
For settings, my idea would be a fetch/remote.set_head that could take three values:
    * never
    * missing: run it only if the ref is missing, this setting would basically
      allow replicating the result of a clone
    * always (with the other patch, this would still be a no-op if it didn't change)

This would probably also require a --no-set-head flag, to disable an always/missing setting. A --missing-set-head or something of the like also may or may not make sense. Alternatively, only two behaviours might be enough (missing and always) since clone already sort of does this.

I'm not sure if the general approach is fine or not, nor am I sure the code itself is any good, but it "works on my computer" :) I'm also hoping that I managed to read all the relevant parts for sending a patch.

Feedback would be highly appreciated!

Thanks, Bence

Bence Ferdinandy (2):
  fetch: set-head with --set-head option
  set-head: do not update if there is no change
 builtin/fetch.c  | 29 ++++++++++++++++++++++++-----
 builtin/remote.c | 15 +++++++++++----
 2 files changed, 35 insertions(+), 9 deletions(-)
-- 
2.46.0
Bence Ferdinandy· Sep 10, 2024, 20:37 UTC · re: Bence Ferdinandy · lore

[RFC PATCH v2 1/2] fetch: set-head with --set-head option

When cloning a repository refs/remotes/origin/HEAD is set automatically. In contrast, when using init, remote add and fetch to set a remote, one needs to call remote set-head --auto to achieve the same result.

Add a --set-head option to git fetch to automatically set heads on remotes.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
 builtin/fetch.c  | 29 ++++++++++++++++++++++++-----
 builtin/remote.c |  5 +++++
 2 files changed, 29 insertions(+), 5 deletions(-)
Show changes to 2 files +29 −5

builtin/fetch.c, builtin/remote.c

diff --git a/builtin/fetch.c b/builtin/fetch.c
index b2b5aee5bf..6392314c6a 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1961,8 +1961,19 @@ static int fetch_finished(int result, struct strbuf *out,
 	return 0;
 }
 
-static int fetch_multiple(struct string_list *list, int max_children,
-			  const struct fetch_config *config)
+static int run_set_head(const char *name)
+{
+	struct child_process cmd = CHILD_PROCESS_INIT;
+	strvec_push(&cmd.args, "remote");
+	strvec_push(&cmd.args, "set-head");
+	strvec_push(&cmd.args, "--auto");
+	strvec_push(&cmd.args, name);
+	cmd.git_cmd = 1;
+	return run_command(&cmd);
+}
+
+static int fetch_multiple(struct string_list *list, int max_children, int set_head,
+			const struct fetch_config *config)
 {
 	int i, result = 0;
 	struct strvec argv = STRVEC_INIT;
@@ -2014,6 +2025,8 @@ static int fetch_multiple(struct string_list *list, int max_children,
 				error(_("could not fetch %s"), name);
 				result = 1;
 			}
+			if (set_head && run_set_head(name))
+				result = 1;
 		}
 
 	strvec_clear(&argv);
@@ -2062,7 +2075,7 @@ static inline void fetch_one_setup_partial(struct remote *remote)
 }
 
 static int fetch_one(struct remote *remote, int argc, const char **argv,
-		     int prune_tags_ok, int use_stdin_refspecs,
+		     int prune_tags_ok, int set_head, int use_stdin_refspecs,
 		     const struct fetch_config *config)
 {
 	struct refspec rs = REFSPEC_INIT_FETCH;
@@ -2135,9 +2148,12 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,
 	refspec_clear(&rs);
 	transport_disconnect(gtransport);
 	gtransport = NULL;
+	if (set_head && run_set_head(remote -> name))
+		exit_code = 1;
 	return exit_code;
 }
 
+
 int cmd_fetch(int argc, const char **argv, const char *prefix)
 {
 	struct fetch_config config = {
@@ -2154,6 +2170,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)
 	struct string_list list = STRING_LIST_INIT_DUP;
 	struct remote *remote = NULL;
 	int all = -1, multiple = 0;
+	int set_head = 0;
 	int result = 0;
 	int prune_tags_ok = 1;
 	int enable_auto_gc = 1;
@@ -2171,6 +2188,8 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)
 		OPT__VERBOSITY(&verbosity),
 		OPT_BOOL(0, "all", &all,
 			 N_("fetch from all remotes")),
+		OPT_BOOL(0, "set-head", &set_head,
+			 N_("auto set remote HEAD")),
 		OPT_BOOL(0, "set-upstream", &set_upstream,
 			 N_("set upstream for git pull/fetch")),
 		OPT_BOOL('a', "append", &append,
@@ -2436,7 +2455,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)
 			trace2_region_leave("fetch", "setup-partial", the_repository);
 		}
 		trace2_region_enter("fetch", "fetch-one", the_repository);
-		result = fetch_one(remote, argc, argv, prune_tags_ok, stdin_refspecs,
+		result = fetch_one(remote, argc, argv, prune_tags_ok, set_head, stdin_refspecs,
 				   &config);
 		trace2_region_leave("fetch", "fetch-one", the_repository);
 	} else {
@@ -2459,7 +2478,7 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)
 
 		/* TODO should this also die if we have a previous partial-clone? */
 		trace2_region_enter("fetch", "fetch-multiple", the_repository);
-		result = fetch_multiple(&list, max_children, &config);
+		result = fetch_multiple(&list, max_children, set_head, &config);
 		trace2_region_leave("fetch", "fetch-multiple", the_repository);
 	}
 
diff --git a/builtin/remote.c b/builtin/remote.c
index 0acc547d69..35c54dd103 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1536,9 +1536,12 @@ static int get_remote_default(const char *key, const char *value UNUSED,
 static int update(int argc, const char **argv, const char *prefix)
 {
 	int i, prune = -1;
+	int set_head = 0;
 	struct option options[] = {
 		OPT_BOOL('p', "prune", &prune,
 			 N_("prune remotes after fetching")),
+		OPT_BOOL(0, "set-head", &set_head,
+			 N_("auto set remote HEAD")),
 		OPT_END()
 	};
 	struct child_process cmd = CHILD_PROCESS_INIT;
@@ -1552,6 +1555,8 @@ static int update(int argc, const char **argv, const char *prefix)
 
 	if (prune != -1)
 		strvec_push(&cmd.args, prune ? "--prune" : "--no-prune");
+	if (set_head)
+		strvec_push(&cmd.args, "--set-head");
 	if (verbose)
 		strvec_push(&cmd.args, "-v");
 	strvec_push(&cmd.args, "--multiple");
-- 
2.46.0
Junio C Hamano· Sep 10, 2024, 22:19 UTC · re: Bence Ferdinandy · lore

Re: [RFC PATCH v2 1/2] fetch: set-head with --set-head option

Bence Ferdinandy <bence@ferdinandy.com> writes:
> When cloning a repository refs/remotes/origin/HEAD is set automatically.
> In contrast, when using init, remote add and fetch to set a remote, one
> needs to call remote set-head --auto to achieve the same result.

In other words, "clone" is roughly equivalent to "init && remote add && fetch && remote set-head".

I do not see a problem with it. You'd do such a repository creation task only once per repository anyway.

> Add a --set-head option to git fetch to automatically set heads on
> remotes.

This practically would help only those who want to blindly follow what the remote considers their primary branch. If the remote flip-flops that every day betweein 'master' and 'main', you'd want to either "fetch && remote set-head" or "fetch --set-head", but it would be weird to flip-flop the HEAD every day to begin with.

And the feature does not help those who want to inspect and then switch. When they say "--set-head", they will unconditionally switch whatever the remote uses and there is no opportunity for them to check.

Ideally, because every "git fetch" you do will automatically learn what their HEAD of the day points at, even without "--set-head", it may be nice to let the user know when their HEAD changed, so that the user can inspect the situation and decide.

If "fetch $repo", when it notices that refs/remotes/$repo/HEAD is missing, always unconditionally stores where their HEAD points at in refs/remotes/$repo/HEAD, and did nothing else, wouldn't that be sufficient?

The users have "remote set-head" to do this when needed. What is the true motivation that "fetch" (which presumably happens a lot more often) needs to be involved in this process? The *only* upside I can see with "fetch --set-head" to blindly follow every switch of HEAD on the remote end is that you can keep up with the remote that flips its HEAD very often, but is that really a realistic need? If we reject such a use case as invalid, I suspect that the end-user experience would be simplified quite a lot. Imagine that we teach "git fetch $repo" to notice refs/remotes/$repo/HEAD is missing, and create it from the remote HEAD information automatically. And we do NOTHING ELSE. What would the end-user experience look like?

 * Usually, you start with "git clone" and 'origin' will know which
   branch 'origin/HEAD' points at.
 * You may run "git remote add -f $repo $URL" to add one.  Because
   this runs "git fetch $repo", the previous addition to the "git
   fetch" will make sure refs/remotes/$repo/HEAD would be there.
 * You may run "git remote add $repo $URL" to add one, and then
   separately "git fetch $repo" yourself.  The end result would be
   the same; refs/remotes/$repo/HEAD will be there.
Having said all that, the implementation here ...
Show 10 quoted lines
> +static int run_set_head(const char *name)
> +{
> +	struct child_process cmd = CHILD_PROCESS_INIT;
> +	strvec_push(&cmd.args, "remote");
> +	strvec_push(&cmd.args, "set-head");
> +	strvec_push(&cmd.args, "--auto");
> +	strvec_push(&cmd.args, name);
> +	cmd.git_cmd = 1;
> +	return run_command(&cmd);
> +}
... does look quite straight-forward.
Show 11 quoted lines
> +static int fetch_multiple(struct string_list *list, int max_children, int set_head,
> +			const struct fetch_config *config)
>  {
>  	int i, result = 0;
>  	struct strvec argv = STRVEC_INIT;
> @@ -2014,6 +2025,8 @@ static int fetch_multiple(struct string_list *list, int max_children,
>  				error(_("could not fetch %s"), name);
>  				result = 1;
>  			}
> +			if (set_head && run_set_head(name))
> +				result = 1;

This looked a bit questionable---I expected that we'd run the subfetches with "--set-head" option, but this makes us do the set-head step ourselves after the subfetches are done. What are pros-and-cons considered to reach the decision to do it this way?

Thanks.
Bence Ferdinandy· Sep 11, 2024, 12:13 UTC · re: Junio C Hamano · lore

Re: [RFC PATCH v2 1/2] fetch: set-head with --set-head option

Hey Junio,
thanks for the very speedy feedback!
On Wed Sep 11, 2024 at 00:19, Junio C Hamano <gitster@pobox.com> wrote:
[snip]
Show 5 quoted lines
>
> Ideally, because every "git fetch" you do will automatically learn
> what their HEAD of the day points at, even without "--set-head", it
> may be nice to let the user know when their HEAD changed, so that
> the user can inspect the situation and decide.

That would actually make sense, we could print a message saying HEAD has changed and I guess helpfully print the exact set-head command they would need to manually update should they wish to do so.

Show 5 quoted lines
>
> If "fetch $repo", when it notices that refs/remotes/$repo/HEAD is
> missing, always unconditionally stores where their HEAD points at
> in refs/remotes/$repo/HEAD, and did nothing else, wouldn't that be
> sufficient?
Show 23 quoted lines
>
> The users have "remote set-head" to do this when needed.  What is
> the true motivation that "fetch" (which presumably happens a lot
> more often) needs to be involved in this process?  The *only* upside
> I can see with "fetch --set-head" to blindly follow every switch of
> HEAD on the remote end is that you can keep up with the remote that
> flips its HEAD very often, but is that really a realistic need?  If
> we reject such a use case as invalid, I suspect that the end-user
> experience would be simplified quite a lot.  Imagine that we teach
> "git fetch $repo" to notice refs/remotes/$repo/HEAD is missing, and
> create it from the remote HEAD information automatically.  And we do
> NOTHING ELSE.  What would the end-user experience look like?
>
>  * Usually, you start with "git clone" and 'origin' will know which
>    branch 'origin/HEAD' points at.
>
>  * You may run "git remote add -f $repo $URL" to add one.  Because
>    this runs "git fetch $repo", the previous addition to the "git
>    fetch" will make sure refs/remotes/$repo/HEAD would be there.
>
>  * You may run "git remote add $repo $URL" to add one, and then
>    separately "git fetch $repo" yourself.  The end result would be
>    the same; refs/remotes/$repo/HEAD will be there.

I'm convinced :) It also does drop a lot of complexity. So there will be no extra flag for fetch, rather:

- if the remote HEAD does not exist, we create it
- if it does exist, but has changed we print a message saying it was changed from X to Y and print the required command to update to this
- no configuration needed

The only place I can imagine this not being sufficient is in a CI/CD process that is fetching into a cached repo needs to be updated, but then those people can just always run remote set-head -a.

Show 15 quoted lines
>
> Having said all that, the implementation here ...
>
> > +static int run_set_head(const char *name)
> > +{
> > +	struct child_process cmd = CHILD_PROCESS_INIT;
> > +	strvec_push(&cmd.args, "remote");
> > +	strvec_push(&cmd.args, "set-head");
> > +	strvec_push(&cmd.args, "--auto");
> > +	strvec_push(&cmd.args, name);
> > +	cmd.git_cmd = 1;
> > +	return run_command(&cmd);
> > +}
>
> ... does look quite straight-forward.

Unfortunately, as Jeff has pointed out in the other thread, this implementation requires the user to authenticate twice ... Now, we could still rely on set-head to actually do the writing, since if you explicitly give it what to set to it will not do a network query, but considering all of the above, I think it would make more sense not to do this, rather just bring in the required logic here. This would also allow for the second patch to be a bit more explicit on what did or did not happen during a remote set-head.

Show 17 quoted lines
>
> > +static int fetch_multiple(struct string_list *list, int max_children, int set_head,
> > +			const struct fetch_config *config)
> >  {
> >  	int i, result = 0;
> >  	struct strvec argv = STRVEC_INIT;
> > @@ -2014,6 +2025,8 @@ static int fetch_multiple(struct string_list *list, int max_children,
> >  				error(_("could not fetch %s"), name);
> >  				result = 1;
> >  			}
> > +			if (set_head && run_set_head(name))
> > +				result = 1;
>
> This looked a bit questionable---I expected that we'd run the
> subfetches with "--set-head" option, but this makes us do the
> set-head step ourselves after the subfetches are done.  What are
> pros-and-cons considered to reach the decision to do it this way?

Just noobish oversight, I figured out how to do it in the subfetch, but with the above there will be no need for this anyway I guess.

I'll send a v2 soonish.

Best, Bence

Junio C Hamano· Sep 11, 2024, 15:52 UTC · re: Bence Ferdinandy · lore

Re: [RFC PATCH v2 1/2] fetch: set-head with --set-head option

"Bence Ferdinandy" <bence@ferdinandy.com> writes:
Show 8 quoted lines
>> Ideally, because every "git fetch" you do will automatically learn
>> what their HEAD of the day points at, even without "--set-head", it
>> may be nice to let the user know when their HEAD changed, so that
>> the user can inspect the situation and decide.
>
> That would actually make sense, we could print a message saying HEAD has
> changed and I guess helpfully print the exact set-head command they would need
> to manually update should they wish to do so.

We need to be careful if we were to do this, though. A user may see that their HEAD points at their 'main' branch, and they know the remote-tracking HEAD points at 'next' branch. But the only thing the latter tells us is that 'next' is, as far as this user is concerned, the primary branch they are interested in from this remote. It may have been set earlier with "git remtoe set-head" explicitly, or it may have been set back when "git clone" created this repository and back then the remote had 'next' marked as its primary branch. In other words, the HEAD we learned from remote while we are fetching from it may or may not be the same from the remote-tracking HEAD we have, but it does not mean that the remote recently flipped its HEAD if these two are different, but what we want to report is "their HEAD used to point at 'master' but now it is pointing at 'main'".

Show 8 quoted lines
> So there will be no
> extra flag for fetch, rather:
>
> - if the remote HEAD does not exist, we create it
>
> - if it does exist, but has changed we print a message saying it was changed from X to Y and print the required command to update to this
>
> - no configuration needed
Very good, with two reservations.
 - if the remote HEAD does not exist, we may still not want to
   create it.  Imagine
	[remote "his"]
		url = https://git.kernel.org/pub/scm/git/git
		push = refs/heads/maint
		push = refs/heads/master
		push = refs/heads/next
		push = +refs/heads/seen
   without any refspec for the fetching side.  "git fetch his master"
   may learn where the remote HEAD is, and it may even be pointing
   at their 'master' branch, but because we do not maintain any
   remote tracking information for their 'master' (in other words,
   refs/remotes/his/master is not updated by this 'fetch' and there
   is no configuration to make future 'fetch' to do so).
 - it would be very nice to report when we notice that they changed
   their HEAD, but I do not think we have enough information to do
   so.  Our existing refs/remotes/origin/HEAD may not have been set
   to match their HEAD in the first place.
Thanks.
Bence Ferdinandy· Sep 19, 2024, 12:13 UTC · re: Bence Ferdinandy · lore

[PATCH v3 0/2] fetch: set remote/HEAD if missing

This series is a complete rewrite of the RFC without using or touching the code of `git remote set-head` (that part of the RFC is being handled in a new thread of it's own).

Based on the previous discussions it does an atomic update of remote/HEAD if it doesn't exist and is able to. Since we have git remote set-head for manual handling of remote/HEAD, my idea was that it only outputs minimal information and in general send the user to use remote set-head for figuring out issues. If there is a simple, clear HEAD and is changed it will give the command to use for switching to this and if something went wrong it tells the user to check with remote set-head. I'm not sure if this is really needed to be honest.

A note on tests:
- the first patch changing refs_update_symref passes all tests
- the series in itself breaks many fetch tests currently, most of them
  seem to stem from the fact that we now have remote/HEAD refs as extra

I'm planning to add a test to fetch for testing not to overwrite existing refs, and of course I'll go and clean up all the tests, but it would be nice to know beforehand if the approach is good and to finalize all the outputs before changing 30 something tests.

Bence Ferdinandy (2):
  update_symref: add REF_CREATE_ONLY option
  fetch: set remote/HEAD if it does not exist
 builtin/branch.c          |  2 +-
 builtin/checkout.c        |  4 +--
 builtin/clone.c           |  6 ++--
 builtin/fetch.c           | 71 +++++++++++++++++++++++++++++++++++++++
 builtin/notes.c           |  2 +-
 builtin/remote.c          |  6 ++--
 builtin/symbolic-ref.c    |  2 +-
 builtin/worktree.c        |  2 +-
 refs.c                    |  6 ++--
 refs.h                    | 12 +++++--
 refs/files-backend.c      |  8 +++++
 reset.c                   |  2 +-
 sequencer.c               |  2 +-
 setup.c                   |  2 +-
 t/helper/test-ref-store.c |  2 +-
 15 files changed, 108 insertions(+), 21 deletions(-)
-- 
2.46.1.546.gaa0cd55cd3.dirty
Bence Ferdinandy· Sep 19, 2024, 12:13 UTC · re: Bence Ferdinandy · lore

[PATCH v3 2/2] fetch: set remote/HEAD if it does not exist

If the user has remote/HEAD set already and it looks like it has changed on the server, then print a message, otherwise set it if we can.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v3: - does not rely on remote set-head anymore so it only authenticates
        once
        - uses the new REF_CREATE_ONLY to atomically check if the ref exists
          and only write it if it doesn't
        - in all other cases the maximum it does is print a warning
 builtin/fetch.c | 71 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 71 insertions(+)
Show changes to builtin/fetch.c +71 −0
diff --git a/builtin/fetch.c b/builtin/fetch.c
index 55f97134aa..2746d40bb5 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1577,6 +1577,70 @@ static int backfill_tags(struct display_state *display_state,
 	return retcode;
 }
 
+static const char *abbrev_ref(const char *name, const char *prefix)
+{
+	skip_prefix(name, prefix, &name);
+	return name;
+}
+#define abbrev_branch(name) abbrev_ref((name), "refs/heads/")
+
+static inline int set_head(const struct ref *remote_refs)
+{
+	int result, ref_changed = 0;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT, b_local_head = STRBUF_INIT, b_prefix = STRBUF_INIT;
+	const char *remote = gtransport->remote->name;
+	char * head_name = NULL;
+	struct ref *ref, *matches;
+	struct ref *fetch_map = NULL, **fetch_map_tail = &fetch_map;
+	struct refspec_item refspec = {
+		.force = 0,
+		.pattern = 1,
+		.src = (char *) "refs/heads/*",
+		.dst = (char *) "refs/heads/*",
+	};
+	struct string_list heads = STRING_LIST_INIT_DUP;
+
+	get_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);
+	matches = guess_remote_head(find_ref_by_name(remote_refs, "HEAD"),
+				    fetch_map, 1);
+	for (ref = matches; ref; ref = ref->next)
+		string_list_append(&heads, abbrev_branch(ref->name));
+
+
+	if (!heads.nr)
+		result = 1;
+	else if (heads.nr > 1) {
+		result = 1;
+	} else
+		head_name = xstrdup(heads.items[0].string);
+	if (head_name) {
+		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
+		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote, head_name);
+		strbuf_addf(&b_prefix, "refs/remotes/%s/", remote);
+		if (!refs_read_symbolic_ref(get_main_ref_store(the_repository),b_head.buf,&b_local_head)) {
+			ref_changed = strcmp(b_remote_head.buf,b_local_head.buf);
+			if (heads.nr == 1 && ref_changed) {
+				printf("The ref \'%s/HEAD\' has changed from the locally recorded "
+					"\'%s\' to \'%s\'.\n",remote, abbrev_ref(b_local_head.buf,b_prefix.buf), head_name);
+				printf("Run \'git remote set-head -a %s\' to set it automatically.\n", remote);
+			}
+		}
+		/* make sure it's valid */
+		if (!refs_ref_exists(get_main_ref_store(the_repository), b_remote_head.buf))
+			result = 1;
+		else if (refs_update_symref(get_main_ref_store(the_repository), b_head.buf,
+						b_remote_head.buf, REF_CREATE_ONLY, "fetch"))
+			result = 1;
+		free(head_name);
+	}
+
+	strbuf_release(&b_head);
+	strbuf_release(&b_local_head);
+	strbuf_release(&b_remote_head);
+	strbuf_release(&b_prefix);
+	return result;
+}
+
 static int do_fetch(struct transport *transport,
 		    struct refspec *rs,
 		    const struct fetch_config *config)
@@ -1646,6 +1710,8 @@ static int do_fetch(struct transport *transport,
 				    "refs/tags/");
 	}
 
+	strvec_push(&transport_ls_refs_options.ref_prefixes,"HEAD");
+
 	if (must_list_refs) {
 		trace2_region_enter("fetch", "remote_refs", the_repository);
 		remote_refs = transport_get_remote_refs(transport,
@@ -1790,6 +1856,10 @@ static int do_fetch(struct transport *transport,
 				  "you need to specify exactly one branch with the --set-upstream option"));
 		}
 	}
+	if (!set_head(remote_refs))
+		printf("Ran into issues with \'%s/HEAD\',\n"
+			"use \'git remote set-head -a %s\' to investigate",
+			gtransport->remote->name,gtransport->remote->name);
 
 cleanup:
 	if (retcode) {
@@ -2020,6 +2090,7 @@ static int fetch_multiple(struct string_list *list, int max_children,
 	return !!result;
 }
 
+
 /*
  * Fetching from the promisor remote should use the given filter-spec
  * or inherit the default filter-spec from the config.
-- 
2.46.1.546.gaa0cd55cd3.dirty
Junio C Hamano· Sep 19, 2024, 23:07 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v3 2/2] fetch: set remote/HEAD if it does not exist

Bence Ferdinandy <bence@ferdinandy.com> writes:
Show 12 quoted lines
> If the user has remote/HEAD set already and it looks like it has changed
> on the server, then print a message, otherwise set it if we can.
>
> Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
> ---
>
> Notes:
>     v3: - does not rely on remote set-head anymore so it only authenticates
>         once
>         - uses the new REF_CREATE_ONLY to atomically check if the ref exists
>           and only write it if it doesn't
>         - in all other cases the maximum it does is print a warning

Except for one thing, I can tell from the above that the design is sensible without looking at the actual patch. Nicely described.

The "one thing" is what to do if you normally do *not* keep track of the remote-tracking refs for this particular remote.

If I am only fetching refs (or HEAD) in FETCH_HEAD for immediate consumtion by doing "git pull bence HEAD" with something like

    [remote "bence"]
	 URL = http://github.com/bence/git

(with no other configuration under remote.bence.* hierarchy), I do not think I want the code to create refs/remotes/bence/HEAD, with no other remote-tracking refs in the same hierarchy, even if it finds no existing refs/remotes/bence/HEAD there. For that, I suspect that you'd need to see if the pointee of refs/remotes/bence/HEAD either already exists or this fetch is about to create it, or something like that, before deciding to create a ref with the REF_CREATE_ONLY flag.

If you are doing so already in the code (I haven't started reading it yet at this moment), you should mention it in your proposed log message, so discount my earlier "Nicely described" a bit ;-).

Let's see what actually happens in the code now.
Show 6 quoted lines
> +static const char *abbrev_ref(const char *name, const char *prefix)
> +{
> +	skip_prefix(name, prefix, &name);
> +	return name;
> +}
> +#define abbrev_branch(name) abbrev_ref((name), "refs/heads/")

We do not call the act of optionally skipping prefix "abbreviate" in this project (object names are abbreviated but that is done by chomping the later bytes of a long name).

I suspect that a caller of either of these two functions are inherently buggy in that they _optionally_ skip the prefix so they do the same thing for "refs/heads/main" and "main" but not "heads/main". The callsites may need to be inspected to see how they should deal with cases where skip_prefix() did *not* see anything to skip.

> +static inline int set_head(const struct ref *remote_refs)

Drop "inline". You do not want to inline this much code if there are many callers, and if you have only one caller, the compiler would inline it into the sole caller if it is the more efficient thing to do.

Show 33 quoted lines
> +{
> +	int result, ref_changed = 0;
> +	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT, b_local_head = STRBUF_INIT, b_prefix = STRBUF_INIT;
> +	const char *remote = gtransport->remote->name;
> +	char * head_name = NULL;
> +	struct ref *ref, *matches;
> +	struct ref *fetch_map = NULL, **fetch_map_tail = &fetch_map;
> +	struct refspec_item refspec = {
> +		.force = 0,
> +		.pattern = 1,
> +		.src = (char *) "refs/heads/*",
> +		.dst = (char *) "refs/heads/*",
> +	};
> +	struct string_list heads = STRING_LIST_INIT_DUP;
> +
> +	get_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);
> +	matches = guess_remote_head(find_ref_by_name(remote_refs, "HEAD"),
> +				    fetch_map, 1);
> +	for (ref = matches; ref; ref = ref->next)
> +		string_list_append(&heads, abbrev_branch(ref->name));
> +
> +
> +	if (!heads.nr)
> +		result = 1;
> +	else if (heads.nr > 1) {
> +		result = 1;
> +	} else
> +		head_name = xstrdup(heads.items[0].string);
> +	if (head_name) {
> +		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
> +		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote, head_name);
> +		strbuf_addf(&b_prefix, "refs/remotes/%s/", remote);
> +		if (!refs_read_symbolic_ref(get_main_ref_store(the_repository),b_head.buf,&b_local_head)) {

Overly long lines around here cannot be sanely reviewed on my terminal, so I'll stop here for now.

Do not omit SP after ",", as that is the least effective way to shorten overly long lines. Instead, consider introducing a one-time-use temporary variables with meaningful name, e.g.

		struct ref_store *store = get_main_ref_store(the_repository);
                
                strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
		...
		if (!refs_read_symbolic_ref(store, b_head.buf, &current)) {
    			/* it's current value is in "current" */
			...
Bence Ferdinandy· Sep 20, 2024, 13:50 UTC · re: Junio C Hamano · lore

Re: [PATCH v3 2/2] fetch: set remote/HEAD if it does not exist

On Fri Sep 20, 2024 at 01:07, Junio C Hamano <gitster@pobox.com> wrote:
Show 35 quoted lines
> Bence Ferdinandy <bence@ferdinandy.com> writes:
>
> > If the user has remote/HEAD set already and it looks like it has changed
> > on the server, then print a message, otherwise set it if we can.
> >
> > Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
> > ---
> >
> > Notes:
> >     v3: - does not rely on remote set-head anymore so it only authenticates
> >         once
> >         - uses the new REF_CREATE_ONLY to atomically check if the ref exists
> >           and only write it if it doesn't
> >         - in all other cases the maximum it does is print a warning
>
> Except for one thing, I can tell from the above that the design is
> sensible without looking at the actual patch.  Nicely described.
>
> The "one thing" is what to do if you normally do *not* keep track of
> the remote-tracking refs for this particular remote.
>
> If I am only fetching refs (or HEAD) in FETCH_HEAD for immediate
> consumtion by doing "git pull bence HEAD" with something like
>
>     [remote "bence"]
> 	 URL = http://github.com/bence/git
>
> (with no other configuration under remote.bence.* hierarchy), I do
> not think I want the code to create refs/remotes/bence/HEAD, with
> no other remote-tracking refs in the same hierarchy, even if it finds
> no existing refs/remotes/bence/HEAD there.  For that, I suspect that 
> you'd need to see if the pointee of refs/remotes/bence/HEAD either
> already exists or this fetch is about to create it, or something
> like that, before deciding to create a ref with the REF_CREATE_ONLY
> flag.
I'll take another look at this.
>
> If you are doing so already in the code (I haven't started reading
> it yet at this moment), you should mention it in your proposed log
> message, so discount my earlier "Nicely described" a bit ;-).
:D
Show 20 quoted lines
>
> Let's see what actually happens in the code now.
>
> > +static const char *abbrev_ref(const char *name, const char *prefix)
> > +{
> > +	skip_prefix(name, prefix, &name);
> > +	return name;
> > +}
> > +#define abbrev_branch(name) abbrev_ref((name), "refs/heads/")
>
> We do not call the act of optionally skipping prefix "abbreviate" in
> this project (object names are abbreviated but that is done by
> chomping the later bytes of a long name).
>
> I suspect that a caller of either of these two functions are
> inherently buggy in that they _optionally_ skip the prefix so they
> do the same thing for "refs/heads/main" and "main" but not
> "heads/main".  The callsites may need to be inspected to see how
> they should deal with cases where skip_prefix() did *not* see
> anything to skip.

I copied this verbatim from builtin/remote.c https://github.com/git/git/blob/master/builtin/remote.c#L267 so it looked like a safe bet T_T I'm not 100% sure, but I think there's no use case where you pass a ref directly to remote except for set_head, but there it's not used, so it looks safe?

That being said, I can change the name(s) to not use abbrev here, especially if you have a good suggestion. Maybe strip_prefix_ref/branch? Again in fetch, we (should) know what type of refs we're calling it on, so should be safe?

Show 7 quoted lines
>
> > +static inline int set_head(const struct ref *remote_refs)
>
> Drop "inline".  You do not want to inline this much code if there
> are many callers, and if you have only one caller, the compiler
> would inline it into the sole caller if it is the more efficient
> thing to do.
Ack.
Show 49 quoted lines
>
> > +{
> > +	int result, ref_changed = 0;
> > +	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT, b_local_head = STRBUF_INIT, b_prefix = STRBUF_INIT;
> > +	const char *remote = gtransport->remote->name;
> > +	char * head_name = NULL;
> > +	struct ref *ref, *matches;
> > +	struct ref *fetch_map = NULL, **fetch_map_tail = &fetch_map;
> > +	struct refspec_item refspec = {
> > +		.force = 0,
> > +		.pattern = 1,
> > +		.src = (char *) "refs/heads/*",
> > +		.dst = (char *) "refs/heads/*",
> > +	};
> > +	struct string_list heads = STRING_LIST_INIT_DUP;
> > +
> > +	get_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);
> > +	matches = guess_remote_head(find_ref_by_name(remote_refs, "HEAD"),
> > +				    fetch_map, 1);
> > +	for (ref = matches; ref; ref = ref->next)
> > +		string_list_append(&heads, abbrev_branch(ref->name));
> > +
> > +
> > +	if (!heads.nr)
> > +		result = 1;
> > +	else if (heads.nr > 1) {
> > +		result = 1;
> > +	} else
> > +		head_name = xstrdup(heads.items[0].string);
> > +	if (head_name) {
> > +		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
> > +		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote, head_name);
> > +		strbuf_addf(&b_prefix, "refs/remotes/%s/", remote);
> > +		if (!refs_read_symbolic_ref(get_main_ref_store(the_repository),b_head.buf,&b_local_head)) {
>
> Overly long lines around here cannot be sanely reviewed on my
> terminal, so I'll stop here for now.  
>
> Do not omit SP after ",", as that is the least effective way to
> shorten overly long lines.  Instead, consider introducing a
> one-time-use temporary variables with meaningful name, e.g.
>
> 		struct ref_store *store = get_main_ref_store(the_repository);
>                 
>                 strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
> 		...
> 		if (!refs_read_symbolic_ref(store, b_head.buf, &current)) {
>     			/* it's current value is in "current" */
> 			...

Ah, yeah terribly sorry about this, I've gotten so used to automatic formatters, that I just didn't take a second look. I'll be more careful in the next round.

-- 
bence.ferdinandy.com
Bence Ferdinandy· Sep 19, 2024, 12:13 UTC · re: Bence Ferdinandy · lore

[PATCH v3 1/2] update_symref: add REF_CREATE_ONLY option

Add a new REF_CREATE_ONLY flag for use by the files backend which will only update the symref if it doesn't already exist. Add the possibility to pass extra flags to refs_update_symref so that it can utilize this new flag.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v3: new patch, passes all tests on it's own
 builtin/branch.c          |  2 +-
 builtin/checkout.c        |  4 ++--
 builtin/clone.c           |  6 +++---
 builtin/notes.c           |  2 +-
 builtin/remote.c          |  6 +++---
 builtin/symbolic-ref.c    |  2 +-
 builtin/worktree.c        |  2 +-
 refs.c                    |  6 +++---
 refs.h                    | 12 ++++++++++--
 refs/files-backend.c      |  8 ++++++++
 reset.c                   |  2 +-
 sequencer.c               |  2 +-
 setup.c                   |  2 +-
 t/helper/test-ref-store.c |  2 +-
 14 files changed, 37 insertions(+), 21 deletions(-)
Show changes to 14 files +37 −21

builtin/branch.c, builtin/checkout.c, builtin/clone.c, builtin/notes.c, builtin/remote.c, builtin/symbolic-ref.c, builtin/worktree.c, refs.c, refs.h, refs/files-backend.c, reset.c, sequencer.c, setup.c, t/helper/test-ref-store.c

diff --git a/builtin/branch.c b/builtin/branch.c
index c98601c6fe..6025bca45e 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -559,7 +559,7 @@ static int replace_each_worktree_head_symref(struct worktree **worktrees,
 			continue;
 
 		refs = get_worktree_ref_store(worktrees[i]);
-		if (refs_update_symref(refs, "HEAD", newref, logmsg))
+		if (refs_update_symref(refs, "HEAD", newref, 0, logmsg))
 			ret = error(_("HEAD of working tree %s is not updated"),
 				    worktrees[i]->path);
 	}
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 4cfe6fab50..23e28321d6 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -1011,7 +1011,7 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 			describe_detached_head(_("HEAD is now at"), new_branch_info->commit);
 		}
 	} else if (new_branch_info->path) {	/* Switch branches. */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path, msg.buf) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path, 0, msg.buf) < 0)
 			die(_("unable to update HEAD"));
 		if (!opts->quiet) {
 			if (old_branch_info->path && !strcmp(new_branch_info->path, old_branch_info->path)) {
@@ -1475,7 +1475,7 @@ static int switch_unborn_to_new_branch(const struct checkout_opts *opts)
 		die(_("You are on a branch yet to be born"));
 	strbuf_addf(&branch_ref, "refs/heads/%s", opts->new_branch);
 	status = refs_update_symref(get_main_ref_store(the_repository),
-				    "HEAD", branch_ref.buf, "checkout -b");
+				    "HEAD", branch_ref.buf, 0, "checkout -b");
 	strbuf_release(&branch_ref);
 	if (!opts->quiet)
 		fprintf(stderr, _("Switched to a new branch '%s'\n"),
diff --git a/builtin/clone.c b/builtin/clone.c
index 269b6e18a4..43b7878a79 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -659,7 +659,7 @@ static void update_remote_refs(const struct ref *refs,
 		strbuf_addstr(&head_ref, branch_top);
 		strbuf_addstr(&head_ref, "HEAD");
 		if (refs_update_symref(get_main_ref_store(the_repository), head_ref.buf,
-				       remote_head_points_at->peer_ref->name,
+				       remote_head_points_at->peer_ref->name, 0,
 				       msg) < 0)
 			die(_("unable to update %s"), head_ref.buf);
 		strbuf_release(&head_ref);
@@ -672,7 +672,7 @@ static void update_head(const struct ref *our, const struct ref *remote,
 	const char *head;
 	if (our && skip_prefix(our->name, "refs/heads/", &head)) {
 		/* Local default branch link */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name, 0, NULL) < 0)
 			die(_("unable to update HEAD"));
 		if (!option_bare) {
 			refs_update_ref(get_main_ref_store(the_repository),
@@ -701,7 +701,7 @@ static void update_head(const struct ref *our, const struct ref *remote,
 		 * Unborn head from remote; same as "our" case above except
 		 * that we have no ref to update.
 		 */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn, 0, NULL) < 0)
 			die(_("unable to update HEAD"));
 		if (!option_bare)
 			install_branch_config(0, head, remote_name, unborn);
diff --git a/builtin/notes.c b/builtin/notes.c
index 04f9dfb7fb..6b42d1139f 100644
--- a/builtin/notes.c
+++ b/builtin/notes.c
@@ -978,7 +978,7 @@ static int merge(int argc, const char **argv, const char *prefix)
 			die(_("a notes merge into %s is already in-progress at %s"),
 			    default_notes_ref(), wt->path);
 		free_worktrees(worktrees);
-		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", default_notes_ref(), NULL))
+		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", default_notes_ref(), 0, NULL))
 			die(_("failed to store link to current notes ref (%s)"),
 			    default_notes_ref());
 		fprintf(stderr, _("Automatic notes merge failed. Fix conflicts in %s "
diff --git a/builtin/remote.c b/builtin/remote.c
index 0acc547d69..d28c65599d 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -243,7 +243,7 @@ static int add(int argc, const char **argv, const char *prefix)
 		strbuf_reset(&buf2);
 		strbuf_addf(&buf2, "refs/remotes/%s/%s", name, master);
 
-		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote add"))
+		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, 0, "remote add"))
 			result = error(_("Could not setup master '%s'"), master);
 	}
 
@@ -863,7 +863,7 @@ static int mv(int argc, const char **argv, const char *prefix)
 		strbuf_reset(&buf3);
 		strbuf_addf(&buf3, "remote: renamed %s to %s",
 				item->string, buf.buf);
-		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, buf3.buf))
+		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, 0, buf3.buf))
 			die(_("creating '%s' failed"), buf.buf);
 		display_progress(progress, ++refs_renamed_nr);
 	}
@@ -1443,7 +1443,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		/* make sure it's valid */
 		if (!refs_ref_exists(get_main_ref_store(the_repository), buf2.buf))
 			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head"))
+		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, 0, "remote set-head"))
 			result |= error(_("Could not setup %s"), buf.buf);
 		else if (opt_a)
 			printf("%s/HEAD set to %s\n", argv[0], head_name);
diff --git a/builtin/symbolic-ref.c b/builtin/symbolic-ref.c
index 81abdd170f..52de3e02af 100644
--- a/builtin/symbolic-ref.c
+++ b/builtin/symbolic-ref.c
@@ -84,7 +84,7 @@ int cmd_symbolic_ref(int argc, const char **argv, const char *prefix)
 		if (check_refname_format(argv[1], REFNAME_ALLOW_ONELEVEL) < 0)
 			die("Refusing to set '%s' to invalid ref '%s'", argv[0], argv[1]);
 		ret = !!refs_update_symref(get_main_ref_store(the_repository),
-					   argv[0], argv[1], msg);
+					   argv[0], argv[1], 0, msg);
 		break;
 	default:
 		usage_with_options(git_symbolic_ref_usage, options);
diff --git a/builtin/worktree.c b/builtin/worktree.c
index 41e7f6a327..71434737e8 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -517,7 +517,7 @@ static int add_worktree(const char *path, const char *refname,
 		ret = refs_update_ref(wt_refs, NULL, "HEAD", &commit->object.oid,
 				      NULL, 0, UPDATE_REFS_MSG_ON_ERR);
 	else
-		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, NULL);
+		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, 0, NULL);
 	if (ret)
 		goto done;
 
diff --git a/refs.c b/refs.c
index ceb72d4bd7..7afe46cadc 100644
--- a/refs.c
+++ b/refs.c
@@ -2085,8 +2085,9 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
 	return peel_object(r, base, peeled) ? -1 : 0;
 }
 
+
 int refs_update_symref(struct ref_store *refs, const char *ref,
-		       const char *target, const char *logmsg)
+		       const char *target, const unsigned int extra_flags, const char *logmsg)
 {
 	struct ref_transaction *transaction;
 	struct strbuf err = STRBUF_INIT;
@@ -2095,7 +2096,7 @@ int refs_update_symref(struct ref_store *refs, const char *ref,
 	transaction = ref_store_transaction_begin(refs, &err);
 	if (!transaction ||
 	    ref_transaction_update(transaction, ref, NULL, NULL,
-				   target, NULL, REF_NO_DEREF,
+				   target, NULL, REF_NO_DEREF | extra_flags,
 				   logmsg, &err) ||
 	    ref_transaction_commit(transaction, &err)) {
 		ret = error("%s", err.buf);
@@ -2920,4 +2921,3 @@ int ref_update_expects_existing_old_ref(struct ref_update *update)
 	return (update->flags & REF_HAVE_OLD) &&
 		(!is_null_oid(&update->old_oid) || update->old_target);
 }
-
diff --git a/refs.h b/refs.h
index f8b919a138..d907451d13 100644
--- a/refs.h
+++ b/refs.h
@@ -569,7 +569,7 @@ int refs_copy_existing_ref(struct ref_store *refs, const char *oldref,
 		    const char *newref, const char *logmsg);
 
 int refs_update_symref(struct ref_store *refs, const char *refname,
-		       const char *target, const char *logmsg);
+		       const char *target, const unsigned int extra_flags, const char *logmsg);
 
 enum action_on_err {
 	UPDATE_REFS_MSG_ON_ERR,
@@ -672,13 +672,21 @@ struct ref_transaction *ref_store_transaction_begin(struct ref_store *refs,
  */
 #define REF_SKIP_CREATE_REFLOG (1 << 12)
 
+/*
+ * If the reference has already been created do not touch it.
+ */
+
+#define REF_CREATE_ONLY (1 << 13)
+
+
 /*
  * Bitmask of all of the flags that are allowed to be passed in to
  * ref_transaction_update() and friends:
  */
 #define REF_TRANSACTION_UPDATE_ALLOWED_FLAGS                                  \
 	(REF_NO_DEREF | REF_FORCE_CREATE_REFLOG | REF_SKIP_OID_VERIFICATION | \
-	 REF_SKIP_REFNAME_VERIFICATION | REF_SKIP_CREATE_REFLOG)
+	 REF_SKIP_REFNAME_VERIFICATION | REF_SKIP_CREATE_REFLOG | \
+	 REF_CREATE_ONLY)
 
 /*
  * Add a reference update to transaction. `new_oid` is the value that
diff --git a/refs/files-backend.c b/refs/files-backend.c
index c7f3f4e591..1440b69b87 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -2993,6 +2993,8 @@ static int files_transaction_finish(struct ref_store *ref_store,
 		struct ref_update *update = transaction->updates[i];
 		struct ref_lock *lock = update->backend_data;
 
+		if (update->flags & REF_CREATE_ONLY && refs_ref_exists(ref_store, update->refname))
+			continue;
 		if (update->flags & REF_NEEDS_COMMIT ||
 		    update->flags & REF_LOG_ONLY) {
 			if (parse_and_write_reflog(refs, update, lock, err)) {
@@ -3031,6 +3033,8 @@ static int files_transaction_finish(struct ref_store *ref_store,
 	 */
 	for (i = 0; i < transaction->nr; i++) {
 		struct ref_update *update = transaction->updates[i];
+		if (update->flags & REF_CREATE_ONLY && refs_ref_exists(ref_store, update->refname))
+			continue;
 		if (update->flags & REF_DELETING &&
 		    !(update->flags & REF_LOG_ONLY) &&
 		    !(update->flags & REF_IS_PRUNING)) {
@@ -3061,6 +3065,8 @@ static int files_transaction_finish(struct ref_store *ref_store,
 	for (i = 0; i < transaction->nr; i++) {
 		struct ref_update *update = transaction->updates[i];
 		struct ref_lock *lock = update->backend_data;
+		if (update->flags & REF_CREATE_ONLY && refs_ref_exists(ref_store, update->refname))
+			continue;
 
 		if (update->flags & REF_DELETING &&
 		    !(update->flags & REF_LOG_ONLY)) {
@@ -3085,6 +3091,8 @@ static int files_transaction_finish(struct ref_store *ref_store,
 
 	for (i = 0; i < transaction->nr; i++) {
 		struct ref_update *update = transaction->updates[i];
+		if (update->flags & REF_CREATE_ONLY && refs_ref_exists(ref_store, update->refname))
+			continue;
 
 		if (update->flags & REF_DELETED_RMDIR) {
 			/*
diff --git a/reset.c b/reset.c
index b22b1be792..8dce5f2133 100644
--- a/reset.c
+++ b/reset.c
@@ -75,7 +75,7 @@ static int update_refs(const struct reset_head_opts *opts,
 				      UPDATE_REFS_MSG_ON_ERR);
 		if (!ret)
 			ret = refs_update_symref(get_main_ref_store(the_repository),
-						 "HEAD", switch_to_branch,
+						 "HEAD", switch_to_branch, 0,
 						 reflog_head);
 	}
 	if (!ret && run_hook)
diff --git a/sequencer.c b/sequencer.c
index 8d01cd50ac..924a78dab8 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -5107,7 +5107,7 @@ static int pick_commits(struct repository *r,
 			}
 			msg = reflog_message(opts, "finish", "returning to %s",
 				head_ref.buf);
-			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf, msg)) {
+			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf, 0, msg)) {
 				res = error(_("could not update HEAD to %s"),
 					head_ref.buf);
 				goto cleanup_head_ref;
diff --git a/setup.c b/setup.c
index 29f8673921..6a1fdef2c3 100644
--- a/setup.c
+++ b/setup.c
@@ -2169,7 +2169,7 @@ void create_reference_database(enum ref_storage_format ref_storage_format,
 			die(_("invalid initial branch name: '%s'"),
 			    initial_branch);
 
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref, 0, NULL) < 0)
 			exit(1);
 		free(ref);
 	}
diff --git a/t/helper/test-ref-store.c b/t/helper/test-ref-store.c
index 65346dee55..90af41edce 100644
--- a/t/helper/test-ref-store.c
+++ b/t/helper/test-ref-store.c
@@ -120,7 +120,7 @@ static int cmd_create_symref(struct ref_store *refs, const char **argv)
 	const char *target = notnull(*argv++, "target");
 	const char *logmsg = *argv++;
 
-	return refs_update_symref(refs, refname, target, logmsg);
+	return refs_update_symref(refs, refname, target, 0, logmsg);
 }
 
 static struct flag_definition transaction_flags[] = {
-- 
2.46.1.546.gaa0cd55cd3.dirty
Junio C Hamano· Sep 19, 2024, 22:46 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v3 1/2] update_symref: add REF_CREATE_ONLY option

Bence Ferdinandy <bence@ferdinandy.com> writes:
> Add a new REF_CREATE_ONLY flag for use by the files backend which will
> only update the symref if it doesn't already exist. Add the possibility
> to pass extra flags to refs_update_symref so that it can utilize this
> new flag.
If I wanted to create a symref that points at A, there are three cases:
 (1) the symref does not exist.  
 (2) the symref exists and points at A.
 (3) the symref exists and points at B.

I'll see a symref that points at A at the end in the first two cases, and my request is silently ignored in the third case.

I'd expect that the caller can tell the failing case apart from the successful case with the return value or something. The caller might want to tell between the first two cases for reporting purposes, but I do not care as much as I would care about detecting true failures.

Nobody actually passes the flag yet, so we would not be able to tell if any of the added code is buggy from this step alone. Let's see what happens in the next patch ;-).

Show 12 quoted lines
> diff --git a/refs.c b/refs.c
> index ceb72d4bd7..7afe46cadc 100644
> --- a/refs.c
> +++ b/refs.c
> @@ -2085,8 +2085,9 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
>  	return peel_object(r, base, peeled) ? -1 : 0;
>  }
>  
> +
>  int refs_update_symref(struct ref_store *refs, const char *ref,
> -		       const char *target, const char *logmsg)
> +		       const char *target, const unsigned int extra_flags, const char *logmsg)

While it is not _wrong_ per-se to mark an "unsigned int" parameter as "const", it is a bit unusual in this code base. The only thing it prevents us from doing is to mutate it until this function returns, which does not help all that much in making the code safer, as opposed to marking a parameter of a pointer type as a const pointer.

Bence Ferdinandy· Sep 20, 2024, 14:11 UTC · re: Junio C Hamano · lore

Re: [PATCH v3 1/2] update_symref: add REF_CREATE_ONLY option

On Fri Sep 20, 2024 at 00:46, Junio C Hamano <gitster@pobox.com> wrote:
> Bence Ferdinandy <bence@ferdinandy.com> writes:
[ snip] 
Show 14 quoted lines
> If I wanted to create a symref that points at A, there are three cases:
>
>  (1) the symref does not exist.  
>  (2) the symref exists and points at A.
>  (3) the symref exists and points at B.
>
> I'll see a symref that points at A at the end in the first two
> cases, and my request is silently ignored in the third case.
>
> I'd expect that the caller can tell the failing case apart from the
> successful case with the return value or something.  The caller
> might want to tell between the first two cases for reporting
> purposes, but I do not care as much as I would care about detecting
> true failures.

Hmm. So in case I'm passing REF_CREATE_ONLY I would not expect the above cases to be error in the sense that transaction_finish should not report a failure and thus have all callers assume things went wrong. On the other hand it's a valid concern, that the caller may want to check what happened. Actually, the idea that I mentioned in

https://lore.kernel.org/git/D4AK4USDVP5T.10INJOFE2I8LE@ferdinandy.com/  

may actually be useful here as well. We could record the state of the reference atomically before the transaction in update, and then if the caller is interested, they can match this against what they requested. That way they can figure out which of the 3 cases they were in without raceconditions after the situation. Actually, this way any feedback could be given to the user post transaction here as well.

If this sounds sensible, then I guess it would make sense to rejoin the set-head patch into this thread as well ...

[snip]
Show 20 quoted lines
>
> > diff --git a/refs.c b/refs.c
> > index ceb72d4bd7..7afe46cadc 100644
> > --- a/refs.c
> > +++ b/refs.c
> > @@ -2085,8 +2085,9 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
> >  	return peel_object(r, base, peeled) ? -1 : 0;
> >  }
> >  
> > +
> >  int refs_update_symref(struct ref_store *refs, const char *ref,
> > -		       const char *target, const char *logmsg)
> > +		       const char *target, const unsigned int extra_flags, const char *logmsg)
>
> While it is not _wrong_ per-se to mark an "unsigned int" parameter
> as "const", it is a bit unusual in this code base.  The only thing
> it prevents us from doing is to mutate it until this function
> returns, which does not help all that much in making the code safer,
> as opposed to marking a parameter of a pointer type as a const
> pointer.
Makes sense, I'll drop it.
Thanks very much for your patience!

Best, Bence

-- 
bence.ferdinandy.com
Phillip Wood· Sep 21, 2024, 13:40 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v3 1/2] update_symref: add REF_CREATE_ONLY option

On 19/09/2024 13:13, Bence Ferdinandy wrote:
> Add a new REF_CREATE_ONLY flag for use by the files backend which will
> only update the symref if it doesn't already exist. Add the possibility
> to pass extra flags to refs_update_symref so that it can utilize this
> new flag.

I'm not sure we need a new flag to do this as it is already supported by the ref transaction api.

	struct ref_transaction *t;
	struct strbuf err = STRBUF_INIT;
	struct ref_store refs = get_main_ref_store(the_repository);
	int ret = 0;
	t = ref_transaction_begin(refs, &err);
	if (!t ||
	    ref_transaction_create(t, b_head.buf, NULL, b_remote_head.buf,
				   REF_NO_DEREF, "fetch", &err) ||
	    ref_transaction_commit(t, &err))
		ret = error(_("%s", err.buf));
	ref_transaction_free(transaction);
	strbuf_release(&err);
	return ret;
Best Wishes
Phillip
Show 315 quoted lines
> Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
> ---
> 
> Notes:
>      v3: new patch, passes all tests on it's own
> 
>   builtin/branch.c          |  2 +-
>   builtin/checkout.c        |  4 ++--
>   builtin/clone.c           |  6 +++---
>   builtin/notes.c           |  2 +-
>   builtin/remote.c          |  6 +++---
>   builtin/symbolic-ref.c    |  2 +-
>   builtin/worktree.c        |  2 +-
>   refs.c                    |  6 +++---
>   refs.h                    | 12 ++++++++++--
>   refs/files-backend.c      |  8 ++++++++
>   reset.c                   |  2 +-
>   sequencer.c               |  2 +-
>   setup.c                   |  2 +-
>   t/helper/test-ref-store.c |  2 +-
>   14 files changed, 37 insertions(+), 21 deletions(-)
> 
> diff --git a/builtin/branch.c b/builtin/branch.c
> index c98601c6fe..6025bca45e 100644
> --- a/builtin/branch.c
> +++ b/builtin/branch.c
> @@ -559,7 +559,7 @@ static int replace_each_worktree_head_symref(struct worktree **worktrees,
>   			continue;
>   
>   		refs = get_worktree_ref_store(worktrees[i]);
> -		if (refs_update_symref(refs, "HEAD", newref, logmsg))
> +		if (refs_update_symref(refs, "HEAD", newref, 0, logmsg))
>   			ret = error(_("HEAD of working tree %s is not updated"),
>   				    worktrees[i]->path);
>   	}
> diff --git a/builtin/checkout.c b/builtin/checkout.c
> index 4cfe6fab50..23e28321d6 100644
> --- a/builtin/checkout.c
> +++ b/builtin/checkout.c
> @@ -1011,7 +1011,7 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
>   			describe_detached_head(_("HEAD is now at"), new_branch_info->commit);
>   		}
>   	} else if (new_branch_info->path) {	/* Switch branches. */
> -		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path, msg.buf) < 0)
> +		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path, 0, msg.buf) < 0)
>   			die(_("unable to update HEAD"));
>   		if (!opts->quiet) {
>   			if (old_branch_info->path && !strcmp(new_branch_info->path, old_branch_info->path)) {
> @@ -1475,7 +1475,7 @@ static int switch_unborn_to_new_branch(const struct checkout_opts *opts)
>   		die(_("You are on a branch yet to be born"));
>   	strbuf_addf(&branch_ref, "refs/heads/%s", opts->new_branch);
>   	status = refs_update_symref(get_main_ref_store(the_repository),
> -				    "HEAD", branch_ref.buf, "checkout -b");
> +				    "HEAD", branch_ref.buf, 0, "checkout -b");
>   	strbuf_release(&branch_ref);
>   	if (!opts->quiet)
>   		fprintf(stderr, _("Switched to a new branch '%s'\n"),
> diff --git a/builtin/clone.c b/builtin/clone.c
> index 269b6e18a4..43b7878a79 100644
> --- a/builtin/clone.c
> +++ b/builtin/clone.c
> @@ -659,7 +659,7 @@ static void update_remote_refs(const struct ref *refs,
>   		strbuf_addstr(&head_ref, branch_top);
>   		strbuf_addstr(&head_ref, "HEAD");
>   		if (refs_update_symref(get_main_ref_store(the_repository), head_ref.buf,
> -				       remote_head_points_at->peer_ref->name,
> +				       remote_head_points_at->peer_ref->name, 0,
>   				       msg) < 0)
>   			die(_("unable to update %s"), head_ref.buf);
>   		strbuf_release(&head_ref);
> @@ -672,7 +672,7 @@ static void update_head(const struct ref *our, const struct ref *remote,
>   	const char *head;
>   	if (our && skip_prefix(our->name, "refs/heads/", &head)) {
>   		/* Local default branch link */
> -		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name, NULL) < 0)
> +		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name, 0, NULL) < 0)
>   			die(_("unable to update HEAD"));
>   		if (!option_bare) {
>   			refs_update_ref(get_main_ref_store(the_repository),
> @@ -701,7 +701,7 @@ static void update_head(const struct ref *our, const struct ref *remote,
>   		 * Unborn head from remote; same as "our" case above except
>   		 * that we have no ref to update.
>   		 */
> -		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn, NULL) < 0)
> +		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn, 0, NULL) < 0)
>   			die(_("unable to update HEAD"));
>   		if (!option_bare)
>   			install_branch_config(0, head, remote_name, unborn);
> diff --git a/builtin/notes.c b/builtin/notes.c
> index 04f9dfb7fb..6b42d1139f 100644
> --- a/builtin/notes.c
> +++ b/builtin/notes.c
> @@ -978,7 +978,7 @@ static int merge(int argc, const char **argv, const char *prefix)
>   			die(_("a notes merge into %s is already in-progress at %s"),
>   			    default_notes_ref(), wt->path);
>   		free_worktrees(worktrees);
> -		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", default_notes_ref(), NULL))
> +		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", default_notes_ref(), 0, NULL))
>   			die(_("failed to store link to current notes ref (%s)"),
>   			    default_notes_ref());
>   		fprintf(stderr, _("Automatic notes merge failed. Fix conflicts in %s "
> diff --git a/builtin/remote.c b/builtin/remote.c
> index 0acc547d69..d28c65599d 100644
> --- a/builtin/remote.c
> +++ b/builtin/remote.c
> @@ -243,7 +243,7 @@ static int add(int argc, const char **argv, const char *prefix)
>   		strbuf_reset(&buf2);
>   		strbuf_addf(&buf2, "refs/remotes/%s/%s", name, master);
>   
> -		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote add"))
> +		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, 0, "remote add"))
>   			result = error(_("Could not setup master '%s'"), master);
>   	}
>   
> @@ -863,7 +863,7 @@ static int mv(int argc, const char **argv, const char *prefix)
>   		strbuf_reset(&buf3);
>   		strbuf_addf(&buf3, "remote: renamed %s to %s",
>   				item->string, buf.buf);
> -		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, buf3.buf))
> +		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, 0, buf3.buf))
>   			die(_("creating '%s' failed"), buf.buf);
>   		display_progress(progress, ++refs_renamed_nr);
>   	}
> @@ -1443,7 +1443,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
>   		/* make sure it's valid */
>   		if (!refs_ref_exists(get_main_ref_store(the_repository), buf2.buf))
>   			result |= error(_("Not a valid ref: %s"), buf2.buf);
> -		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head"))
> +		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, 0, "remote set-head"))
>   			result |= error(_("Could not setup %s"), buf.buf);
>   		else if (opt_a)
>   			printf("%s/HEAD set to %s\n", argv[0], head_name);
> diff --git a/builtin/symbolic-ref.c b/builtin/symbolic-ref.c
> index 81abdd170f..52de3e02af 100644
> --- a/builtin/symbolic-ref.c
> +++ b/builtin/symbolic-ref.c
> @@ -84,7 +84,7 @@ int cmd_symbolic_ref(int argc, const char **argv, const char *prefix)
>   		if (check_refname_format(argv[1], REFNAME_ALLOW_ONELEVEL) < 0)
>   			die("Refusing to set '%s' to invalid ref '%s'", argv[0], argv[1]);
>   		ret = !!refs_update_symref(get_main_ref_store(the_repository),
> -					   argv[0], argv[1], msg);
> +					   argv[0], argv[1], 0, msg);
>   		break;
>   	default:
>   		usage_with_options(git_symbolic_ref_usage, options);
> diff --git a/builtin/worktree.c b/builtin/worktree.c
> index 41e7f6a327..71434737e8 100644
> --- a/builtin/worktree.c
> +++ b/builtin/worktree.c
> @@ -517,7 +517,7 @@ static int add_worktree(const char *path, const char *refname,
>   		ret = refs_update_ref(wt_refs, NULL, "HEAD", &commit->object.oid,
>   				      NULL, 0, UPDATE_REFS_MSG_ON_ERR);
>   	else
> -		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, NULL);
> +		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, 0, NULL);
>   	if (ret)
>   		goto done;
>   
> diff --git a/refs.c b/refs.c
> index ceb72d4bd7..7afe46cadc 100644
> --- a/refs.c
> +++ b/refs.c
> @@ -2085,8 +2085,9 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
>   	return peel_object(r, base, peeled) ? -1 : 0;
>   }
>   
> +
>   int refs_update_symref(struct ref_store *refs, const char *ref,
> -		       const char *target, const char *logmsg)
> +		       const char *target, const unsigned int extra_flags, const char *logmsg)
>   {
>   	struct ref_transaction *transaction;
>   	struct strbuf err = STRBUF_INIT;
> @@ -2095,7 +2096,7 @@ int refs_update_symref(struct ref_store *refs, const char *ref,
>   	transaction = ref_store_transaction_begin(refs, &err);
>   	if (!transaction ||
>   	    ref_transaction_update(transaction, ref, NULL, NULL,
> -				   target, NULL, REF_NO_DEREF,
> +				   target, NULL, REF_NO_DEREF | extra_flags,
>   				   logmsg, &err) ||
>   	    ref_transaction_commit(transaction, &err)) {
>   		ret = error("%s", err.buf);
> @@ -2920,4 +2921,3 @@ int ref_update_expects_existing_old_ref(struct ref_update *update)
>   	return (update->flags & REF_HAVE_OLD) &&
>   		(!is_null_oid(&update->old_oid) || update->old_target);
>   }
> -
> diff --git a/refs.h b/refs.h
> index f8b919a138..d907451d13 100644
> --- a/refs.h
> +++ b/refs.h
> @@ -569,7 +569,7 @@ int refs_copy_existing_ref(struct ref_store *refs, const char *oldref,
>   		    const char *newref, const char *logmsg);
>   
>   int refs_update_symref(struct ref_store *refs, const char *refname,
> -		       const char *target, const char *logmsg);
> +		       const char *target, const unsigned int extra_flags, const char *logmsg);
>   
>   enum action_on_err {
>   	UPDATE_REFS_MSG_ON_ERR,
> @@ -672,13 +672,21 @@ struct ref_transaction *ref_store_transaction_begin(struct ref_store *refs,
>    */
>   #define REF_SKIP_CREATE_REFLOG (1 << 12)
>   
> +/*
> + * If the reference has already been created do not touch it.
> + */
> +
> +#define REF_CREATE_ONLY (1 << 13)
> +
> +
>   /*
>    * Bitmask of all of the flags that are allowed to be passed in to
>    * ref_transaction_update() and friends:
>    */
>   #define REF_TRANSACTION_UPDATE_ALLOWED_FLAGS                                  \
>   	(REF_NO_DEREF | REF_FORCE_CREATE_REFLOG | REF_SKIP_OID_VERIFICATION | \
> -	 REF_SKIP_REFNAME_VERIFICATION | REF_SKIP_CREATE_REFLOG)
> +	 REF_SKIP_REFNAME_VERIFICATION | REF_SKIP_CREATE_REFLOG | \
> +	 REF_CREATE_ONLY)
>   
>   /*
>    * Add a reference update to transaction. `new_oid` is the value that
> diff --git a/refs/files-backend.c b/refs/files-backend.c
> index c7f3f4e591..1440b69b87 100644
> --- a/refs/files-backend.c
> +++ b/refs/files-backend.c
> @@ -2993,6 +2993,8 @@ static int files_transaction_finish(struct ref_store *ref_store,
>   		struct ref_update *update = transaction->updates[i];
>   		struct ref_lock *lock = update->backend_data;
>   
> +		if (update->flags & REF_CREATE_ONLY && refs_ref_exists(ref_store, update->refname))
> +			continue;
>   		if (update->flags & REF_NEEDS_COMMIT ||
>   		    update->flags & REF_LOG_ONLY) {
>   			if (parse_and_write_reflog(refs, update, lock, err)) {
> @@ -3031,6 +3033,8 @@ static int files_transaction_finish(struct ref_store *ref_store,
>   	 */
>   	for (i = 0; i < transaction->nr; i++) {
>   		struct ref_update *update = transaction->updates[i];
> +		if (update->flags & REF_CREATE_ONLY && refs_ref_exists(ref_store, update->refname))
> +			continue;
>   		if (update->flags & REF_DELETING &&
>   		    !(update->flags & REF_LOG_ONLY) &&
>   		    !(update->flags & REF_IS_PRUNING)) {
> @@ -3061,6 +3065,8 @@ static int files_transaction_finish(struct ref_store *ref_store,
>   	for (i = 0; i < transaction->nr; i++) {
>   		struct ref_update *update = transaction->updates[i];
>   		struct ref_lock *lock = update->backend_data;
> +		if (update->flags & REF_CREATE_ONLY && refs_ref_exists(ref_store, update->refname))
> +			continue;
>   
>   		if (update->flags & REF_DELETING &&
>   		    !(update->flags & REF_LOG_ONLY)) {
> @@ -3085,6 +3091,8 @@ static int files_transaction_finish(struct ref_store *ref_store,
>   
>   	for (i = 0; i < transaction->nr; i++) {
>   		struct ref_update *update = transaction->updates[i];
> +		if (update->flags & REF_CREATE_ONLY && refs_ref_exists(ref_store, update->refname))
> +			continue;
>   
>   		if (update->flags & REF_DELETED_RMDIR) {
>   			/*
> diff --git a/reset.c b/reset.c
> index b22b1be792..8dce5f2133 100644
> --- a/reset.c
> +++ b/reset.c
> @@ -75,7 +75,7 @@ static int update_refs(const struct reset_head_opts *opts,
>   				      UPDATE_REFS_MSG_ON_ERR);
>   		if (!ret)
>   			ret = refs_update_symref(get_main_ref_store(the_repository),
> -						 "HEAD", switch_to_branch,
> +						 "HEAD", switch_to_branch, 0,
>   						 reflog_head);
>   	}
>   	if (!ret && run_hook)
> diff --git a/sequencer.c b/sequencer.c
> index 8d01cd50ac..924a78dab8 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -5107,7 +5107,7 @@ static int pick_commits(struct repository *r,
>   			}
>   			msg = reflog_message(opts, "finish", "returning to %s",
>   				head_ref.buf);
> -			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf, msg)) {
> +			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf, 0, msg)) {
>   				res = error(_("could not update HEAD to %s"),
>   					head_ref.buf);
>   				goto cleanup_head_ref;
> diff --git a/setup.c b/setup.c
> index 29f8673921..6a1fdef2c3 100644
> --- a/setup.c
> +++ b/setup.c
> @@ -2169,7 +2169,7 @@ void create_reference_database(enum ref_storage_format ref_storage_format,
>   			die(_("invalid initial branch name: '%s'"),
>   			    initial_branch);
>   
> -		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref, NULL) < 0)
> +		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref, 0, NULL) < 0)
>   			exit(1);
>   		free(ref);
>   	}
> diff --git a/t/helper/test-ref-store.c b/t/helper/test-ref-store.c
> index 65346dee55..90af41edce 100644
> --- a/t/helper/test-ref-store.c
> +++ b/t/helper/test-ref-store.c
> @@ -120,7 +120,7 @@ static int cmd_create_symref(struct ref_store *refs, const char **argv)
>   	const char *target = notnull(*argv++, "target");
>   	const char *logmsg = *argv++;
>   
> -	return refs_update_symref(refs, refname, target, logmsg);
> +	return refs_update_symref(refs, refname, target, 0, logmsg);
>   }
>   
>   static struct flag_definition transaction_flags[] = {
Bence Ferdinandy· Sep 21, 2024, 22:19 UTC · re: Phillip Wood · lore

Re: [PATCH v3 1/2] update_symref: add REF_CREATE_ONLY option

On Sat Sep 21, 2024 at 15:40, Phillip Wood <phillip.wood123@gmail.com> wrote:
Show 8 quoted lines
> On 19/09/2024 13:13, Bence Ferdinandy wrote:
> > Add a new REF_CREATE_ONLY flag for use by the files backend which will
> > only update the symref if it doesn't already exist. Add the possibility
> > to pass extra flags to refs_update_symref so that it can utilize this
> > new flag.
>
> I'm not sure we need a new flag to do this as it is already supported by
> the ref transaction api.

Thanks, I was not aware of ref_transaction_create. It also seems to return with TRANSACTION_NAME_CONFLICT so we should be able to see from the error code if indeed the existence was the problem or something else went wrong.

Best, Bence

Junio C Hamano· Sep 22, 2024, 16:56 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v3 1/2] update_symref: add REF_CREATE_ONLY option

"Bence Ferdinandy" <bence@ferdinandy.com> writes:
Show 13 quoted lines
> On Sat Sep 21, 2024 at 15:40, Phillip Wood <phillip.wood123@gmail.com> wrote:
>> On 19/09/2024 13:13, Bence Ferdinandy wrote:
>> > Add a new REF_CREATE_ONLY flag for use by the files backend which will
>> > only update the symref if it doesn't already exist. Add the possibility
>> > to pass extra flags to refs_update_symref so that it can utilize this
>> > new flag.
>>
>> I'm not sure we need a new flag to do this as it is already supported by
>> the ref transaction api.
>
> Thanks, I was not aware of ref_transaction_create. It also seems to return with
> TRANSACTION_NAME_CONFLICT so we should be able to see from the error code if
> indeed the existence was the problem or something else went wrong.

Thank you for working on this, and thanks, Phillip, for a useful suggestion.

Bence Ferdinandy· Sep 29, 2024, 22:58 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v3 1/2] update_symref: add REF_CREATE_ONLY option

On Sun Sep 22, 2024 at 00:19, Bence Ferdinandy <bence@ferdinandy.com> wrote:
Show 14 quoted lines
>
> On Sat Sep 21, 2024 at 15:40, Phillip Wood <phillip.wood123@gmail.com> wrote:
>> On 19/09/2024 13:13, Bence Ferdinandy wrote:
>> > Add a new REF_CREATE_ONLY flag for use by the files backend which will
>> > only update the symref if it doesn't already exist. Add the possibility
>> > to pass extra flags to refs_update_symref so that it can utilize this
>> > new flag.
>>
>> I'm not sure we need a new flag to do this as it is already supported by
>> the ref transaction api.
>
> Thanks, I was not aware of ref_transaction_create. It also seems to return with
> TRANSACTION_NAME_CONFLICT so we should be able to see from the error code if
> indeed the existence was the problem or something else went wrong.

Unfortunately, it seems that my reading of the code did not pass practice. When using ref_transaction_create ref_transaction_commit will return with -2 if the reference already exists, but it also returns with -2 for various other issues, like if the lock file already exists. I could parse the error message to see what was the cause, but that doesn't feel like a robust solution. Since fetch should _not_ error out on this, I think the REF_CREATE_ONLY flag is warranted. As it stands, it would serve a different purpose than ref_transaction_create, i.e. a "silent" create-only.

I'll send a v4 tomorrow hopefully.

Best, Bence

Patrick Steinhardt· Sep 30, 2024, 06:40 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v3 1/2] update_symref: add REF_CREATE_ONLY option

On Mon, Sep 30, 2024 at 12:58:05AM +0200, Bence Ferdinandy wrote:
Show 27 quoted lines
> 
> On Sun Sep 22, 2024 at 00:19, Bence Ferdinandy <bence@ferdinandy.com> wrote:
> >
> > On Sat Sep 21, 2024 at 15:40, Phillip Wood <phillip.wood123@gmail.com> wrote:
> >> On 19/09/2024 13:13, Bence Ferdinandy wrote:
> >> > Add a new REF_CREATE_ONLY flag for use by the files backend which will
> >> > only update the symref if it doesn't already exist. Add the possibility
> >> > to pass extra flags to refs_update_symref so that it can utilize this
> >> > new flag.
> >>
> >> I'm not sure we need a new flag to do this as it is already supported by
> >> the ref transaction api.
> >
> > Thanks, I was not aware of ref_transaction_create. It also seems to return with
> > TRANSACTION_NAME_CONFLICT so we should be able to see from the error code if
> > indeed the existence was the problem or something else went wrong.
> 
> Unfortunately, it seems that my reading of the code did not pass practice. When
> using ref_transaction_create ref_transaction_commit will return with -2 if the
> reference already exists, but it also returns with -2 for various other issues,
> like if the lock file already exists. I could parse the error message to see
> what was the cause, but that doesn't feel like a robust solution. Since fetch
> should _not_ error out on this, I think the REF_CREATE_ONLY flag is warranted.
> As it stands, it would serve a different purpose than ref_transaction_create,
> i.e. a "silent" create-only. 
> 
> I'll send a v4 tomorrow hopefully.

I don't think that is a good reason to introduce this new flag though. If we need to have a proper way to identify this specific failure case we should rather update the already-existing mechanism to give us useful signals, shouldn't we?

The problem with this flag is that it basically duplicates functionality that already exists, and it needs to be wired up by every ref backend that we have and that we're adding in the future. Your patch for example only implements the functionality for the "files" backend, but it must also be wired up for the "reftable" backend or otherwise it would be broken.

Another issue is that it gives you more ways to create nonsensical ref updates. With it you could for example create requests with a non-zero old object ID, and if it has `REF_CREATE_ONLY` set it would never be possible to fulfill the request. There's probably other cases where you can create nonsensical ref updates already, but we shouldn't add more ways of doing that.

Mind you, if we go the way I propose and improve the error reporting we'd also have to adapt both backends to do so. But that would be plugging a gap for which we have no proper solution right now instead of circumventing the current design by duplicating the functionality that we already have in a way that makes us able to handle this.

Patrick
Bence Ferdinandy· Sep 30, 2024, 09:27 UTC · re: Patrick Steinhardt · lore

Re: [PATCH v3 1/2] update_symref: add REF_CREATE_ONLY option

On Mon Sep 30, 2024 at 08:40, Patrick Steinhardt <ps@pks.im> wrote:
Show 26 quoted lines
> I don't think that is a good reason to introduce this new flag though.
> If we need to have a proper way to identify this specific failure case
> we should rather update the already-existing mechanism to give us useful
> signals, shouldn't we?
>
> The problem with this flag is that it basically duplicates functionality
> that already exists, and it needs to be wired up by every ref backend
> that we have and that we're adding in the future. Your patch for example
> only implements the functionality for the "files" backend, but it must
> also be wired up for the "reftable" backend or otherwise it would be
> broken.
>
> Another issue is that it gives you more ways to create nonsensical ref
> updates. With it you could for example create requests with a non-zero
> old object ID, and if it has `REF_CREATE_ONLY` set it would never be
> possible to fulfill the request. There's probably other cases where you
> can create nonsensical ref updates already, but we shouldn't add more
> ways of doing that.
>
> Mind you, if we go the way I propose and improve the error reporting
> we'd also have to adapt both backends to do so. But that would be
> plugging a gap for which we have no proper solution right now instead of
> circumventing the current design by duplicating the functionality that
> we already have in a way that makes us able to handle this.
>
> Patrick

You make a convincing argument :) I'll try to see if I can add another transaction error code for when you only want to create not overwrite and the ref already exists and pass it up (for both files and reftables, you also don't mention it, but I think this would not concern packed after a quick glance at the code).

Best, Bence

-- 
bence.ferdinandy.com
Bence Ferdinandy· Sep 30, 2024, 22:19 UTC · re: Bence Ferdinandy · lore

[PATCH v4 0/5] improve handling of remote/HEAD

This version has dropped some previous patches in favour of new ones. These solve the problem of atomically deciding what the head was before we updated it and also not reinventing the wheel for having an "create this ref, but if it already exists, fail silently" call.

Since now both changes to remote set-head and fetch utilize some common preceding patches I have again included in this series my changes to remote set-head. Sorry for the mess tangent there.

All tests (that run on my system) pass for the patches up to the last patch, which changes fetch. For the last patch I updated some of the test where it was trivial (lot of remote/HEAD -> something appeared as expected), but there is still work to be done there (i.e. figure out which tests need updating and which are actual bugs in the patch).

One specific thing I wanted to mention here. Junio asked earlier what happens on fetch, when we have a config like this:

Show 5 quoted lines
> If I am only fetching refs (or HEAD) in FETCH_HEAD for immediate
> consumtion by doing "git pull bence HEAD" with something like
>
>     [remote "bence"]
>          URL = http://github.com/bence/git

In this case setting head will error out with the generic error message I put at the end.

Bence Ferdinandy (5):
  refs_update_symref: atomically record overwritten ref
  set-head: better output for --auto
  transaction: add TRANSACTION_CREATE_EXISTS error
  refs_update_symref: add create_only option
  fetch: set remote/HEAD if it does not exist
 builtin/branch.c          |  2 +-
 builtin/checkout.c        |  5 ++-
 builtin/clone.c           |  8 ++--
 builtin/fetch.c           | 83 +++++++++++++++++++++++++++++++++++++++
 builtin/notes.c           |  3 +-
 builtin/remote.c          | 43 ++++++++++++++++----
 builtin/symbolic-ref.c    |  2 +-
 builtin/worktree.c        |  2 +-
 refs.c                    | 35 ++++++++++++-----
 refs.h                    |  7 +++-
 refs/files-backend.c      | 29 ++++++++++----
 refs/refs-internal.h      |  8 ++++
 refs/reftable-backend.c   |  6 ++-
 reset.c                   |  2 +-
 sequencer.c               |  3 +-
 setup.c                   |  3 +-
 t/helper/test-ref-store.c |  2 +-
 t/t5505-remote.sh         | 13 +++++-
 t/t5514-fetch-multiple.sh |  9 +++++
 19 files changed, 222 insertions(+), 43 deletions(-)
-- 
2.47.0.rc0.5.gf1cffeb8df.dirty
Bence Ferdinandy· Sep 30, 2024, 22:19 UTC · re: Bence Ferdinandy · lore

[PATCH v4 1/5] refs_update_symref: atomically record overwritten ref

When updating a symref it's currently not possible to know for sure what was the previous value that was overwritten. Make use of ref_transaction's atomicity and record the previous value there. Add a new variable to refs_update_symref's signature to be able to pass this information back up to callers.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
 builtin/branch.c          | 2 +-
 builtin/checkout.c        | 4 ++--
 builtin/clone.c           | 6 +++---
 builtin/notes.c           | 2 +-
 builtin/remote.c          | 6 +++---
 builtin/symbolic-ref.c    | 2 +-
 builtin/worktree.c        | 2 +-
 refs.c                    | 8 ++++++--
 refs.h                    | 3 ++-
 refs/files-backend.c      | 1 +
 refs/refs-internal.h      | 8 ++++++++
 reset.c                   | 2 +-
 sequencer.c               | 2 +-
 setup.c                   | 2 +-
 t/helper/test-ref-store.c | 2 +-
 15 files changed, 33 insertions(+), 19 deletions(-)
Show changes to 15 files +33 −19

builtin/branch.c, builtin/checkout.c, builtin/clone.c, builtin/notes.c, builtin/remote.c, builtin/symbolic-ref.c, builtin/worktree.c, refs.c, refs.h, refs/files-backend.c, refs/refs-internal.h, reset.c, sequencer.c, setup.c, t/helper/test-ref-store.c

diff --git a/builtin/branch.c b/builtin/branch.c
index fd1611ebf5..6c87690b58 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -559,7 +559,7 @@ static int replace_each_worktree_head_symref(struct worktree **worktrees,
 			continue;
 
 		refs = get_worktree_ref_store(worktrees[i]);
-		if (refs_update_symref(refs, "HEAD", newref, logmsg))
+		if (refs_update_symref(refs, "HEAD", newref, logmsg, NULL))
 			ret = error(_("HEAD of working tree %s is not updated"),
 				    worktrees[i]->path);
 	}
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 9c30000d3a..356ee9bcde 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -1015,7 +1015,7 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 			describe_detached_head(_("HEAD is now at"), new_branch_info->commit);
 		}
 	} else if (new_branch_info->path) {	/* Switch branches. */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path, msg.buf) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path, msg.buf, NULL) < 0)
 			die(_("unable to update HEAD"));
 		if (!opts->quiet) {
 			if (old_branch_info->path && !strcmp(new_branch_info->path, old_branch_info->path)) {
@@ -1479,7 +1479,7 @@ static int switch_unborn_to_new_branch(const struct checkout_opts *opts)
 		die(_("You are on a branch yet to be born"));
 	strbuf_addf(&branch_ref, "refs/heads/%s", opts->new_branch);
 	status = refs_update_symref(get_main_ref_store(the_repository),
-				    "HEAD", branch_ref.buf, "checkout -b");
+				    "HEAD", branch_ref.buf, "checkout -b", NULL);
 	strbuf_release(&branch_ref);
 	if (!opts->quiet)
 		fprintf(stderr, _("Switched to a new branch '%s'\n"),
diff --git a/builtin/clone.c b/builtin/clone.c
index e77339c847..ead2af20ea 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -661,7 +661,7 @@ static void update_remote_refs(const struct ref *refs,
 		strbuf_addstr(&head_ref, "HEAD");
 		if (refs_update_symref(get_main_ref_store(the_repository), head_ref.buf,
 				       remote_head_points_at->peer_ref->name,
-				       msg) < 0)
+				       msg, NULL) < 0)
 			die(_("unable to update %s"), head_ref.buf);
 		strbuf_release(&head_ref);
 	}
@@ -673,7 +673,7 @@ static void update_head(const struct ref *our, const struct ref *remote,
 	const char *head;
 	if (our && skip_prefix(our->name, "refs/heads/", &head)) {
 		/* Local default branch link */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name, NULL, NULL) < 0)
 			die(_("unable to update HEAD"));
 		if (!option_bare) {
 			refs_update_ref(get_main_ref_store(the_repository),
@@ -702,7 +702,7 @@ static void update_head(const struct ref *our, const struct ref *remote,
 		 * Unborn head from remote; same as "our" case above except
 		 * that we have no ref to update.
 		 */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn, NULL, NULL) < 0)
 			die(_("unable to update HEAD"));
 		if (!option_bare)
 			install_branch_config(0, head, remote_name, unborn);
diff --git a/builtin/notes.c b/builtin/notes.c
index 8c26e45526..ba646f06ff 100644
--- a/builtin/notes.c
+++ b/builtin/notes.c
@@ -980,7 +980,7 @@ static int merge(int argc, const char **argv, const char *prefix)
 			die(_("a notes merge into %s is already in-progress at %s"),
 			    notes_ref, wt->path);
 		free_worktrees(worktrees);
-		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", notes_ref, NULL))
+		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", notes_ref, NULL, NULL))
 			die(_("failed to store link to current notes ref (%s)"),
 			    notes_ref);
 		fprintf(stderr, _("Automatic notes merge failed. Fix conflicts in %s "
diff --git a/builtin/remote.c b/builtin/remote.c
index 76670ddd8b..d8ff440027 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -244,7 +244,7 @@ static int add(int argc, const char **argv, const char *prefix)
 		strbuf_reset(&buf2);
 		strbuf_addf(&buf2, "refs/remotes/%s/%s", name, master);
 
-		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote add"))
+		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote add", NULL))
 			result = error(_("Could not setup master '%s'"), master);
 	}
 
@@ -864,7 +864,7 @@ static int mv(int argc, const char **argv, const char *prefix)
 		strbuf_reset(&buf3);
 		strbuf_addf(&buf3, "remote: renamed %s to %s",
 				item->string, buf.buf);
-		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, buf3.buf))
+		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, buf3.buf, NULL))
 			die(_("creating '%s' failed"), buf.buf);
 		display_progress(progress, ++refs_renamed_nr);
 	}
@@ -1444,7 +1444,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		/* make sure it's valid */
 		if (!refs_ref_exists(get_main_ref_store(the_repository), buf2.buf))
 			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head"))
+		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head", NULL))
 			result |= error(_("Could not setup %s"), buf.buf);
 		else if (opt_a)
 			printf("%s/HEAD set to %s\n", argv[0], head_name);
diff --git a/builtin/symbolic-ref.c b/builtin/symbolic-ref.c
index 299d23d76a..7728fbc3c1 100644
--- a/builtin/symbolic-ref.c
+++ b/builtin/symbolic-ref.c
@@ -88,7 +88,7 @@ int cmd_symbolic_ref(int argc,
 		if (check_refname_format(argv[1], REFNAME_ALLOW_ONELEVEL) < 0)
 			die("Refusing to set '%s' to invalid ref '%s'", argv[0], argv[1]);
 		ret = !!refs_update_symref(get_main_ref_store(the_repository),
-					   argv[0], argv[1], msg);
+					   argv[0], argv[1], msg, NULL);
 		break;
 	default:
 		usage_with_options(git_symbolic_ref_usage, options);
diff --git a/builtin/worktree.c b/builtin/worktree.c
index fc31d072a6..a7ab4193c1 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -517,7 +517,7 @@ static int add_worktree(const char *path, const char *refname,
 		ret = refs_update_ref(wt_refs, NULL, "HEAD", &commit->object.oid,
 				      NULL, 0, UPDATE_REFS_MSG_ON_ERR);
 	else
-		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, NULL);
+		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, NULL, NULL);
 	if (ret)
 		goto done;
 
diff --git a/refs.c b/refs.c
index 5f729ed412..301db0dcdc 100644
--- a/refs.c
+++ b/refs.c
@@ -2114,7 +2114,8 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
 }
 
 int refs_update_symref(struct ref_store *refs, const char *ref,
-		       const char *target, const char *logmsg)
+		       const char *target, const char *logmsg,
+		       struct strbuf *before_target)
 {
 	struct ref_transaction *transaction;
 	struct strbuf err = STRBUF_INIT;
@@ -2130,6 +2131,10 @@ int refs_update_symref(struct ref_store *refs, const char *ref,
 	}
 
 	strbuf_release(&err);
+
+	if (before_target && transaction->updates[0]->before_target)
+		strbuf_addstr(before_target, transaction->updates[0]->before_target);
+
 	if (transaction)
 		ref_transaction_free(transaction);
 
@@ -2948,4 +2953,3 @@ int ref_update_expects_existing_old_ref(struct ref_update *update)
 	return (update->flags & REF_HAVE_OLD) &&
 		(!is_null_oid(&update->old_oid) || update->old_target);
 }
-
diff --git a/refs.h b/refs.h
index 108dfc93b3..f38616db84 100644
--- a/refs.h
+++ b/refs.h
@@ -571,7 +571,8 @@ int refs_copy_existing_ref(struct ref_store *refs, const char *oldref,
 		    const char *newref, const char *logmsg);
 
 int refs_update_symref(struct ref_store *refs, const char *refname,
-		       const char *target, const char *logmsg);
+		       const char *target, const char *logmsg,
+		       struct strbuf *before_target);
 
 enum action_on_err {
 	UPDATE_REFS_MSG_ON_ERR,
diff --git a/refs/files-backend.c b/refs/files-backend.c
index 0824c0b8a9..8415f2d020 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -2577,6 +2577,7 @@ static int lock_ref_for_update(struct files_ref_store *refs,
 	}
 
 	update->backend_data = lock;
+	update->before_target = xstrdup_or_null(referent.buf);
 
 	if (update->type & REF_ISSYMREF) {
 		if (update->flags & REF_NO_DEREF) {
diff --git a/refs/refs-internal.h b/refs/refs-internal.h
index 2313c830d8..7df3e6271e 100644
--- a/refs/refs-internal.h
+++ b/refs/refs-internal.h
@@ -104,6 +104,14 @@ struct ref_update {
 	 */
 	const char *old_target;
 
+	/*
+	 * The previous target before applying new_target will be
+	 * written here, to be used by callers when they do not want to
+	 * check old_target during the transaction, but do want to know
+	 * what it was.
+	 */
+	const char *before_target;
+
 	/*
 	 * One or more of REF_NO_DEREF, REF_FORCE_CREATE_REFLOG,
 	 * REF_HAVE_NEW, REF_HAVE_OLD, or backend-specific flags.
diff --git a/reset.c b/reset.c
index b22b1be792..cc36a9ed56 100644
--- a/reset.c
+++ b/reset.c
@@ -76,7 +76,7 @@ static int update_refs(const struct reset_head_opts *opts,
 		if (!ret)
 			ret = refs_update_symref(get_main_ref_store(the_repository),
 						 "HEAD", switch_to_branch,
-						 reflog_head);
+						 reflog_head, NULL);
 	}
 	if (!ret && run_hook)
 		run_hooks_l(the_repository, "post-checkout",
diff --git a/sequencer.c b/sequencer.c
index 8d01cd50ac..23b162924c 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -5107,7 +5107,7 @@ static int pick_commits(struct repository *r,
 			}
 			msg = reflog_message(opts, "finish", "returning to %s",
 				head_ref.buf);
-			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf, msg)) {
+			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf, msg, NULL)) {
 				res = error(_("could not update HEAD to %s"),
 					head_ref.buf);
 				goto cleanup_head_ref;
diff --git a/setup.c b/setup.c
index 94e79b2e48..d95f051465 100644
--- a/setup.c
+++ b/setup.c
@@ -2275,7 +2275,7 @@ void create_reference_database(enum ref_storage_format ref_storage_format,
 			die(_("invalid initial branch name: '%s'"),
 			    initial_branch);
 
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref, NULL, NULL) < 0)
 			exit(1);
 		free(ref);
 	}
diff --git a/t/helper/test-ref-store.c b/t/helper/test-ref-store.c
index 65346dee55..a911302bea 100644
--- a/t/helper/test-ref-store.c
+++ b/t/helper/test-ref-store.c
@@ -120,7 +120,7 @@ static int cmd_create_symref(struct ref_store *refs, const char **argv)
 	const char *target = notnull(*argv++, "target");
 	const char *logmsg = *argv++;
 
-	return refs_update_symref(refs, refname, target, logmsg);
+	return refs_update_symref(refs, refname, target, logmsg, NULL);
 }
 
 static struct flag_definition transaction_flags[] = {
-- 
2.47.0.rc0.5.gf1cffeb8df.dirty
Junio C Hamano· Oct 1, 2024, 19:10 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v4 1/5] refs_update_symref: atomically record overwritten ref

Bence Ferdinandy <bence@ferdinandy.com> writes:
Show 15 quoted lines
> diff --git a/refs.c b/refs.c
> index 5f729ed412..301db0dcdc 100644
> --- a/refs.c
> +++ b/refs.c
> @@ -2114,7 +2114,8 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
>  }
>  
>  int refs_update_symref(struct ref_store *refs, const char *ref,
> -		       const char *target, const char *logmsg)
> +		       const char *target, const char *logmsg,
> +		       struct strbuf *before_target)
>  {
>  	struct ref_transaction *transaction;
>  	struct strbuf err = STRBUF_INIT;
> @@ -2130,6 +2131,10 @@ int refs_update_symref(struct ref_store *refs, const char *ref,

Let's extend the precontext of this hunk a bit. The function begins like this:

	transaction = ref_store_transaction_begin(refs, &err);
	if (!transaction ||
	    ref_transaction_update(transaction, ref, NULL, NULL,
				   target, NULL, REF_NO_DEREF,
				   logmsg, &err) ||
	    ref_transaction_commit(transaction, &err)) {
		ret = error("%s", err.buf);
>  	}
>  	strbuf_release(&err);

We begin a transaction, update ref to point to target in the transaction, and commit the transaction. An error at any stage of this three-step process will bypass the rest and we give an error message.

> +
> +	if (before_target && transaction->updates[0]->before_target)
> +		strbuf_addstr(before_target, transaction->updates[0]->before_target);
What if ref_store_transaction_begin() failed?

If we want to say "we append the before_target recorded in the transaction to the caller-supplied strbuf only when we manage to do the update, and we leave before_target intact otherwise" We'd at least need

	if (transaction && before_target &&
            transaction->updates[0]->before_target)

wouldn't it? Like the code that frees it (below), this new call should be prepared to see !transaction.

>  	if (transaction)
>  		ref_transaction_free(transaction);
Show 5 quoted lines
> @@ -2948,4 +2953,3 @@ int ref_update_expects_existing_old_ref(struct ref_update *update)
>  	return (update->flags & REF_HAVE_OLD) &&
>  		(!is_null_oid(&update->old_oid) || update->old_target);
>  }
> -
Good (even though it is unrelated to the topic of this series).
Show 23 quoted lines
> diff --git a/refs.h b/refs.h
> index 108dfc93b3..f38616db84 100644
> --- a/refs.h
> +++ b/refs.h
> @@ -571,7 +571,8 @@ int refs_copy_existing_ref(struct ref_store *refs, const char *oldref,
>  		    const char *newref, const char *logmsg);
>  
>  int refs_update_symref(struct ref_store *refs, const char *refname,
> -		       const char *target, const char *logmsg);
> +		       const char *target, const char *logmsg,
> +		       struct strbuf *before_target);
>  
>  enum action_on_err {
>  	UPDATE_REFS_MSG_ON_ERR,
> diff --git a/refs/files-backend.c b/refs/files-backend.c
> index 0824c0b8a9..8415f2d020 100644
> --- a/refs/files-backend.c
> +++ b/refs/files-backend.c
> @@ -2577,6 +2577,7 @@ static int lock_ref_for_update(struct files_ref_store *refs,
>  	}
>  
>  	update->backend_data = lock;
> +	update->before_target = xstrdup_or_null(referent.buf);

OK, so this comes from the backends, as they are the only thing that knows what the current value is (the caller can only indirectly infer if it has old_target, in which case the backend checks if the attempt is stale).

Do we need a corresponding change for the other, reftable, backend?
Show 13 quoted lines
> diff --git a/t/helper/test-ref-store.c b/t/helper/test-ref-store.c
> index 65346dee55..a911302bea 100644
> --- a/t/helper/test-ref-store.c
> +++ b/t/helper/test-ref-store.c
> @@ -120,7 +120,7 @@ static int cmd_create_symref(struct ref_store *refs, const char **argv)
>  	const char *target = notnull(*argv++, "target");
>  	const char *logmsg = *argv++;
>  
> -	return refs_update_symref(refs, refname, target, logmsg);
> +	return refs_update_symref(refs, refname, target, logmsg, NULL);
>  }
>  
>  static struct flag_definition transaction_flags[] = {
Bence Ferdinandy· Sep 30, 2024, 22:19 UTC · re: Bence Ferdinandy · lore

[PATCH v4 2/5] set-head: better output for --auto

Currently, set-head --auto will print a message saying "remote/HEAD set to branch", which implies something was changed.

Change the output of --auto, so the output actually reflects what was done: a) set a previously unset HEAD, b) change HEAD because remote changed or c) no updates.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v1-v2:
    
        was RFC in https://lore.kernel.org/git/20240910203835.2288291-1-bence@ferdinandy.com/
    
    v3:
    
        This patch was originally sent along when I thought set-head was
        going to be invoked by fetch, but the discussion on the RFC
        concluded that it should be not. This opened the possibility to make
        it more explicit.
    
        Note: although I feel both things the patch does are really just
        cosmetic, an argument could be made for breaking it into two, one
        for the no-op part and one for the --auto print update.
    
        Was sent in:
        https://lore.kernel.org/git/20240915221055.904107-1-bence@ferdinandy.com/
    
    v4:
        - changes are now handled atomically via the ref update transaction
        - outputs have changed along the lines of Junio's suggestion
        - minor refactor to set_head for improved legibility
 builtin/remote.c  | 36 ++++++++++++++++++++++++++++++------
 t/t5505-remote.sh | 13 ++++++++++++-
 2 files changed, 42 insertions(+), 7 deletions(-)
Show changes to 2 files +42 −7

builtin/remote.c, t/t5505-remote.sh

diff --git a/builtin/remote.c b/builtin/remote.c
index d8ff440027..c61a7800ff 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1399,12 +1399,34 @@ static int show(int argc, const char **argv, const char *prefix)
 	return result;
 }
 
+static void report_auto(const char *remote, const char *head_name,
+			struct strbuf *buf_prev) {
+	struct strbuf buf_prefix = STRBUF_INIT;
+	const char *prev_head;
+
+	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
+	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
+
+	if (prev_head && !strcmp(prev_head, head_name))
+		printf("'%s/HEAD' is unchanged and points to '%s'\n",
+			remote, head_name);
+	else if (prev_head)
+		printf("'%s/HEAD' has changed from '%s' and now points to '%s'\n",
+			remote, prev_head, head_name);
+	else
+		printf("'%s/HEAD' is now created and points to '%s'\n",
+			remote, head_name);
+}
+
 static int set_head(int argc, const char **argv, const char *prefix)
 {
 	int i, opt_a = 0, opt_d = 0, result = 0;
-	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;
+	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT,
+		buf_prev = STRBUF_INIT;
 	char *head_name = NULL;
 
+	struct ref_store *refs = get_main_ref_store(the_repository);
+
 	struct option options[] = {
 		OPT_BOOL('a', "auto", &opt_a,
 			 N_("set refs/remotes/<name>/HEAD according to remote")),
@@ -1434,7 +1456,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 			head_name = xstrdup(states.heads.items[0].string);
 		free_remote_ref_states(&states);
 	} else if (opt_d && !opt_a && argc == 1) {
-		if (refs_delete_ref(get_main_ref_store(the_repository), NULL, buf.buf, NULL, REF_NO_DEREF))
+		if (refs_delete_ref(refs, NULL, buf.buf, NULL, REF_NO_DEREF))
 			result |= error(_("Could not delete %s"), buf.buf);
 	} else
 		usage_with_options(builtin_remote_sethead_usage, options);
@@ -1442,17 +1464,19 @@ static int set_head(int argc, const char **argv, const char *prefix)
 	if (head_name) {
 		strbuf_addf(&buf2, "refs/remotes/%s/%s", argv[0], head_name);
 		/* make sure it's valid */
-		if (!refs_ref_exists(get_main_ref_store(the_repository), buf2.buf))
+		if (!refs_ref_exists(refs, buf2.buf))
 			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head", NULL))
+		else if (refs_update_symref(refs, buf.buf, buf2.buf, "remote set-head", &buf_prev))
 			result |= error(_("Could not setup %s"), buf.buf);
-		else if (opt_a)
-			printf("%s/HEAD set to %s\n", argv[0], head_name);
+		else if (opt_a) {
+			report_auto(argv[0], head_name, &buf_prev);
+		}
 		free(head_name);
 	}
 
 	strbuf_release(&buf);
 	strbuf_release(&buf2);
+	strbuf_release(&buf_prev);
 	return result;
 }
 
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 532035933f..711d6e5598 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -434,7 +434,7 @@ test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
 		cd test &&
 		git fetch two "refs/heads/*:refs/remotes/two/*" &&
 		git remote set-head --auto two >output 2>&1 &&
-		echo "two/HEAD set to main" >expect &&
+		echo "'\''two/HEAD'\'' is now created and points to '\''main'\''" >expect &&
 		test_cmp expect output
 	)
 '
@@ -453,6 +453,17 @@ test_expect_success 'set-head explicit' '
 	)
 '
 
+
+test_expect_success 'set-head --auto reports change' '
+	(
+		cd test &&
+		git remote set-head origin side2 &&
+		git remote set-head --auto origin >output 2>&1 &&
+		echo "'\''origin/HEAD'\'' has changed from '\''side2'\'' and now points to '\''main'\''" >expect &&
+		test_cmp expect output
+	)
+'
+
 cat >test/expect <<EOF
 Pruning origin
 URL: $(pwd)/one
-- 
2.47.0.rc0.5.gf1cffeb8df.dirty
Junio C Hamano· Oct 1, 2024, 22:54 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v4 2/5] set-head: better output for --auto

Bence Ferdinandy <bence@ferdinandy.com> writes:
Show 18 quoted lines
> +static void report_auto(const char *remote, const char *head_name,
> +			struct strbuf *buf_prev) {
> +	struct strbuf buf_prefix = STRBUF_INIT;
> +	const char *prev_head;
> +
> +	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
> +	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
> +
> +	if (prev_head && !strcmp(prev_head, head_name))
> +		printf("'%s/HEAD' is unchanged and points to '%s'\n",
> +			remote, head_name);
> +	else if (prev_head)
> +		printf("'%s/HEAD' has changed from '%s' and now points to '%s'\n",
> +			remote, prev_head, head_name);
> +	else
> +		printf("'%s/HEAD' is now created and points to '%s'\n",
> +			remote, head_name);
> +}
OK.
Show 10 quoted lines
>  static int set_head(int argc, const char **argv, const char *prefix)
>  {
>  	int i, opt_a = 0, opt_d = 0, result = 0;
> -	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;
> +	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT,
> +		buf_prev = STRBUF_INIT;
>  	char *head_name = NULL;
>  
> +	struct ref_store *refs = get_main_ref_store(the_repository);
> +

What does the blank line between "head_name" and "refs" declaration indicate? Is "refs" so special that it must stand separately from all others?

The primary purpose of this step is to pass a strbuf to refs_update_symref() so that we can learn what the original value of the HEAD was, so that report_auto() can give a more intelligent message. The introduction of "refs" to avoid repetitive calls to get_main_ref_store() is orthogonal to that. It is a good change, but it should stand on its own, as a separate "preparatory clean-up" patch.

Show 31 quoted lines
> diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
> index 532035933f..711d6e5598 100755
> --- a/t/t5505-remote.sh
> +++ b/t/t5505-remote.sh
> @@ -434,7 +434,7 @@ test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
>  		cd test &&
>  		git fetch two "refs/heads/*:refs/remotes/two/*" &&
>  		git remote set-head --auto two >output 2>&1 &&
> -		echo "two/HEAD set to main" >expect &&
> +		echo "'\''two/HEAD'\'' is now created and points to '\''main'\''" >expect &&
>  		test_cmp expect output
>  	)
>  '
> @@ -453,6 +453,17 @@ test_expect_success 'set-head explicit' '
>  	)
>  '
>  
> +
> +test_expect_success 'set-head --auto reports change' '
> +	(
> +		cd test &&
> +		git remote set-head origin side2 &&
> +		git remote set-head --auto origin >output 2>&1 &&
> +		echo "'\''origin/HEAD'\'' has changed from '\''side2'\'' and now points to '\''main'\''" >expect &&
> +		test_cmp expect output
> +	)
> +'
> +
>  cat >test/expect <<EOF
>  Pruning origin
>  URL: $(pwd)/one
Bence Ferdinandy· Sep 30, 2024, 22:19 UTC · re: Bence Ferdinandy · lore

[PATCH v4 5/5] fetch: set remote/HEAD if it does not exist

If the user has remote/HEAD set already and it looks like it has changed on the server, then print a message, otherwise set it if we can. Silently pass if the user already has the same remote/HEAD set as reported by the server.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v3: - does not rely on remote set-head anymore so it only authenticates
        once
        - uses the new REF_CREATE_ONLY to atomically check if the ref exists
          and only write it if it doesn't
        - in all other cases the maximum it does is print a warning
    
    v4: - instead of the discarded REF_CREATE_ONLY, it uses the existing,
          but updated transaction api to request a silent create only
        - it now uses the atomic before_target to determine reporting
        - refactored for legibility
 builtin/fetch.c           | 83 +++++++++++++++++++++++++++++++++++++++
 t/t5514-fetch-multiple.sh |  9 +++++
 2 files changed, 92 insertions(+)
Show changes to 2 files +92 −0

builtin/fetch.c, t/t5514-fetch-multiple.sh

diff --git a/builtin/fetch.c b/builtin/fetch.c
index c900f57721..f10a9d5371 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1577,6 +1577,82 @@ static int backfill_tags(struct display_state *display_state,
 	return retcode;
 }
 
+static void report_set_head(const char *remote, const char *head_name,
+			struct strbuf *buf_prev) {
+	struct strbuf buf_prefix = STRBUF_INIT;
+	const char *prev_head;
+
+	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
+	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
+
+	if (prev_head && !strcmp(prev_head, head_name))
+		;
+	else if (prev_head) {
+		printf("'HEAD' at '%s' has changed from '%s' to '%s'\n",
+			remote, prev_head, head_name);
+		printf("Run 'git remote set-head %s %s' to follow the change.\n",
+			remote, head_name);
+	}
+}
+
+static const char *strip_refshead(const char *name){
+	skip_prefix(name, "refs/heads/", &name);
+	return name;
+}
+
+static int set_head(const struct ref *remote_refs)
+{
+	int result = 0;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
+		b_local_head = STRBUF_INIT;
+	const char *remote = gtransport->remote->name;
+	char *head_name = NULL;
+	struct ref *ref, *matches;
+	struct ref *fetch_map = NULL, **fetch_map_tail = &fetch_map;
+	struct refspec_item refspec = {
+		.force = 0,
+		.pattern = 1,
+		.src = (char *) "refs/heads/*",
+		.dst = (char *) "refs/heads/*",
+	};
+	struct string_list heads = STRING_LIST_INIT_DUP;
+	struct ref_store *refs = get_main_ref_store(the_repository);
+
+	get_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);
+	matches = guess_remote_head(find_ref_by_name(remote_refs, "HEAD"),
+				    fetch_map, 1);
+	for (ref = matches; ref; ref = ref->next) {
+		string_list_append(&heads, strip_refshead(ref->name));
+	}
+
+
+	if (!heads.nr)
+		result = 1;
+	else if (heads.nr > 1) {
+		result = 1;
+	} else
+		head_name = xstrdup(heads.items[0].string);
+	if (head_name) {
+		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
+		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote, head_name);
+		/* make sure it's valid */
+		if (!refs_ref_exists(refs, b_remote_head.buf))
+			result |= error(_("Not a valid ref: %s"), b_remote_head.buf);
+		else if (refs_update_symref(refs, b_head.buf, b_remote_head.buf,
+					"remote set-head", &b_local_head, true))
+			result |= error(_("Could not setup %s"), b_head.buf);
+		else {
+			report_set_head(remote, head_name, &b_local_head);
+		}
+		free(head_name);
+	}
+
+	strbuf_release(&b_head);
+	strbuf_release(&b_local_head);
+	strbuf_release(&b_remote_head);
+	return result;
+}
+
 static int do_fetch(struct transport *transport,
 		    struct refspec *rs,
 		    const struct fetch_config *config)
@@ -1646,6 +1722,8 @@ static int do_fetch(struct transport *transport,
 				    "refs/tags/");
 	}
 
+	strvec_push(&transport_ls_refs_options.ref_prefixes,"HEAD");
+
 	if (must_list_refs) {
 		trace2_region_enter("fetch", "remote_refs", the_repository);
 		remote_refs = transport_get_remote_refs(transport,
@@ -1790,6 +1868,10 @@ static int do_fetch(struct transport *transport,
 				  "you need to specify exactly one branch with the --set-upstream option"));
 		}
 	}
+	if (set_head(remote_refs))
+		printf("Ran into issues with creating \'%s/HEAD\',\n"
+			"use \'git remote set-head -a %s\' to investigate.\n",
+			gtransport->remote->name,gtransport->remote->name);
 
 cleanup:
 	if (retcode) {
@@ -2020,6 +2102,7 @@ static int fetch_multiple(struct string_list *list, int max_children,
 	return !!result;
 }
 
+
 /*
  * Fetching from the promisor remote should use the given filter-spec
  * or inherit the default filter-spec from the config.
diff --git a/t/t5514-fetch-multiple.sh b/t/t5514-fetch-multiple.sh
index 579872c258..ef5874b7b3 100755
--- a/t/t5514-fetch-multiple.sh
+++ b/t/t5514-fetch-multiple.sh
@@ -45,14 +45,17 @@ test_expect_success setup '
 '
 
 cat > test/expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
   origin/HEAD -> origin/main
   origin/main
   origin/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -97,6 +100,7 @@ cat > expect << EOF
   origin/HEAD -> origin/main
   origin/main
   origin/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
@@ -112,8 +116,10 @@ test_expect_success 'git fetch --multiple (but only one remote)' '
 '
 
 cat > expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -221,14 +227,17 @@ test_expect_success 'git fetch --multiple --jobs=0 picks a default' '
 
 create_fetch_all_expect () {
 	cat >expect <<-\EOF
+	  one/HEAD -> one/main
 	  one/main
 	  one/side
 	  origin/HEAD -> origin/main
 	  origin/main
 	  origin/side
+	  three/HEAD -> three/main
 	  three/another
 	  three/main
 	  three/side
+	  two/HEAD -> two/main
 	  two/another
 	  two/main
 	  two/side
-- 
2.47.0.rc0.5.gf1cffeb8df.dirty
Bence Ferdinandy· Sep 30, 2024, 22:19 UTC · re: Bence Ferdinandy · lore

[PATCH v4 3/5] transaction: add TRANSACTION_CREATE_EXISTS error

Currently there is only one special error for transaction, for when there is a naming conflict, all other errors are dumped under a generic error. Add a new special error case for when the caller requests the reference to be updated only when it does not yet exist and the reference actually does exist.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
 refs.h                  |  4 +++-
 refs/files-backend.c    | 28 ++++++++++++++++++++--------
 refs/reftable-backend.c |  6 ++++--
 3 files changed, 27 insertions(+), 11 deletions(-)
Show changes to 3 files +27 −11

refs.h, refs/files-backend.c, refs/reftable-backend.c

diff --git a/refs.h b/refs.h
index f38616db84..166affbc89 100644
--- a/refs.h
+++ b/refs.h
@@ -759,8 +759,10 @@ int ref_transaction_verify(struct ref_transaction *transaction,
 
 /* Naming conflict (for example, the ref names A and A/B conflict). */
 #define TRANSACTION_NAME_CONFLICT -1
+/* When only creation was requested, but the ref already exists. */
+#define TRANSACTION_CREATE_EXISTS -2
 /* All other errors. */
-#define TRANSACTION_GENERIC_ERROR -2
+#define TRANSACTION_GENERIC_ERROR -3
 
 /*
  * Perform the preparatory stages of committing `transaction`. Acquire
diff --git a/refs/files-backend.c b/refs/files-backend.c
index 8415f2d020..272ad81315 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -2502,14 +2502,18 @@ static int split_symref_update(struct ref_update *update,
 static int check_old_oid(struct ref_update *update, struct object_id *oid,
 			 struct strbuf *err)
 {
+	int ret = TRANSACTION_GENERIC_ERROR;
+
 	if (!(update->flags & REF_HAVE_OLD) ||
 		   oideq(oid, &update->old_oid))
 		return 0;
 
-	if (is_null_oid(&update->old_oid))
+	if (is_null_oid(&update->old_oid)) {
 		strbuf_addf(err, "cannot lock ref '%s': "
 			    "reference already exists",
 			    ref_update_original_update_refname(update));
+		ret = TRANSACTION_CREATE_EXISTS;
+	}
 	else if (is_null_oid(oid))
 		strbuf_addf(err, "cannot lock ref '%s': "
 			    "reference is missing but expected %s",
@@ -2522,7 +2526,7 @@ static int check_old_oid(struct ref_update *update, struct object_id *oid,
 			    oid_to_hex(oid),
 			    oid_to_hex(&update->old_oid));
 
-	return -1;
+	return ret;
 }
 
 /*
@@ -2603,9 +2607,13 @@ static int lock_ref_for_update(struct files_ref_store *refs,
 					ret = TRANSACTION_GENERIC_ERROR;
 					goto out;
 				}
-			} else if  (check_old_oid(update, &lock->old_oid, err)) {
-				ret = TRANSACTION_GENERIC_ERROR;
-				goto out;
+			} else {
+				int checkret;
+				checkret = check_old_oid(update, &lock->old_oid, err);
+				if  (checkret) {
+					ret = checkret;
+					goto out;
+				}
 			}
 		} else {
 			/*
@@ -2636,9 +2644,13 @@ static int lock_ref_for_update(struct files_ref_store *refs,
 				    update->old_target);
 			ret = TRANSACTION_GENERIC_ERROR;
 			goto out;
-		} else if  (check_old_oid(update, &lock->old_oid, err)) {
-			ret = TRANSACTION_GENERIC_ERROR;
-			goto out;
+		} else {
+			int checkret;
+			checkret = check_old_oid(update, &lock->old_oid, err);
+			if  (checkret) {
+				ret = checkret;
+				goto out;
+			}
 		}
 
 		/*
diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
index f5f957e6de..f66bbf5d49 100644
--- a/refs/reftable-backend.c
+++ b/refs/reftable-backend.c
@@ -1197,10 +1197,13 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 				goto done;
 			}
 		} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {
-			if (is_null_oid(&u->old_oid))
+			ret = TRANSACTION_NAME_CONFLICT;
+			if (is_null_oid(&u->old_oid)) {
 				strbuf_addf(err, _("cannot lock ref '%s': "
 						   "reference already exists"),
 					    ref_update_original_update_refname(u));
+				ret = TRANSACTION_CREATE_EXISTS;
+			}
 			else if (is_null_oid(&current_oid))
 				strbuf_addf(err, _("cannot lock ref '%s': "
 						   "reference is missing but expected %s"),
@@ -1212,7 +1215,6 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 					    ref_update_original_update_refname(u),
 					    oid_to_hex(&current_oid),
 					    oid_to_hex(&u->old_oid));
-			ret = -1;
 			goto done;
 		}
 
-- 
2.47.0.rc0.5.gf1cffeb8df.dirty
Bence Ferdinandy· Sep 30, 2024, 22:19 UTC · re: Bence Ferdinandy · lore

[PATCH v4 4/5] refs_update_symref: add create_only option

Allow the caller to specify that it only wants to update the symref if it does not already exist. Silently ignore the error from the transaction API if the symref already exists.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
 builtin/branch.c          |  2 +-
 builtin/checkout.c        |  5 +++--
 builtin/clone.c           |  8 +++++---
 builtin/notes.c           |  3 ++-
 builtin/remote.c          |  9 ++++++---
 builtin/symbolic-ref.c    |  2 +-
 builtin/worktree.c        |  2 +-
 refs.c                    | 29 +++++++++++++++++++++--------
 refs.h                    |  2 +-
 reset.c                   |  2 +-
 sequencer.c               |  3 ++-
 setup.c                   |  3 ++-
 t/helper/test-ref-store.c |  2 +-
 13 files changed, 47 insertions(+), 25 deletions(-)
Show changes to 13 files +47 −25

builtin/branch.c, builtin/checkout.c, builtin/clone.c, builtin/notes.c, builtin/remote.c, builtin/symbolic-ref.c, builtin/worktree.c, refs.c, refs.h, reset.c, sequencer.c, setup.c, t/helper/test-ref-store.c

diff --git a/builtin/branch.c b/builtin/branch.c
index 6c87690b58..3c9bc39800 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -559,7 +559,7 @@ static int replace_each_worktree_head_symref(struct worktree **worktrees,
 			continue;
 
 		refs = get_worktree_ref_store(worktrees[i]);
-		if (refs_update_symref(refs, "HEAD", newref, logmsg, NULL))
+		if (refs_update_symref(refs, "HEAD", newref, logmsg, NULL, false))
 			ret = error(_("HEAD of working tree %s is not updated"),
 				    worktrees[i]->path);
 	}
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 356ee9bcde..d9514e9b7a 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -1015,7 +1015,8 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 			describe_detached_head(_("HEAD is now at"), new_branch_info->commit);
 		}
 	} else if (new_branch_info->path) {	/* Switch branches. */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path, msg.buf, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path, 
+					msg.buf, NULL, false) < 0)
 			die(_("unable to update HEAD"));
 		if (!opts->quiet) {
 			if (old_branch_info->path && !strcmp(new_branch_info->path, old_branch_info->path)) {
@@ -1479,7 +1480,7 @@ static int switch_unborn_to_new_branch(const struct checkout_opts *opts)
 		die(_("You are on a branch yet to be born"));
 	strbuf_addf(&branch_ref, "refs/heads/%s", opts->new_branch);
 	status = refs_update_symref(get_main_ref_store(the_repository),
-				    "HEAD", branch_ref.buf, "checkout -b", NULL);
+				    "HEAD", branch_ref.buf, "checkout -b", NULL, false);
 	strbuf_release(&branch_ref);
 	if (!opts->quiet)
 		fprintf(stderr, _("Switched to a new branch '%s'\n"),
diff --git a/builtin/clone.c b/builtin/clone.c
index ead2af20ea..25d0bcf3aa 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -661,7 +661,7 @@ static void update_remote_refs(const struct ref *refs,
 		strbuf_addstr(&head_ref, "HEAD");
 		if (refs_update_symref(get_main_ref_store(the_repository), head_ref.buf,
 				       remote_head_points_at->peer_ref->name,
-				       msg, NULL) < 0)
+				       msg, NULL, false) < 0)
 			die(_("unable to update %s"), head_ref.buf);
 		strbuf_release(&head_ref);
 	}
@@ -673,7 +673,8 @@ static void update_head(const struct ref *our, const struct ref *remote,
 	const char *head;
 	if (our && skip_prefix(our->name, "refs/heads/", &head)) {
 		/* Local default branch link */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name, NULL, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name, 
+					NULL, NULL, false) < 0)
 			die(_("unable to update HEAD"));
 		if (!option_bare) {
 			refs_update_ref(get_main_ref_store(the_repository),
@@ -702,7 +703,8 @@ static void update_head(const struct ref *our, const struct ref *remote,
 		 * Unborn head from remote; same as "our" case above except
 		 * that we have no ref to update.
 		 */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn, NULL, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn, 
+					NULL, NULL, false) < 0)
 			die(_("unable to update HEAD"));
 		if (!option_bare)
 			install_branch_config(0, head, remote_name, unborn);
diff --git a/builtin/notes.c b/builtin/notes.c
index ba646f06ff..fb49e491c3 100644
--- a/builtin/notes.c
+++ b/builtin/notes.c
@@ -980,7 +980,8 @@ static int merge(int argc, const char **argv, const char *prefix)
 			die(_("a notes merge into %s is already in-progress at %s"),
 			    notes_ref, wt->path);
 		free_worktrees(worktrees);
-		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", notes_ref, NULL, NULL))
+		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", notes_ref, 
+					NULL, NULL, false))
 			die(_("failed to store link to current notes ref (%s)"),
 			    notes_ref);
 		fprintf(stderr, _("Automatic notes merge failed. Fix conflicts in %s "
diff --git a/builtin/remote.c b/builtin/remote.c
index c61a7800ff..a1cec6276d 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -244,7 +244,8 @@ static int add(int argc, const char **argv, const char *prefix)
 		strbuf_reset(&buf2);
 		strbuf_addf(&buf2, "refs/remotes/%s/%s", name, master);
 
-		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote add", NULL))
+		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf,
+					"remote add", NULL, false))
 			result = error(_("Could not setup master '%s'"), master);
 	}
 
@@ -864,7 +865,8 @@ static int mv(int argc, const char **argv, const char *prefix)
 		strbuf_reset(&buf3);
 		strbuf_addf(&buf3, "remote: renamed %s to %s",
 				item->string, buf.buf);
-		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, buf3.buf, NULL))
+		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf,
+					buf3.buf, NULL, false))
 			die(_("creating '%s' failed"), buf.buf);
 		display_progress(progress, ++refs_renamed_nr);
 	}
@@ -1466,7 +1468,8 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		/* make sure it's valid */
 		if (!refs_ref_exists(refs, buf2.buf))
 			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(refs, buf.buf, buf2.buf, "remote set-head", &buf_prev))
+		else if (refs_update_symref(refs, buf.buf, buf2.buf,
+					"remote set-head", &buf_prev, false))
 			result |= error(_("Could not setup %s"), buf.buf);
 		else if (opt_a) {
 			report_auto(argv[0], head_name, &buf_prev);
diff --git a/builtin/symbolic-ref.c b/builtin/symbolic-ref.c
index 7728fbc3c1..169f5f5340 100644
--- a/builtin/symbolic-ref.c
+++ b/builtin/symbolic-ref.c
@@ -88,7 +88,7 @@ int cmd_symbolic_ref(int argc,
 		if (check_refname_format(argv[1], REFNAME_ALLOW_ONELEVEL) < 0)
 			die("Refusing to set '%s' to invalid ref '%s'", argv[0], argv[1]);
 		ret = !!refs_update_symref(get_main_ref_store(the_repository),
-					   argv[0], argv[1], msg, NULL);
+					   argv[0], argv[1], msg, NULL, false);
 		break;
 	default:
 		usage_with_options(git_symbolic_ref_usage, options);
diff --git a/builtin/worktree.c b/builtin/worktree.c
index a7ab4193c1..46b515a243 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -517,7 +517,7 @@ static int add_worktree(const char *path, const char *refname,
 		ret = refs_update_ref(wt_refs, NULL, "HEAD", &commit->object.oid,
 				      NULL, 0, UPDATE_REFS_MSG_ON_ERR);
 	else
-		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, NULL, NULL);
+		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, NULL, NULL, false);
 	if (ret)
 		goto done;
 
diff --git a/refs.c b/refs.c
index 301db0dcdc..e012b89ee9 100644
--- a/refs.c
+++ b/refs.c
@@ -2115,19 +2115,32 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
 
 int refs_update_symref(struct ref_store *refs, const char *ref,
 		       const char *target, const char *logmsg,
-		       struct strbuf *before_target)
+		       struct strbuf *before_target, bool create_only)
 {
 	struct ref_transaction *transaction;
 	struct strbuf err = STRBUF_INIT;
-	int ret = 0;
+	int ret = 0, create_ret = 0;
 
 	transaction = ref_store_transaction_begin(refs, &err);
-	if (!transaction ||
-	    ref_transaction_update(transaction, ref, NULL, NULL,
-				   target, NULL, REF_NO_DEREF,
-				   logmsg, &err) ||
-	    ref_transaction_commit(transaction, &err)) {
-		ret = error("%s", err.buf);
+	if (create_only) {
+		if (!transaction ||
+		    ref_transaction_create(transaction, ref, NULL, target,
+					   REF_NO_DEREF, logmsg, &err)) {
+			ret = error("%s", err.buf);
+		}
+		else {
+		    create_ret = ref_transaction_commit(transaction, &err);
+		    if (create_ret && create_ret != TRANSACTION_CREATE_EXISTS)
+			ret = error("%s", err.buf);
+		}
+	}
+	else
+		if (!transaction ||
+		    ref_transaction_update(transaction, ref, NULL, NULL,
+					   target, NULL, REF_NO_DEREF,
+					   logmsg, &err) ||
+		    ref_transaction_commit(transaction, &err)) {
+			ret = error("%s", err.buf);
 	}
 
 	strbuf_release(&err);
diff --git a/refs.h b/refs.h
index 166affbc89..b64fd2318b 100644
--- a/refs.h
+++ b/refs.h
@@ -572,7 +572,7 @@ int refs_copy_existing_ref(struct ref_store *refs, const char *oldref,
 
 int refs_update_symref(struct ref_store *refs, const char *refname,
 		       const char *target, const char *logmsg,
-		       struct strbuf *before_target);
+		       struct strbuf *before_target, bool create_only);
 
 enum action_on_err {
 	UPDATE_REFS_MSG_ON_ERR,
diff --git a/reset.c b/reset.c
index cc36a9ed56..674896fb61 100644
--- a/reset.c
+++ b/reset.c
@@ -76,7 +76,7 @@ static int update_refs(const struct reset_head_opts *opts,
 		if (!ret)
 			ret = refs_update_symref(get_main_ref_store(the_repository),
 						 "HEAD", switch_to_branch,
-						 reflog_head, NULL);
+						 reflog_head, NULL, false);
 	}
 	if (!ret && run_hook)
 		run_hooks_l(the_repository, "post-checkout",
diff --git a/sequencer.c b/sequencer.c
index 23b162924c..1a46ef56ba 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -5107,7 +5107,8 @@ static int pick_commits(struct repository *r,
 			}
 			msg = reflog_message(opts, "finish", "returning to %s",
 				head_ref.buf);
-			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf, msg, NULL)) {
+			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf, 
+						msg, NULL, false)) {
 				res = error(_("could not update HEAD to %s"),
 					head_ref.buf);
 				goto cleanup_head_ref;
diff --git a/setup.c b/setup.c
index d95f051465..67abbfaf3c 100644
--- a/setup.c
+++ b/setup.c
@@ -2275,7 +2275,8 @@ void create_reference_database(enum ref_storage_format ref_storage_format,
 			die(_("invalid initial branch name: '%s'"),
 			    initial_branch);
 
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref, NULL, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref, 
+					NULL, NULL, false) < 0)
 			exit(1);
 		free(ref);
 	}
diff --git a/t/helper/test-ref-store.c b/t/helper/test-ref-store.c
index a911302bea..b6b06bb2e9 100644
--- a/t/helper/test-ref-store.c
+++ b/t/helper/test-ref-store.c
@@ -120,7 +120,7 @@ static int cmd_create_symref(struct ref_store *refs, const char **argv)
 	const char *target = notnull(*argv++, "target");
 	const char *logmsg = *argv++;
 
-	return refs_update_symref(refs, refname, target, logmsg, NULL);
+	return refs_update_symref(refs, refname, target, logmsg, NULL, false);
 }
 
 static struct flag_definition transaction_flags[] = {
-- 
2.47.0.rc0.5.gf1cffeb8df.dirty
Junio C Hamano· Oct 3, 2024, 19:21 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v4 0/5] improve handling of remote/HEAD

Bence Ferdinandy <bence@ferdinandy.com> writes:
Show 27 quoted lines
> Bence Ferdinandy (5):
>   refs_update_symref: atomically record overwritten ref
>   set-head: better output for --auto
>   transaction: add TRANSACTION_CREATE_EXISTS error
>   refs_update_symref: add create_only option
>   fetch: set remote/HEAD if it does not exist
>
>  builtin/branch.c          |  2 +-
>  builtin/checkout.c        |  5 ++-
>  builtin/clone.c           |  8 ++--
>  builtin/fetch.c           | 83 +++++++++++++++++++++++++++++++++++++++
>  builtin/notes.c           |  3 +-
>  builtin/remote.c          | 43 ++++++++++++++++----
>  builtin/symbolic-ref.c    |  2 +-
>  builtin/worktree.c        |  2 +-
>  refs.c                    | 35 ++++++++++++-----
>  refs.h                    |  7 +++-
>  refs/files-backend.c      | 29 ++++++++++----
>  refs/refs-internal.h      |  8 ++++
>  refs/reftable-backend.c   |  6 ++-
>  reset.c                   |  2 +-
>  sequencer.c               |  3 +-
>  setup.c                   |  3 +-
>  t/helper/test-ref-store.c |  2 +-
>  t/t5505-remote.sh         | 13 +++++-
>  t/t5514-fetch-multiple.sh |  9 +++++
>  19 files changed, 222 insertions(+), 43 deletions(-)

These seem to break some of the tests, either standalone or when merged to 'seen'.

I have the topic queued directly on top of e9356ba3 (another batch after 2.47-rc0, 2024-09-30); here is how the summary report looks like.

Thanks.
Test Summary Report
-------------------
t0410-partial-clone.sh                           (Wstat: 256 (exited 1) Tests: 38 Failed: 1)
  Failed test:  32
  Non-zero exit status: 1
t4207-log-decoration-colors.sh                   (Wstat: 256 (exited 1) Tests: 4 Failed: 1)
  Failed test:  2
  Non-zero exit status: 1
t5300-pack-object.sh                             (Wstat: 256 (exited 1) Tests: 56 Failed: 1)
  Failed test:  51
  Non-zero exit status: 1
t5512-ls-remote.sh                               (Wstat: 256 (exited 1) Tests: 40 Failed: 1)
  Failed test:  32
  Non-zero exit status: 1
t5527-fetch-odd-refs.sh                          (Wstat: 256 (exited 1) Tests: 5 Failed: 1)
  Failed test:  4
  Non-zero exit status: 1
t5514-fetch-multiple.sh                          (Wstat: 256 (exited 1) Tests: 25 Failed: 4)
  Failed tests:  9-10, 18, 24
  Non-zero exit status: 1
t5521-pull-options.sh                            (Wstat: 256 (exited 1) Tests: 22 Failed: 8)
  Failed tests:  2-9
  Non-zero exit status: 1
t5505-remote.sh                                  (Wstat: 256 (exited 1) Tests: 114 Failed: 7)
  Failed tests:  4, 31, 51-52, 54-56
  Non-zero exit status: 1
t5574-fetch-output.sh                            (Wstat: 256 (exited 1) Tests: 14 Failed: 3)
  Failed tests:  11-13
  Non-zero exit status: 1
t5703-upload-pack-ref-in-want.sh                 (Wstat: 256 (exited 1) Tests: 26 Failed: 1)
  Failed test:  12
  Non-zero exit status: 1
t5616-partial-clone.sh                           (Wstat: 256 (exited 1) Tests: 46 Failed: 2)
  Failed tests:  10, 22
  Non-zero exit status: 1
t5516-fetch-push.sh                              (Wstat: 256 (exited 1) Tests: 120 Failed: 1)
  Failed test:  102
  Non-zero exit status: 1
t7814-grep-recurse-submodules.sh                 (Wstat: 256 (exited 1) Tests: 34 Failed: 1)
  Failed test:  33
  Non-zero exit status: 1
t7900-maintenance.sh                             (Wstat: 256 (exited 1) Tests: 53 Failed: 1)
  Failed test:  23
  Non-zero exit status: 1
t7406-submodule-update.sh                        (Wstat: 256 (exited 1) Tests: 68 Failed: 1)
  Failed test:  59
  Non-zero exit status: 1
t9211-scalar-clone.sh                            (Wstat: 256 (exited 1) Tests: 13 Failed: 2)
  Failed tests:  8-9
  Non-zero exit status: 1
t9210-scalar.sh                                  (Wstat: 256 (exited 1) Tests: 21 Failed: 1)
  Failed test:  11
  Non-zero exit status: 1
t9902-completion.sh                              (Wstat: 256 (exited 1) Tests: 256 Failed: 56)
  Failed tests:  54-55, 59, 61, 63, 71-75, 77, 86, 88-89
                92-93, 95, 101, 113, 116-117, 119-129, 138-141
                143-157, 162-163, 168, 170-171
  Non-zero exit status: 1
Files=1029, Tests=31709, 371 wallclock secs (11.74 usr  4.74 sys + 853.87 cusr 6465.88 csys = 7336.23 CPU)
Result: FAIL
Bence Ferdinandy· Oct 3, 2024, 19:48 UTC · re: Junio C Hamano · lore

Re: [PATCH v4 0/5] improve handling of remote/HEAD

2024. okt. 3. 21:22:13 Junio C Hamano <gitster@pobox.com>:
Show 38 quoted lines
> Bence Ferdinandy <bence@ferdinandy.com> writes:
>
>> Bence Ferdinandy (5):
>>   refs_update_symref: atomically record overwritten ref
>>   set-head: better output for --auto
>>   transaction: add TRANSACTION_CREATE_EXISTS error
>>   refs_update_symref: add create_only option
>>   fetch: set remote/HEAD if it does not exist
>>
>> builtin/branch.c          |  2 +-
>> builtin/checkout.c        |  5 ++-
>> builtin/clone.c           |  8 ++--
>> builtin/fetch.c           | 83 +++++++++++++++++++++++++++++++++++++++
>> builtin/notes.c           |  3 +-
>> builtin/remote.c          | 43 ++++++++++++++++----
>> builtin/symbolic-ref.c    |  2 +-
>> builtin/worktree.c        |  2 +-
>> refs.c                    | 35 ++++++++++++-----
>> refs.h                    |  7 +++-
>> refs/files-backend.c      | 29 ++++++++++----
>> refs/refs-internal.h      |  8 ++++
>> refs/reftable-backend.c   |  6 ++-
>> reset.c                   |  2 +-
>> sequencer.c               |  3 +-
>> setup.c                   |  3 +-
>> t/helper/test-ref-store.c |  2 +-
>> t/t5505-remote.sh         | 13 +++++-
>> t/t5514-fetch-multiple.sh |  9 +++++
>> 19 files changed, 222 insertions(+), 43 deletions(-)
>
> These seem to break some of the tests, either standalone or when
> merged to 'seen'.
>
> I have the topic queued directly on top of e9356ba3 (another batch
> after 2.47-rc0, 2024-09-30); here is how the summary report looks
> like.
>
> Thanks.

Sorry, I should have probably left in the RFC prefix to be more clear about this... As I noted in the cover letter, the first four patches pass the tests, the last one doesn't (yet), as I'm still not quite sure if even the approach is fine. If those look good I'll send a v5 and also fix the tests as well. Although based on your review of the first 2 patches there's already some changes needed there, so I can just do a v5 with that and tests fixed and see if that version looks good or not.

I'm currently on a holiday, so my ETA on this is early next week.

Thanks, Bence

Show 80 quoted lines
>
> Test Summary Report
> -------------------
> t0410-partial-clone.sh                           (Wstat: 256 (exited 1) 
> Tests: 38 Failed: 1)
>   Failed test:  32
>   Non-zero exit status: 1
> t4207-log-decoration-colors.sh                   (Wstat: 256 (exited 1) 
> Tests: 4 Failed: 1)
>   Failed test:  2
>   Non-zero exit status: 1
> t5300-pack-object.sh                             (Wstat: 256 (exited 1) 
> Tests: 56 Failed: 1)
>   Failed test:  51
>   Non-zero exit status: 1
> t5512-ls-remote.sh                               (Wstat: 256 (exited 1) 
> Tests: 40 Failed: 1)
>   Failed test:  32
>   Non-zero exit status: 1
> t5527-fetch-odd-refs.sh                          (Wstat: 256 (exited 1) 
> Tests: 5 Failed: 1)
>   Failed test:  4
>   Non-zero exit status: 1
> t5514-fetch-multiple.sh                          (Wstat: 256 (exited 1) 
> Tests: 25 Failed: 4)
>   Failed tests:  9-10, 18, 24
>   Non-zero exit status: 1
> t5521-pull-options.sh                            (Wstat: 256 (exited 1) 
> Tests: 22 Failed: 8)
>   Failed tests:  2-9
>   Non-zero exit status: 1
> t5505-remote.sh                                  (Wstat: 256 (exited 1) 
> Tests: 114 Failed: 7)
>   Failed tests:  4, 31, 51-52, 54-56
>   Non-zero exit status: 1
> t5574-fetch-output.sh                            (Wstat: 256 (exited 1) 
> Tests: 14 Failed: 3)
>   Failed tests:  11-13
>   Non-zero exit status: 1
> t5703-upload-pack-ref-in-want.sh                 (Wstat: 256 (exited 1) 
> Tests: 26 Failed: 1)
>   Failed test:  12
>   Non-zero exit status: 1
> t5616-partial-clone.sh                           (Wstat: 256 (exited 1) 
> Tests: 46 Failed: 2)
>   Failed tests:  10, 22
>   Non-zero exit status: 1
> t5516-fetch-push.sh                              (Wstat: 256 (exited 1) 
> Tests: 120 Failed: 1)
>   Failed test:  102
>   Non-zero exit status: 1
> t7814-grep-recurse-submodules.sh                 (Wstat: 256 (exited 1) 
> Tests: 34 Failed: 1)
>   Failed test:  33
>   Non-zero exit status: 1
> t7900-maintenance.sh                             (Wstat: 256 (exited 1) 
> Tests: 53 Failed: 1)
>   Failed test:  23
>   Non-zero exit status: 1
> t7406-submodule-update.sh                        (Wstat: 256 (exited 1) 
> Tests: 68 Failed: 1)
>   Failed test:  59
>   Non-zero exit status: 1
> t9211-scalar-clone.sh                            (Wstat: 256 (exited 1) 
> Tests: 13 Failed: 2)
>   Failed tests:  8-9
>   Non-zero exit status: 1
> t9210-scalar.sh                                  (Wstat: 256 (exited 1) 
> Tests: 21 Failed: 1)
>   Failed test:  11
>   Non-zero exit status: 1
> t9902-completion.sh                              (Wstat: 256 (exited 1) 
> Tests: 256 Failed: 56)
>   Failed tests:  54-55, 59, 61, 63, 71-75, 77, 86, 88-89
>                 92-93, 95, 101, 113, 116-117, 119-129, 138-141
>                 143-157, 162-163, 168, 170-171
>   Non-zero exit status: 1
> Files=1029, Tests=31709, 371 wallclock secs (11.74 usr  4.74 sys + 
> 853.87 cusr 6465.88 csys = 7336.23 CPU)
> Result: FAIL
Bence Ferdinandy· Oct 9, 2024, 13:57 UTC · re: Junio C Hamano · lore

[PATCH v5 1/6] refs_update_symref: atomically record overwritten ref

When updating a symref it's currently not possible to know for sure what was the previous value that was overwritten. Make use of ref_transaction's atomicity and record the previous value there. Add a new variable to refs_update_symref's signature to be able to pass this information back up to callers.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    v5: - added before_target to reftables backend
        - added an extra safety check for transaction's existence in refs.c
 builtin/branch.c          | 2 +-
 builtin/checkout.c        | 4 ++--
 builtin/clone.c           | 6 +++---
 builtin/notes.c           | 2 +-
 builtin/remote.c          | 6 +++---
 builtin/symbolic-ref.c    | 2 +-
 builtin/worktree.c        | 2 +-
 refs.c                    | 8 ++++++--
 refs.h                    | 3 ++-
 refs/files-backend.c      | 1 +
 refs/refs-internal.h      | 8 ++++++++
 refs/reftable-backend.c   | 1 +
 reset.c                   | 2 +-
 sequencer.c               | 2 +-
 setup.c                   | 2 +-
 t/helper/test-ref-store.c | 2 +-
 16 files changed, 34 insertions(+), 19 deletions(-)
Show changes to 16 files +34 −19

builtin/branch.c, builtin/checkout.c, builtin/clone.c, builtin/notes.c, builtin/remote.c, builtin/symbolic-ref.c, builtin/worktree.c, refs.c, refs.h, refs/files-backend.c, refs/refs-internal.h, refs/reftable-backend.c, reset.c, sequencer.c, setup.c, t/helper/test-ref-store.c

diff --git a/builtin/branch.c b/builtin/branch.c
index fd1611ebf5..6c87690b58 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -559,7 +559,7 @@ static int replace_each_worktree_head_symref(struct worktree **worktrees,
 			continue;
 
 		refs = get_worktree_ref_store(worktrees[i]);
-		if (refs_update_symref(refs, "HEAD", newref, logmsg))
+		if (refs_update_symref(refs, "HEAD", newref, logmsg, NULL))
 			ret = error(_("HEAD of working tree %s is not updated"),
 				    worktrees[i]->path);
 	}
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 9c30000d3a..356ee9bcde 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -1015,7 +1015,7 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 			describe_detached_head(_("HEAD is now at"), new_branch_info->commit);
 		}
 	} else if (new_branch_info->path) {	/* Switch branches. */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path, msg.buf) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path, msg.buf, NULL) < 0)
 			die(_("unable to update HEAD"));
 		if (!opts->quiet) {
 			if (old_branch_info->path && !strcmp(new_branch_info->path, old_branch_info->path)) {
@@ -1479,7 +1479,7 @@ static int switch_unborn_to_new_branch(const struct checkout_opts *opts)
 		die(_("You are on a branch yet to be born"));
 	strbuf_addf(&branch_ref, "refs/heads/%s", opts->new_branch);
 	status = refs_update_symref(get_main_ref_store(the_repository),
-				    "HEAD", branch_ref.buf, "checkout -b");
+				    "HEAD", branch_ref.buf, "checkout -b", NULL);
 	strbuf_release(&branch_ref);
 	if (!opts->quiet)
 		fprintf(stderr, _("Switched to a new branch '%s'\n"),
diff --git a/builtin/clone.c b/builtin/clone.c
index e77339c847..ead2af20ea 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -661,7 +661,7 @@ static void update_remote_refs(const struct ref *refs,
 		strbuf_addstr(&head_ref, "HEAD");
 		if (refs_update_symref(get_main_ref_store(the_repository), head_ref.buf,
 				       remote_head_points_at->peer_ref->name,
-				       msg) < 0)
+				       msg, NULL) < 0)
 			die(_("unable to update %s"), head_ref.buf);
 		strbuf_release(&head_ref);
 	}
@@ -673,7 +673,7 @@ static void update_head(const struct ref *our, const struct ref *remote,
 	const char *head;
 	if (our && skip_prefix(our->name, "refs/heads/", &head)) {
 		/* Local default branch link */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name, NULL, NULL) < 0)
 			die(_("unable to update HEAD"));
 		if (!option_bare) {
 			refs_update_ref(get_main_ref_store(the_repository),
@@ -702,7 +702,7 @@ static void update_head(const struct ref *our, const struct ref *remote,
 		 * Unborn head from remote; same as "our" case above except
 		 * that we have no ref to update.
 		 */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn, NULL, NULL) < 0)
 			die(_("unable to update HEAD"));
 		if (!option_bare)
 			install_branch_config(0, head, remote_name, unborn);
diff --git a/builtin/notes.c b/builtin/notes.c
index 8c26e45526..ba646f06ff 100644
--- a/builtin/notes.c
+++ b/builtin/notes.c
@@ -980,7 +980,7 @@ static int merge(int argc, const char **argv, const char *prefix)
 			die(_("a notes merge into %s is already in-progress at %s"),
 			    notes_ref, wt->path);
 		free_worktrees(worktrees);
-		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", notes_ref, NULL))
+		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", notes_ref, NULL, NULL))
 			die(_("failed to store link to current notes ref (%s)"),
 			    notes_ref);
 		fprintf(stderr, _("Automatic notes merge failed. Fix conflicts in %s "
diff --git a/builtin/remote.c b/builtin/remote.c
index 76670ddd8b..d8ff440027 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -244,7 +244,7 @@ static int add(int argc, const char **argv, const char *prefix)
 		strbuf_reset(&buf2);
 		strbuf_addf(&buf2, "refs/remotes/%s/%s", name, master);
 
-		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote add"))
+		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote add", NULL))
 			result = error(_("Could not setup master '%s'"), master);
 	}
 
@@ -864,7 +864,7 @@ static int mv(int argc, const char **argv, const char *prefix)
 		strbuf_reset(&buf3);
 		strbuf_addf(&buf3, "remote: renamed %s to %s",
 				item->string, buf.buf);
-		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, buf3.buf))
+		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, buf3.buf, NULL))
 			die(_("creating '%s' failed"), buf.buf);
 		display_progress(progress, ++refs_renamed_nr);
 	}
@@ -1444,7 +1444,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		/* make sure it's valid */
 		if (!refs_ref_exists(get_main_ref_store(the_repository), buf2.buf))
 			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head"))
+		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head", NULL))
 			result |= error(_("Could not setup %s"), buf.buf);
 		else if (opt_a)
 			printf("%s/HEAD set to %s\n", argv[0], head_name);
diff --git a/builtin/symbolic-ref.c b/builtin/symbolic-ref.c
index 299d23d76a..7728fbc3c1 100644
--- a/builtin/symbolic-ref.c
+++ b/builtin/symbolic-ref.c
@@ -88,7 +88,7 @@ int cmd_symbolic_ref(int argc,
 		if (check_refname_format(argv[1], REFNAME_ALLOW_ONELEVEL) < 0)
 			die("Refusing to set '%s' to invalid ref '%s'", argv[0], argv[1]);
 		ret = !!refs_update_symref(get_main_ref_store(the_repository),
-					   argv[0], argv[1], msg);
+					   argv[0], argv[1], msg, NULL);
 		break;
 	default:
 		usage_with_options(git_symbolic_ref_usage, options);
diff --git a/builtin/worktree.c b/builtin/worktree.c
index fc31d072a6..a7ab4193c1 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -517,7 +517,7 @@ static int add_worktree(const char *path, const char *refname,
 		ret = refs_update_ref(wt_refs, NULL, "HEAD", &commit->object.oid,
 				      NULL, 0, UPDATE_REFS_MSG_ON_ERR);
 	else
-		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, NULL);
+		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, NULL, NULL);
 	if (ret)
 		goto done;
 
diff --git a/refs.c b/refs.c
index 5f729ed412..91cacee6f9 100644
--- a/refs.c
+++ b/refs.c
@@ -2114,7 +2114,8 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
 }
 
 int refs_update_symref(struct ref_store *refs, const char *ref,
-		       const char *target, const char *logmsg)
+		       const char *target, const char *logmsg,
+		       struct strbuf *before_target)
 {
 	struct ref_transaction *transaction;
 	struct strbuf err = STRBUF_INIT;
@@ -2130,6 +2131,10 @@ int refs_update_symref(struct ref_store *refs, const char *ref,
 	}
 
 	strbuf_release(&err);
+
+	if (transaction && before_target && transaction->updates[0]->before_target)
+		strbuf_addstr(before_target, transaction->updates[0]->before_target);
+
 	if (transaction)
 		ref_transaction_free(transaction);
 
@@ -2948,4 +2953,3 @@ int ref_update_expects_existing_old_ref(struct ref_update *update)
 	return (update->flags & REF_HAVE_OLD) &&
 		(!is_null_oid(&update->old_oid) || update->old_target);
 }
-
diff --git a/refs.h b/refs.h
index 108dfc93b3..f38616db84 100644
--- a/refs.h
+++ b/refs.h
@@ -571,7 +571,8 @@ int refs_copy_existing_ref(struct ref_store *refs, const char *oldref,
 		    const char *newref, const char *logmsg);
 
 int refs_update_symref(struct ref_store *refs, const char *refname,
-		       const char *target, const char *logmsg);
+		       const char *target, const char *logmsg,
+		       struct strbuf *before_target);
 
 enum action_on_err {
 	UPDATE_REFS_MSG_ON_ERR,
diff --git a/refs/files-backend.c b/refs/files-backend.c
index 0824c0b8a9..8415f2d020 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -2577,6 +2577,7 @@ static int lock_ref_for_update(struct files_ref_store *refs,
 	}
 
 	update->backend_data = lock;
+	update->before_target = xstrdup_or_null(referent.buf);
 
 	if (update->type & REF_ISSYMREF) {
 		if (update->flags & REF_NO_DEREF) {
diff --git a/refs/refs-internal.h b/refs/refs-internal.h
index 2313c830d8..7df3e6271e 100644
--- a/refs/refs-internal.h
+++ b/refs/refs-internal.h
@@ -104,6 +104,14 @@ struct ref_update {
 	 */
 	const char *old_target;
 
+	/*
+	 * The previous target before applying new_target will be
+	 * written here, to be used by callers when they do not want to
+	 * check old_target during the transaction, but do want to know
+	 * what it was.
+	 */
+	const char *before_target;
+
 	/*
 	 * One or more of REF_NO_DEREF, REF_FORCE_CREATE_REFLOG,
 	 * REF_HAVE_NEW, REF_HAVE_OLD, or backend-specific flags.
diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
index 3c96fbf66f..32330b6bc6 100644
--- a/refs/reftable-backend.c
+++ b/refs/reftable-backend.c
@@ -1244,6 +1244,7 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 			if (ret)
 				goto done;
 		}
+		u->before_target = xstrdup_or_null(referent.buf);
 	}
 
 	transaction->backend_data = tx_data;
diff --git a/reset.c b/reset.c
index b22b1be792..cc36a9ed56 100644
--- a/reset.c
+++ b/reset.c
@@ -76,7 +76,7 @@ static int update_refs(const struct reset_head_opts *opts,
 		if (!ret)
 			ret = refs_update_symref(get_main_ref_store(the_repository),
 						 "HEAD", switch_to_branch,
-						 reflog_head);
+						 reflog_head, NULL);
 	}
 	if (!ret && run_hook)
 		run_hooks_l(the_repository, "post-checkout",
diff --git a/sequencer.c b/sequencer.c
index 8d01cd50ac..23b162924c 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -5107,7 +5107,7 @@ static int pick_commits(struct repository *r,
 			}
 			msg = reflog_message(opts, "finish", "returning to %s",
 				head_ref.buf);
-			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf, msg)) {
+			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf, msg, NULL)) {
 				res = error(_("could not update HEAD to %s"),
 					head_ref.buf);
 				goto cleanup_head_ref;
diff --git a/setup.c b/setup.c
index 94e79b2e48..d95f051465 100644
--- a/setup.c
+++ b/setup.c
@@ -2275,7 +2275,7 @@ void create_reference_database(enum ref_storage_format ref_storage_format,
 			die(_("invalid initial branch name: '%s'"),
 			    initial_branch);
 
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref, NULL, NULL) < 0)
 			exit(1);
 		free(ref);
 	}
diff --git a/t/helper/test-ref-store.c b/t/helper/test-ref-store.c
index 65346dee55..a911302bea 100644
--- a/t/helper/test-ref-store.c
+++ b/t/helper/test-ref-store.c
@@ -120,7 +120,7 @@ static int cmd_create_symref(struct ref_store *refs, const char **argv)
 	const char *target = notnull(*argv++, "target");
 	const char *logmsg = *argv++;
 
-	return refs_update_symref(refs, refname, target, logmsg);
+	return refs_update_symref(refs, refname, target, logmsg, NULL);
 }
 
 static struct flag_definition transaction_flags[] = {
-- 
2.47.0.6.g9542df9767
Bence Ferdinandy· Oct 9, 2024, 13:57 UTC · re: Bence Ferdinandy · lore

[PATCH v5 5/6] refs_update_symref: add create_only option

Allow the caller to specify that it only wants to update the symref if it does not already exist. Silently ignore the error from the transaction API if the symref already exists.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    v5: no change
 builtin/branch.c          |  2 +-
 builtin/checkout.c        |  5 +++--
 builtin/clone.c           |  8 +++++---
 builtin/notes.c           |  3 ++-
 builtin/remote.c          |  9 ++++++---
 builtin/symbolic-ref.c    |  2 +-
 builtin/worktree.c        |  2 +-
 refs.c                    | 29 +++++++++++++++++++++--------
 refs.h                    |  2 +-
 reset.c                   |  2 +-
 sequencer.c               |  3 ++-
 setup.c                   |  3 ++-
 t/helper/test-ref-store.c |  2 +-
 13 files changed, 47 insertions(+), 25 deletions(-)
Show changes to 13 files +47 −25

builtin/branch.c, builtin/checkout.c, builtin/clone.c, builtin/notes.c, builtin/remote.c, builtin/symbolic-ref.c, builtin/worktree.c, refs.c, refs.h, reset.c, sequencer.c, setup.c, t/helper/test-ref-store.c

diff --git a/builtin/branch.c b/builtin/branch.c
index 6c87690b58..3c9bc39800 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -559,7 +559,7 @@ static int replace_each_worktree_head_symref(struct worktree **worktrees,
 			continue;
 
 		refs = get_worktree_ref_store(worktrees[i]);
-		if (refs_update_symref(refs, "HEAD", newref, logmsg, NULL))
+		if (refs_update_symref(refs, "HEAD", newref, logmsg, NULL, false))
 			ret = error(_("HEAD of working tree %s is not updated"),
 				    worktrees[i]->path);
 	}
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 356ee9bcde..d9514e9b7a 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -1015,7 +1015,8 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 			describe_detached_head(_("HEAD is now at"), new_branch_info->commit);
 		}
 	} else if (new_branch_info->path) {	/* Switch branches. */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path, msg.buf, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path, 
+					msg.buf, NULL, false) < 0)
 			die(_("unable to update HEAD"));
 		if (!opts->quiet) {
 			if (old_branch_info->path && !strcmp(new_branch_info->path, old_branch_info->path)) {
@@ -1479,7 +1480,7 @@ static int switch_unborn_to_new_branch(const struct checkout_opts *opts)
 		die(_("You are on a branch yet to be born"));
 	strbuf_addf(&branch_ref, "refs/heads/%s", opts->new_branch);
 	status = refs_update_symref(get_main_ref_store(the_repository),
-				    "HEAD", branch_ref.buf, "checkout -b", NULL);
+				    "HEAD", branch_ref.buf, "checkout -b", NULL, false);
 	strbuf_release(&branch_ref);
 	if (!opts->quiet)
 		fprintf(stderr, _("Switched to a new branch '%s'\n"),
diff --git a/builtin/clone.c b/builtin/clone.c
index ead2af20ea..25d0bcf3aa 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -661,7 +661,7 @@ static void update_remote_refs(const struct ref *refs,
 		strbuf_addstr(&head_ref, "HEAD");
 		if (refs_update_symref(get_main_ref_store(the_repository), head_ref.buf,
 				       remote_head_points_at->peer_ref->name,
-				       msg, NULL) < 0)
+				       msg, NULL, false) < 0)
 			die(_("unable to update %s"), head_ref.buf);
 		strbuf_release(&head_ref);
 	}
@@ -673,7 +673,8 @@ static void update_head(const struct ref *our, const struct ref *remote,
 	const char *head;
 	if (our && skip_prefix(our->name, "refs/heads/", &head)) {
 		/* Local default branch link */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name, NULL, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name, 
+					NULL, NULL, false) < 0)
 			die(_("unable to update HEAD"));
 		if (!option_bare) {
 			refs_update_ref(get_main_ref_store(the_repository),
@@ -702,7 +703,8 @@ static void update_head(const struct ref *our, const struct ref *remote,
 		 * Unborn head from remote; same as "our" case above except
 		 * that we have no ref to update.
 		 */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn, NULL, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn, 
+					NULL, NULL, false) < 0)
 			die(_("unable to update HEAD"));
 		if (!option_bare)
 			install_branch_config(0, head, remote_name, unborn);
diff --git a/builtin/notes.c b/builtin/notes.c
index ba646f06ff..fb49e491c3 100644
--- a/builtin/notes.c
+++ b/builtin/notes.c
@@ -980,7 +980,8 @@ static int merge(int argc, const char **argv, const char *prefix)
 			die(_("a notes merge into %s is already in-progress at %s"),
 			    notes_ref, wt->path);
 		free_worktrees(worktrees);
-		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", notes_ref, NULL, NULL))
+		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", notes_ref, 
+					NULL, NULL, false))
 			die(_("failed to store link to current notes ref (%s)"),
 			    notes_ref);
 		fprintf(stderr, _("Automatic notes merge failed. Fix conflicts in %s "
diff --git a/builtin/remote.c b/builtin/remote.c
index 2480128b88..79f22467f2 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -244,7 +244,8 @@ static int add(int argc, const char **argv, const char *prefix)
 		strbuf_reset(&buf2);
 		strbuf_addf(&buf2, "refs/remotes/%s/%s", name, master);
 
-		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote add", NULL))
+		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf,
+					"remote add", NULL, false))
 			result = error(_("Could not setup master '%s'"), master);
 	}
 
@@ -864,7 +865,8 @@ static int mv(int argc, const char **argv, const char *prefix)
 		strbuf_reset(&buf3);
 		strbuf_addf(&buf3, "remote: renamed %s to %s",
 				item->string, buf.buf);
-		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, buf3.buf, NULL))
+		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf,
+					buf3.buf, NULL, false))
 			die(_("creating '%s' failed"), buf.buf);
 		display_progress(progress, ++refs_renamed_nr);
 	}
@@ -1465,7 +1467,8 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		/* make sure it's valid */
 		if (!refs_ref_exists(refs, buf2.buf))
 			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(refs, buf.buf, buf2.buf, "remote set-head", &buf_prev))
+		else if (refs_update_symref(refs, buf.buf, buf2.buf,
+					"remote set-head", &buf_prev, false))
 			result |= error(_("Could not setup %s"), buf.buf);
 		else if (opt_a) {
 			report_auto(argv[0], head_name, &buf_prev);
diff --git a/builtin/symbolic-ref.c b/builtin/symbolic-ref.c
index 7728fbc3c1..169f5f5340 100644
--- a/builtin/symbolic-ref.c
+++ b/builtin/symbolic-ref.c
@@ -88,7 +88,7 @@ int cmd_symbolic_ref(int argc,
 		if (check_refname_format(argv[1], REFNAME_ALLOW_ONELEVEL) < 0)
 			die("Refusing to set '%s' to invalid ref '%s'", argv[0], argv[1]);
 		ret = !!refs_update_symref(get_main_ref_store(the_repository),
-					   argv[0], argv[1], msg, NULL);
+					   argv[0], argv[1], msg, NULL, false);
 		break;
 	default:
 		usage_with_options(git_symbolic_ref_usage, options);
diff --git a/builtin/worktree.c b/builtin/worktree.c
index a7ab4193c1..46b515a243 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -517,7 +517,7 @@ static int add_worktree(const char *path, const char *refname,
 		ret = refs_update_ref(wt_refs, NULL, "HEAD", &commit->object.oid,
 				      NULL, 0, UPDATE_REFS_MSG_ON_ERR);
 	else
-		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, NULL, NULL);
+		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, NULL, NULL, false);
 	if (ret)
 		goto done;
 
diff --git a/refs.c b/refs.c
index 91cacee6f9..3d2c07dd67 100644
--- a/refs.c
+++ b/refs.c
@@ -2115,19 +2115,32 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
 
 int refs_update_symref(struct ref_store *refs, const char *ref,
 		       const char *target, const char *logmsg,
-		       struct strbuf *before_target)
+		       struct strbuf *before_target, bool create_only)
 {
 	struct ref_transaction *transaction;
 	struct strbuf err = STRBUF_INIT;
-	int ret = 0;
+	int ret = 0, create_ret = 0;
 
 	transaction = ref_store_transaction_begin(refs, &err);
-	if (!transaction ||
-	    ref_transaction_update(transaction, ref, NULL, NULL,
-				   target, NULL, REF_NO_DEREF,
-				   logmsg, &err) ||
-	    ref_transaction_commit(transaction, &err)) {
-		ret = error("%s", err.buf);
+	if (create_only) {
+		if (!transaction ||
+		    ref_transaction_create(transaction, ref, NULL, target,
+					   REF_NO_DEREF, logmsg, &err)) {
+			ret = error("%s", err.buf);
+		}
+		else {
+		    create_ret = ref_transaction_commit(transaction, &err);
+		    if (create_ret && create_ret != TRANSACTION_CREATE_EXISTS)
+			ret = error("%s", err.buf);
+		}
+	}
+	else
+		if (!transaction ||
+		    ref_transaction_update(transaction, ref, NULL, NULL,
+					   target, NULL, REF_NO_DEREF,
+					   logmsg, &err) ||
+		    ref_transaction_commit(transaction, &err)) {
+			ret = error("%s", err.buf);
 	}
 
 	strbuf_release(&err);
diff --git a/refs.h b/refs.h
index 166affbc89..b64fd2318b 100644
--- a/refs.h
+++ b/refs.h
@@ -572,7 +572,7 @@ int refs_copy_existing_ref(struct ref_store *refs, const char *oldref,
 
 int refs_update_symref(struct ref_store *refs, const char *refname,
 		       const char *target, const char *logmsg,
-		       struct strbuf *before_target);
+		       struct strbuf *before_target, bool create_only);
 
 enum action_on_err {
 	UPDATE_REFS_MSG_ON_ERR,
diff --git a/reset.c b/reset.c
index cc36a9ed56..674896fb61 100644
--- a/reset.c
+++ b/reset.c
@@ -76,7 +76,7 @@ static int update_refs(const struct reset_head_opts *opts,
 		if (!ret)
 			ret = refs_update_symref(get_main_ref_store(the_repository),
 						 "HEAD", switch_to_branch,
-						 reflog_head, NULL);
+						 reflog_head, NULL, false);
 	}
 	if (!ret && run_hook)
 		run_hooks_l(the_repository, "post-checkout",
diff --git a/sequencer.c b/sequencer.c
index 23b162924c..1a46ef56ba 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -5107,7 +5107,8 @@ static int pick_commits(struct repository *r,
 			}
 			msg = reflog_message(opts, "finish", "returning to %s",
 				head_ref.buf);
-			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf, msg, NULL)) {
+			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf, 
+						msg, NULL, false)) {
 				res = error(_("could not update HEAD to %s"),
 					head_ref.buf);
 				goto cleanup_head_ref;
diff --git a/setup.c b/setup.c
index d95f051465..67abbfaf3c 100644
--- a/setup.c
+++ b/setup.c
@@ -2275,7 +2275,8 @@ void create_reference_database(enum ref_storage_format ref_storage_format,
 			die(_("invalid initial branch name: '%s'"),
 			    initial_branch);
 
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref, NULL, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref, 
+					NULL, NULL, false) < 0)
 			exit(1);
 		free(ref);
 	}
diff --git a/t/helper/test-ref-store.c b/t/helper/test-ref-store.c
index a911302bea..b6b06bb2e9 100644
--- a/t/helper/test-ref-store.c
+++ b/t/helper/test-ref-store.c
@@ -120,7 +120,7 @@ static int cmd_create_symref(struct ref_store *refs, const char **argv)
 	const char *target = notnull(*argv++, "target");
 	const char *logmsg = *argv++;
 
-	return refs_update_symref(refs, refname, target, logmsg, NULL);
+	return refs_update_symref(refs, refname, target, logmsg, NULL, false);
 }
 
 static struct flag_definition transaction_flags[] = {
-- 
2.47.0.6.g9542df9767
Junio C Hamano· Oct 9, 2024, 21:37 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v5 5/6] refs_update_symref: add create_only option

Bence Ferdinandy <bence@ferdinandy.com> writes:
Show 13 quoted lines
> diff --git a/builtin/branch.c b/builtin/branch.c
> index 6c87690b58..3c9bc39800 100644
> --- a/builtin/branch.c
> +++ b/builtin/branch.c
> @@ -559,7 +559,7 @@ static int replace_each_worktree_head_symref(struct worktree **worktrees,
>  			continue;
>  
>  		refs = get_worktree_ref_store(worktrees[i]);
> -		if (refs_update_symref(refs, "HEAD", newref, logmsg, NULL))
> +		if (refs_update_symref(refs, "HEAD", newref, logmsg, NULL, false))
>  			ret = error(_("HEAD of working tree %s is not updated"),
>  				    worktrees[i]->path);
>  	}

Didn't somebody tell you not to use "bool" in previous rounds of reviews?

We are still smoking out platforms that have trouble with the type by using it only in a tightly controlled weather balloon we raised in 8277dbe9 (git-compat-util: convert skip_{prefix,suffix}{,_mem} to bool, 2023-12-16), if I recall correctly.

Assuming either (1) I forgot that we are OK with bool everywhere, or (2) the patch will be updated to use plain 0 for false and everything else for true, let's keep reading. I as a reader expect that some *.h file will tell us what the new parameter means.

Show 15 quoted lines
> diff --git a/refs.c b/refs.c
> index 91cacee6f9..3d2c07dd67 100644
> --- a/refs.c
> +++ b/refs.c
> @@ -2115,19 +2115,32 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
>  
>  int refs_update_symref(struct ref_store *refs, const char *ref,
>  		       const char *target, const char *logmsg,
> -		       struct strbuf *before_target)
> +		       struct strbuf *before_target, bool create_only)
>  {
>  	struct ref_transaction *transaction;
>  	struct strbuf err = STRBUF_INIT;
> -	int ret = 0;
> +	int ret = 0, create_ret = 0;

Isn't it sufficient for create_ret to be in the scope deep in the nested if/else where it is used?

Show 21 quoted lines
>  	transaction = ref_store_transaction_begin(refs, &err);
> +	if (create_only) {
> +		if (!transaction ||
> +		    ref_transaction_create(transaction, ref, NULL, target,
> +					   REF_NO_DEREF, logmsg, &err)) {
> +			ret = error("%s", err.buf);
> +		}
> +		else {
> +		    create_ret = ref_transaction_commit(transaction, &err);
> +		    if (create_ret && create_ret != TRANSACTION_CREATE_EXISTS)
> +			ret = error("%s", err.buf);
> +		}
> +	}
> +	else
> +		if (!transaction ||
> +		    ref_transaction_update(transaction, ref, NULL, NULL,
> +					   target, NULL, REF_NO_DEREF,
> +					   logmsg, &err) ||
> +		    ref_transaction_commit(transaction, &err)) {
> +			ret = error("%s", err.buf);
>  	}
Style: in if/else if/... cascade, opening and closing {braces} sit
on the same line as these keywords, i.e.
	if (create_only) {
		if (...) {
			ret = error(...);
		} else {
			...
		}
	} else if (!transaction ||
			...) {
	}

But more importantly, I think it is much cleaner to handle failure from transaction_begin outside the if/else cascade. i.e.

	transaction = ref_store_transaction_begin(refs, &err);
	if (!transaction) {
        error_return:
		ret = error("%s", err.buf);
        	goto cleanup;
	}
        if (create_only) {
        	int create_ret;
		if (ref_transaction_create(...))
			goto error_return;
		create_ret = ref_transaction_commit(...);
                if (create_ret && create_ret != TRANSACTION_CREATE_EXISTS)
			goto error_return;
	} else {
		if (ref_transaction_update(...) ||
		    ref_transaction_commit(...))
			goto error_return;
	}
cleanup:
	strbuf_release(&err);
	if (transaction) {
		if (before_target && ...)
			strbuf_addstr(before_target, ...);
		ref_transaction_free(transaction);
	}
	return ret;

I didn't spot any caller that passes "true" to signal that it wants the create_only behaviour in this step.

Which is OK---let's keep reading to find such a caller in the next step.

Bence Ferdinandy· Oct 9, 2024, 13:57 UTC · re: Bence Ferdinandy · lore

[PATCH v5 6/6] fetch: set remote/HEAD if it does not exist

If the user has remote/HEAD set already and it looks like it has changed on the server, then print a message, otherwise set it if we can. Silently pass if the user already has the same remote/HEAD set as reported by the server.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v3: - does not rely on remote set-head anymore so it only authenticates
        once
        - uses the new REF_CREATE_ONLY to atomically check if the ref exists
          and only write it if it doesn't
        - in all other cases the maximum it does is print a warning
    
    v4: - instead of the discarded REF_CREATE_ONLY, it uses the existing,
          but updated transaction api to request a silent create only
        - it now uses the atomic before_target to determine reporting
        - refactored for legibility
    
    v5: - instead of printing a not too useful message, it now fails
          silently, this in line with the objective to only set up
          remote/HEAD automatically if the right thing is trivial, for
          everything else there is remote set-head
        - fixed all failing tests
        - added to new tests, one for checking if remote/HEAD is set to the
          correct one, and one to test that we do not override remote/HEAD
          if it has changed on the server from what we have locally
 builtin/fetch.c                  |  82 +++++++++++
 t/t4207-log-decoration-colors.sh |   3 +-
 t/t5505-remote.sh                |  11 +-
 t/t5510-fetch.sh                 | 229 ++++++++++++++++---------------
 t/t5512-ls-remote.sh             |   2 +
 t/t5514-fetch-multiple.sh        |  17 ++-
 t/t5516-fetch-push.sh            |   3 +-
 t/t5527-fetch-odd-refs.sh        |   3 +-
 t/t7900-maintenance.sh           |   3 +-
 t/t9210-scalar.sh                |   5 +-
 t/t9211-scalar-clone.sh          |   6 +-
 t/t9902-completion.sh            |  67 ++++++++-
 12 files changed, 308 insertions(+), 123 deletions(-)
Show changes to 12 files +308 −123

builtin/fetch.c, t/t4207-log-decoration-colors.sh, t/t5505-remote.sh, t/t5510-fetch.sh, t/t5512-ls-remote.sh, t/t5514-fetch-multiple.sh, t/t5516-fetch-push.sh, t/t5527-fetch-odd-refs.sh, t/t7900-maintenance.sh, t/t9210-scalar.sh, t/t9211-scalar-clone.sh, t/t9902-completion.sh

diff --git a/builtin/fetch.c b/builtin/fetch.c
index 80a64d0d26..c3d3c05950 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1578,6 +1578,82 @@ static int backfill_tags(struct display_state *display_state,
 	return retcode;
 }
 
+static void report_set_head(const char *remote, const char *head_name,
+			struct strbuf *buf_prev) {
+	struct strbuf buf_prefix = STRBUF_INIT;
+	const char *prev_head;
+
+	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
+	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
+
+	if (prev_head && !strcmp(prev_head, head_name))
+		;
+	else if (prev_head) {
+		printf("'HEAD' at '%s' has changed from '%s' to '%s'\n",
+			remote, prev_head, head_name);
+		printf("Run 'git remote set-head %s %s' to follow the change.\n",
+			remote, head_name);
+	}
+}
+
+static const char *strip_refshead(const char *name){
+	skip_prefix(name, "refs/heads/", &name);
+	return name;
+}
+
+static int set_head(const struct ref *remote_refs)
+{
+	int result = 0;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
+		b_local_head = STRBUF_INIT;
+	const char *remote = gtransport->remote->name;
+	char *head_name = NULL;
+	struct ref *ref, *matches;
+	struct ref *fetch_map = NULL, **fetch_map_tail = &fetch_map;
+	struct refspec_item refspec = {
+		.force = 0,
+		.pattern = 1,
+		.src = (char *) "refs/heads/*",
+		.dst = (char *) "refs/heads/*",
+	};
+	struct string_list heads = STRING_LIST_INIT_DUP;
+	struct ref_store *refs = get_main_ref_store(the_repository);
+
+	get_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);
+	matches = guess_remote_head(find_ref_by_name(remote_refs, "HEAD"),
+				    fetch_map, 1);
+	for (ref = matches; ref; ref = ref->next) {
+		string_list_append(&heads, strip_refshead(ref->name));
+	}
+
+
+	if (!heads.nr)
+		result = 1;
+	else if (heads.nr > 1) {
+		result = 1;
+	} else
+		head_name = xstrdup(heads.items[0].string);
+	if (head_name) {
+		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
+		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote, head_name);
+		/* make sure it's valid */
+		if (!refs_ref_exists(refs, b_remote_head.buf))
+			result |= error(_("Not a valid ref: %s"), b_remote_head.buf);
+		else if (refs_update_symref(refs, b_head.buf, b_remote_head.buf,
+					"remote set-head", &b_local_head, true))
+			result |= error(_("Could not setup %s"), b_head.buf);
+		else {
+			report_set_head(remote, head_name, &b_local_head);
+		}
+		free(head_name);
+	}
+
+	strbuf_release(&b_head);
+	strbuf_release(&b_local_head);
+	strbuf_release(&b_remote_head);
+	return result;
+}
+
 static int do_fetch(struct transport *transport,
 		    struct refspec *rs,
 		    const struct fetch_config *config)
@@ -1647,6 +1723,8 @@ static int do_fetch(struct transport *transport,
 				    "refs/tags/");
 	}
 
+	strvec_push(&transport_ls_refs_options.ref_prefixes,"HEAD");
+
 	if (must_list_refs) {
 		trace2_region_enter("fetch", "remote_refs", the_repository);
 		remote_refs = transport_get_remote_refs(transport,
@@ -1791,6 +1869,9 @@ static int do_fetch(struct transport *transport,
 				  "you need to specify exactly one branch with the --set-upstream option"));
 		}
 	}
+	if (set_head(remote_refs))
+		; // Way too many cases where this can go wrong.
+		  // Just fail silently.
 
 cleanup:
 	if (retcode) {
@@ -2021,6 +2102,7 @@ static int fetch_multiple(struct string_list *list, int max_children,
 	return !!result;
 }
 
+
 /*
  * Fetching from the promisor remote should use the given filter-spec
  * or inherit the default filter-spec from the config.
diff --git a/t/t4207-log-decoration-colors.sh b/t/t4207-log-decoration-colors.sh
index 73ea9e5155..d55d22cb2f 100755
--- a/t/t4207-log-decoration-colors.sh
+++ b/t/t4207-log-decoration-colors.sh
@@ -59,7 +59,8 @@ ${c_reset}${c_tag}tag: ${c_reset}${c_tag}v1.0${c_reset}${c_commit}, \
 ${c_reset}${c_tag}tag: ${c_reset}${c_tag}B${c_reset}${c_commit})${c_reset} B
 ${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
 ${c_tag}tag: ${c_reset}${c_tag}A1${c_reset}${c_commit}, \
-${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit})${c_reset} A1
+${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit}, \
+${c_reset}${c_remoteBranch}other/HEAD${c_reset}${c_commit})${c_reset} A1
 	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
 ${c_stash}refs/stash${c_reset}${c_commit})${c_reset} On main: Changes to A.t
 	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 262a4de0aa..399a501740 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -71,7 +71,7 @@ test_expect_success 'add another remote' '
 		cd test &&
 		git remote add -f second ../two &&
 		tokens_match "origin second" "$(git remote)" &&
-		check_tracking_branch second main side another &&
+		check_tracking_branch second main side another HEAD &&
 		git for-each-ref "--format=%(refname)" refs/remotes |
 		sed -e "/^refs\/remotes\/origin\//d" \
 		    -e "/^refs\/remotes\/second\//d" >actual &&
@@ -725,6 +725,7 @@ test_expect_success 'reject --no-no-tags' '
 cat >one/expect <<\EOF
   apis/main
   apis/side
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
@@ -742,6 +743,7 @@ test_expect_success 'update' '
 '
 
 cat >one/expect <<\EOF
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
@@ -754,7 +756,7 @@ EOF
 test_expect_success 'update with arguments' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -797,7 +799,7 @@ EOF
 test_expect_success 'update default' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -809,6 +811,7 @@ test_expect_success 'update default' '
 '
 
 cat >one/expect <<\EOF
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
@@ -831,7 +834,7 @@ test_expect_success 'update default (overridden, with funny whitespace)' '
 test_expect_success 'update (with remotes.default defined)' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index 0890b9f61c..dfc8d748ba 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -75,6 +75,30 @@ test_expect_success "fetch test for-merge" '
 	cut -f -2 .git/FETCH_HEAD >actual &&
 	test_cmp expected actual'
 
+test_expect_success "fetch test remote HEAD" '
+	cd "$D" &&
+	cd two &&
+	git fetch &&
+	git rev-parse --verify refs/remotes/origin/HEAD &&
+	git rev-parse --verify refs/remotes/origin/main &&
+	head=$(git rev-parse refs/remotes/origin/HEAD) &&
+	branch=$(git rev-parse refs/remotes/origin/main) &&
+	test "z$head" = "z$branch"'
+
+test_expect_success "fetch test remote HEAD change" '
+	cd "$D" &&
+	cd two &&
+	git switch -c other &&
+	git push -u origin other &&
+	git rev-parse --verify refs/remotes/origin/HEAD &&
+	git rev-parse --verify refs/remotes/origin/main &&
+	git rev-parse --verify refs/remotes/origin/other &&
+	git remote set-head origin other &&
+	git fetch &&
+	head=$(git rev-parse refs/remotes/origin/HEAD) &&
+	branch=$(git rev-parse refs/remotes/origin/other) &&
+	test "z$head" = "z$branch"'
+
 test_expect_success 'fetch --prune on its own works as expected' '
 	cd "$D" &&
 	git clone . prune &&
@@ -478,7 +502,6 @@ test_expect_success 'unbundle 1' '
 	test_must_fail git fetch "$D/bundle1" main:main
 '
 
-
 test_expect_success 'bundle 1 has only 3 files ' '
 	cd "$D" &&
 	test_bundle_object_count bundle1 3
@@ -819,7 +842,7 @@ test_expect_success 'fetch from multiple configured URLs in single remote' '
 
 # configured prune tests
 
-set_config_tristate () {
+set_config_tristate() {
 	# var=$1 val=$2
 	case "$2" in
 	unset)
@@ -833,12 +856,12 @@ set_config_tristate () {
 	esac
 }
 
-test_configured_prune () {
+test_configured_prune() {
 	test_configured_prune_type "$@" "name"
 	test_configured_prune_type "$@" "link"
 }
 
-test_configured_prune_type () {
+test_configured_prune_type() {
 	fetch_prune=$1
 	remote_origin_prune=$2
 	fetch_prune_tags=$3
@@ -848,8 +871,7 @@ test_configured_prune_type () {
 	cmdline=$7
 	mode=$8
 
-	if test -z "$cmdline_setup"
-	then
+	if test -z "$cmdline_setup"; then
 		test_expect_success 'setup cmdline_setup variable for subsequent test' '
 			remote_url="file://$(git -C one config remote.origin.url)" &&
 			remote_fetch="$(git -C one config remote.origin.fetch)" &&
@@ -857,12 +879,10 @@ test_configured_prune_type () {
 		'
 	fi
 
-	if test "$mode" = 'link'
-	then
+	if test "$mode" = 'link'; then
 		new_cmdline=""
 
-		if test "$cmdline" = ""
-		then
+		if test "$cmdline" = ""; then
 			new_cmdline=$cmdline_setup
 		else
 			new_cmdline=$(perl -e '
@@ -873,10 +893,8 @@ test_configured_prune_type () {
 		fi
 
 		if test "$fetch_prune_tags" = 'true' ||
-		   test "$remote_origin_prune_tags" = 'true'
-		then
-			if ! printf '%s' "$cmdline\n" | grep -q refs/remotes/origin/
-			then
+			test "$remote_origin_prune_tags" = 'true'; then
+			if ! printf '%s' "$cmdline\n" | grep -q refs/remotes/origin/; then
 				new_cmdline="$new_cmdline refs/tags/*:refs/tags/*"
 			fi
 		fi
@@ -946,100 +964,100 @@ test_configured_prune_type () {
 # $7 git-fetch $cmdline:
 #
 #                     $1    $2    $3    $4    $5     $6     $7
-test_configured_prune unset unset unset unset kept   kept   ""
-test_configured_prune unset unset unset unset kept   kept   "--no-prune"
-test_configured_prune unset unset unset unset pruned kept   "--prune"
-test_configured_prune unset unset unset unset kept   pruned \
+test_configured_prune unset unset unset unset kept kept ""
+test_configured_prune unset unset unset unset kept kept "--no-prune"
+test_configured_prune unset unset unset unset pruned kept "--prune"
+test_configured_prune unset unset unset unset kept pruned \
 	"--prune origin refs/tags/*:refs/tags/*"
 test_configured_prune unset unset unset unset pruned pruned \
 	"--prune origin refs/tags/*:refs/tags/* +refs/heads/*:refs/remotes/origin/*"
 
-test_configured_prune false unset unset unset kept   kept   ""
-test_configured_prune false unset unset unset kept   kept   "--no-prune"
-test_configured_prune false unset unset unset pruned kept   "--prune"
+test_configured_prune false unset unset unset kept kept ""
+test_configured_prune false unset unset unset kept kept "--no-prune"
+test_configured_prune false unset unset unset pruned kept "--prune"
 
-test_configured_prune true  unset unset unset pruned kept   ""
-test_configured_prune true  unset unset unset pruned kept   "--prune"
-test_configured_prune true  unset unset unset kept   kept   "--no-prune"
+test_configured_prune true unset unset unset pruned kept ""
+test_configured_prune true unset unset unset pruned kept "--prune"
+test_configured_prune true unset unset unset kept kept "--no-prune"
 
-test_configured_prune unset false unset unset kept   kept   ""
-test_configured_prune unset false unset unset kept   kept   "--no-prune"
-test_configured_prune unset false unset unset pruned kept   "--prune"
+test_configured_prune unset false unset unset kept kept ""
+test_configured_prune unset false unset unset kept kept "--no-prune"
+test_configured_prune unset false unset unset pruned kept "--prune"
 
-test_configured_prune false false unset unset kept   kept   ""
-test_configured_prune false false unset unset kept   kept   "--no-prune"
-test_configured_prune false false unset unset pruned kept   "--prune"
-test_configured_prune false false unset unset kept   pruned \
+test_configured_prune false false unset unset kept kept ""
+test_configured_prune false false unset unset kept kept "--no-prune"
+test_configured_prune false false unset unset pruned kept "--prune"
+test_configured_prune false false unset unset kept pruned \
 	"--prune origin refs/tags/*:refs/tags/*"
 test_configured_prune false false unset unset pruned pruned \
 	"--prune origin refs/tags/*:refs/tags/* +refs/heads/*:refs/remotes/origin/*"
 
-test_configured_prune true  false unset unset kept   kept   ""
-test_configured_prune true  false unset unset pruned kept   "--prune"
-test_configured_prune true  false unset unset kept   kept   "--no-prune"
+test_configured_prune true false unset unset kept kept ""
+test_configured_prune true false unset unset pruned kept "--prune"
+test_configured_prune true false unset unset kept kept "--no-prune"
 
-test_configured_prune unset true  unset unset pruned kept   ""
-test_configured_prune unset true  unset unset kept   kept   "--no-prune"
-test_configured_prune unset true  unset unset pruned kept   "--prune"
+test_configured_prune unset true unset unset pruned kept ""
+test_configured_prune unset true unset unset kept kept "--no-prune"
+test_configured_prune unset true unset unset pruned kept "--prune"
 
-test_configured_prune false true  unset unset pruned kept   ""
-test_configured_prune false true  unset unset kept   kept   "--no-prune"
-test_configured_prune false true  unset unset pruned kept   "--prune"
+test_configured_prune false true unset unset pruned kept ""
+test_configured_prune false true unset unset kept kept "--no-prune"
+test_configured_prune false true unset unset pruned kept "--prune"
 
-test_configured_prune true  true  unset unset pruned kept   ""
-test_configured_prune true  true  unset unset pruned kept   "--prune"
-test_configured_prune true  true  unset unset kept   kept   "--no-prune"
-test_configured_prune true  true  unset unset kept   pruned \
+test_configured_prune true true unset unset pruned kept ""
+test_configured_prune true true unset unset pruned kept "--prune"
+test_configured_prune true true unset unset kept kept "--no-prune"
+test_configured_prune true true unset unset kept pruned \
 	"--prune origin refs/tags/*:refs/tags/*"
-test_configured_prune true  true  unset unset pruned pruned \
+test_configured_prune true true unset unset pruned pruned \
 	"--prune origin refs/tags/*:refs/tags/* +refs/heads/*:refs/remotes/origin/*"
 
 # --prune-tags on its own does nothing, needs --prune as well, same
 # for fetch.pruneTags without fetch.prune
-test_configured_prune unset unset unset unset kept kept     "--prune-tags"
-test_configured_prune unset unset true unset  kept kept     ""
-test_configured_prune unset unset unset true  kept kept     ""
+test_configured_prune unset unset unset unset kept kept "--prune-tags"
+test_configured_prune unset unset true unset kept kept ""
+test_configured_prune unset unset unset true kept kept ""
 
 # These will prune the tags
 test_configured_prune unset unset unset unset pruned pruned "--prune --prune-tags"
-test_configured_prune true  unset true  unset pruned pruned ""
-test_configured_prune unset true  unset true  pruned pruned ""
+test_configured_prune true unset true unset pruned pruned ""
+test_configured_prune unset true unset true pruned pruned ""
 
 # remote.<name>.pruneTags overrides fetch.pruneTags, just like
 # remote.<name>.prune overrides fetch.prune if set.
-test_configured_prune true  unset true unset pruned pruned  ""
-test_configured_prune false true  false true  pruned pruned ""
-test_configured_prune true  false true  false kept   kept   ""
+test_configured_prune true unset true unset pruned pruned ""
+test_configured_prune false true false true pruned pruned ""
+test_configured_prune true false true false kept kept ""
 
 # When --prune-tags is supplied it's ignored if an explicit refspec is
 # given, same for the configuration options.
 test_configured_prune unset unset unset unset pruned kept \
 	"--prune --prune-tags origin +refs/heads/*:refs/remotes/origin/*"
-test_configured_prune unset unset true  unset pruned kept \
+test_configured_prune unset unset true unset pruned kept \
 	"--prune origin +refs/heads/*:refs/remotes/origin/*"
-test_configured_prune unset unset unset true pruned  kept \
+test_configured_prune unset unset unset true pruned kept \
 	"--prune origin +refs/heads/*:refs/remotes/origin/*"
 
 # Pruning that also takes place if a file:// url replaces a named
 # remote. However, because there's no implicit
 # +refs/heads/*:refs/remotes/origin/* refspec and supplying it on the
 # command-line negates --prune-tags, the branches will not be pruned.
-test_configured_prune_type unset unset unset unset kept   kept   "origin --prune-tags" "name"
-test_configured_prune_type unset unset unset unset kept   kept   "origin --prune-tags" "link"
+test_configured_prune_type unset unset unset unset kept kept "origin --prune-tags" "name"
+test_configured_prune_type unset unset unset unset kept kept "origin --prune-tags" "link"
 test_configured_prune_type unset unset unset unset pruned pruned "origin --prune --prune-tags" "name"
-test_configured_prune_type unset unset unset unset kept   pruned "origin --prune --prune-tags" "link"
+test_configured_prune_type unset unset unset unset kept pruned "origin --prune --prune-tags" "link"
 test_configured_prune_type unset unset unset unset pruned pruned "--prune --prune-tags origin" "name"
-test_configured_prune_type unset unset unset unset kept   pruned "--prune --prune-tags origin" "link"
-test_configured_prune_type unset unset true  unset pruned pruned "--prune origin" "name"
-test_configured_prune_type unset unset true  unset kept   pruned "--prune origin" "link"
-test_configured_prune_type unset unset unset true  pruned pruned "--prune origin" "name"
-test_configured_prune_type unset unset unset true  kept   pruned "--prune origin" "link"
-test_configured_prune_type true  unset true  unset pruned pruned "origin" "name"
-test_configured_prune_type true  unset true  unset kept   pruned "origin" "link"
-test_configured_prune_type unset  true true  unset pruned pruned "origin" "name"
-test_configured_prune_type unset  true true  unset kept   pruned "origin" "link"
-test_configured_prune_type unset  true unset true  pruned pruned "origin" "name"
-test_configured_prune_type unset  true unset true  kept   pruned "origin" "link"
+test_configured_prune_type unset unset unset unset kept pruned "--prune --prune-tags origin" "link"
+test_configured_prune_type unset unset true unset pruned pruned "--prune origin" "name"
+test_configured_prune_type unset unset true unset kept pruned "--prune origin" "link"
+test_configured_prune_type unset unset unset true pruned pruned "--prune origin" "name"
+test_configured_prune_type unset unset unset true kept pruned "--prune origin" "link"
+test_configured_prune_type true unset true unset pruned pruned "origin" "name"
+test_configured_prune_type true unset true unset kept pruned "origin" "link"
+test_configured_prune_type unset true true unset pruned pruned "origin" "name"
+test_configured_prune_type unset true true unset kept pruned "origin" "link"
+test_configured_prune_type unset true unset true pruned pruned "origin" "name"
+test_configured_prune_type unset true unset true kept pruned "origin" "link"
 
 # When all remote.origin.fetch settings are deleted a --prune
 # --prune-tags still implicitly supplies refs/tags/*:refs/tags/* so
@@ -1137,8 +1155,7 @@ test_expect_success 'fetching with auto-gc does not lock up' '
 	)
 '
 
-for section in fetch transfer
-do
+for section in fetch transfer; do
 	test_expect_success "$section.hideRefs affects connectivity check" '
 		GIT_TRACE="$PWD"/trace git -c $section.hideRefs=refs -c \
 			$section.hideRefs="!refs/tags/" fetch &&
@@ -1157,21 +1174,23 @@ test_expect_success 'prepare source branch' '
 	test 3 -le $(wc -l <actual)
 '
 
-validate_store_type () {
+validate_store_type() {
 	git -C dest count-objects -v >actual &&
-	case "$store_type" in
-	packed)
-		grep "^count: 0$" actual ;;
-	loose)
-		grep "^packs: 0$" actual ;;
-	esac || {
+		case "$store_type" in
+		packed)
+			grep "^count: 0$" actual
+			;;
+		loose)
+			grep "^packs: 0$" actual
+			;;
+		esac || {
 		echo "store_type is $store_type"
 		cat actual
 		false
 	}
 }
 
-test_unpack_limit () {
+test_unpack_limit() {
 	store_type=$1
 
 	case "$store_type" in
@@ -1192,43 +1211,39 @@ test_unpack_limit () {
 test_unpack_limit packed
 test_unpack_limit loose
 
-setup_negotiation_tip () {
+setup_negotiation_tip() {
 	SERVER="$1"
 	URL="$2"
 	USE_PROTOCOL_V2="$3"
 
 	rm -rf "$SERVER" client trace &&
-	git init -b main "$SERVER" &&
-	test_commit -C "$SERVER" alpha_1 &&
-	test_commit -C "$SERVER" alpha_2 &&
-	git -C "$SERVER" checkout --orphan beta &&
-	test_commit -C "$SERVER" beta_1 &&
-	test_commit -C "$SERVER" beta_2 &&
-
-	git clone "$URL" client &&
-
-	if test "$USE_PROTOCOL_V2" -eq 1
-	then
-		git -C "$SERVER" config protocol.version 2 &&
-		git -C client config protocol.version 2
-	fi &&
-
-	test_commit -C "$SERVER" beta_s &&
-	git -C "$SERVER" checkout main &&
-	test_commit -C "$SERVER" alpha_s &&
-	git -C "$SERVER" tag -d alpha_1 alpha_2 beta_1 beta_2
+		git init -b main "$SERVER" &&
+		test_commit -C "$SERVER" alpha_1 &&
+		test_commit -C "$SERVER" alpha_2 &&
+		git -C "$SERVER" checkout --orphan beta &&
+		test_commit -C "$SERVER" beta_1 &&
+		test_commit -C "$SERVER" beta_2 &&
+		git clone "$URL" client &&
+		if test "$USE_PROTOCOL_V2" -eq 1; then
+			git -C "$SERVER" config protocol.version 2 &&
+				git -C client config protocol.version 2
+		fi &&
+		test_commit -C "$SERVER" beta_s &&
+		git -C "$SERVER" checkout main &&
+		test_commit -C "$SERVER" alpha_s &&
+		git -C "$SERVER" tag -d alpha_1 alpha_2 beta_1 beta_2
 }
 
-check_negotiation_tip () {
+check_negotiation_tip() {
 	# Ensure that {alpha,beta}_1 are sent as "have", but not {alpha_beta}_2
 	ALPHA_1=$(git -C client rev-parse alpha_1) &&
-	grep "fetch> have $ALPHA_1" trace &&
-	BETA_1=$(git -C client rev-parse beta_1) &&
-	grep "fetch> have $BETA_1" trace &&
-	ALPHA_2=$(git -C client rev-parse alpha_2) &&
-	! grep "fetch> have $ALPHA_2" trace &&
-	BETA_2=$(git -C client rev-parse beta_2) &&
-	! grep "fetch> have $BETA_2" trace
+		grep "fetch> have $ALPHA_1" trace &&
+		BETA_1=$(git -C client rev-parse beta_1) &&
+		grep "fetch> have $BETA_1" trace &&
+		ALPHA_2=$(git -C client rev-parse alpha_2) &&
+		! grep "fetch> have $ALPHA_2" trace &&
+		BETA_2=$(git -C client rev-parse beta_2) &&
+		! grep "fetch> have $BETA_2" trace
 }
 
 test_expect_success '--negotiation-tip limits "have" lines sent' '
diff --git a/t/t5512-ls-remote.sh b/t/t5512-ls-remote.sh
index 64b3491e4e..1b3865e154 100755
--- a/t/t5512-ls-remote.sh
+++ b/t/t5512-ls-remote.sh
@@ -293,6 +293,8 @@ test_expect_success 'ls-remote with filtered symref (refname)' '
 	cat >expect <<-EOF &&
 	ref: refs/heads/main	HEAD
 	$rev	HEAD
+	ref: refs/remotes/origin/main	refs/remotes/origin/HEAD
+	$rev	refs/remotes/origin/HEAD
 	EOF
 	git ls-remote --symref . HEAD >actual &&
 	test_cmp expect actual
diff --git a/t/t5514-fetch-multiple.sh b/t/t5514-fetch-multiple.sh
index 579872c258..e3482b27b2 100755
--- a/t/t5514-fetch-multiple.sh
+++ b/t/t5514-fetch-multiple.sh
@@ -45,14 +45,17 @@ test_expect_success setup '
 '
 
 cat > test/expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
   origin/HEAD -> origin/main
   origin/main
   origin/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -97,6 +100,7 @@ cat > expect << EOF
   origin/HEAD -> origin/main
   origin/main
   origin/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
@@ -112,8 +116,10 @@ test_expect_success 'git fetch --multiple (but only one remote)' '
 '
 
 cat > expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -141,7 +147,7 @@ test_expect_success 'git fetch --multiple (bad remote names)' '
 
 test_expect_success 'git fetch --all (skipFetchAll)' '
 	(cd test4 &&
-	 for b in $(git branch -r)
+	 for b in $(git branch -r | grep -v HEAD)
 	 do
 		git branch -r -d $b || exit 1
 	 done &&
@@ -153,11 +159,14 @@ test_expect_success 'git fetch --all (skipFetchAll)' '
 '
 
 cat > expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -165,7 +174,7 @@ EOF
 
 test_expect_success 'git fetch --multiple (ignoring skipFetchAll)' '
 	(cd test4 &&
-	 for b in $(git branch -r)
+	 for b in $(git branch -r | grep -v HEAD)
 	 do
 		git branch -r -d $b || exit 1
 	 done &&
@@ -221,14 +230,17 @@ test_expect_success 'git fetch --multiple --jobs=0 picks a default' '
 
 create_fetch_all_expect () {
 	cat >expect <<-\EOF
+	  one/HEAD -> one/main
 	  one/main
 	  one/side
 	  origin/HEAD -> origin/main
 	  origin/main
 	  origin/side
+	  three/HEAD -> three/main
 	  three/another
 	  three/main
 	  three/side
+	  two/HEAD -> two/main
 	  two/another
 	  two/main
 	  two/side
@@ -265,6 +277,7 @@ test_expect_success 'git fetch (fetch all remotes with fetch.all = true)' '
 
 create_fetch_one_expect () {
 	cat >expect <<-\EOF
+	  one/HEAD -> one/main
 	  one/main
 	  one/side
 	  origin/HEAD -> origin/main
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 331778bd42..5a051aa0c7 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1395,7 +1395,8 @@ test_expect_success 'fetch follows tags by default' '
 		git tag -m "annotated" tag &&
 		git for-each-ref >tmp1 &&
 		sed -n "p; s|refs/heads/main$|refs/remotes/origin/main|p" tmp1 |
-		sort -k 3 >../expect
+		sed -n "p; s|refs/heads/main$|refs/remotes/origin/HEAD|p"  |
+		sort -k 4 >../expect
 	) &&
 	test_when_finished "rm -rf dst" &&
 	git init dst &&
diff --git a/t/t5527-fetch-odd-refs.sh b/t/t5527-fetch-odd-refs.sh
index 98ece27c6a..d3996af6ee 100755
--- a/t/t5527-fetch-odd-refs.sh
+++ b/t/t5527-fetch-odd-refs.sh
@@ -52,7 +52,8 @@ test_expect_success LONG_REF 'fetch handles extremely long refname' '
 	long
 	main
 	EOF
-	git for-each-ref --format="%(subject)" refs/remotes/long >actual &&
+	git for-each-ref --format="%(subject)" refs/remotes/long \
+		--exclude=refs/remotes/long/HEAD >actual &&
 	test_cmp expect actual
 '
 
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index a66d0e089d..88331d1ceb 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -329,7 +329,8 @@ test_expect_success 'incremental-repack task' '
 
 	# Delete refs that have not been repacked in these packs.
 	git for-each-ref --format="delete %(refname)" \
-		refs/prefetch refs/tags refs/remotes >refs &&
+		refs/prefetch refs/tags refs/remotes \
+		--exclude=refs/remotes/*/HEAD >refs &&
 	git update-ref --stdin <refs &&
 
 	# Replace the object directory with this pack layout.
diff --git a/t/t9210-scalar.sh b/t/t9210-scalar.sh
index 027235d61a..a81662713e 100755
--- a/t/t9210-scalar.sh
+++ b/t/t9210-scalar.sh
@@ -150,7 +150,8 @@ test_expect_success 'scalar clone' '
 			"$(pwd)" &&
 
 		git for-each-ref --format="%(refname)" refs/remotes/origin/ >actual &&
-		echo "refs/remotes/origin/parallel" >expect &&
+		echo "refs/remotes/origin/HEAD" >>expect &&
+		echo "refs/remotes/origin/parallel" >>expect &&
 		test_cmp expect actual &&
 
 		test_path_is_missing 1/2 &&
@@ -219,7 +220,7 @@ test_expect_success 'scalar reconfigure --all with includeIf.onbranch' '
 	done
 '
 
- test_expect_success 'scalar reconfigure --all with detached HEADs' '
+test_expect_success 'scalar reconfigure --all with detached HEADs' '
 	repos="two three four" &&
 	for num in $repos
 	do
diff --git a/t/t9211-scalar-clone.sh b/t/t9211-scalar-clone.sh
index 7869f45ee6..01f71910f5 100755
--- a/t/t9211-scalar-clone.sh
+++ b/t/t9211-scalar-clone.sh
@@ -31,7 +31,7 @@ test_expect_success 'set up repository to clone' '
 	)
 '
 
-cleanup_clone () {
+cleanup_clone() {
 	rm -rf "$1"
 }
 
@@ -127,7 +127,7 @@ test_expect_success '--single-branch clones HEAD only' '
 	(
 		cd $enlistment/src &&
 		git for-each-ref refs/remotes/origin >out &&
-		test_line_count = 1 out &&
+		test_line_count = 2 out &&
 		grep "refs/remotes/origin/base" out
 	) &&
 
@@ -141,7 +141,7 @@ test_expect_success '--no-single-branch clones all branches' '
 	(
 		cd $enlistment/src &&
 		git for-each-ref refs/remotes/origin >out &&
-		test_line_count = 2 out &&
+		test_line_count = 3 out &&
 		grep "refs/remotes/origin/base" out &&
 		grep "refs/remotes/origin/parallel" out
 	) &&
diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh
index cc6aa9f0cd..be87e92e5c 100755
--- a/t/t9902-completion.sh
+++ b/t/t9902-completion.sh
@@ -658,6 +658,7 @@ test_expect_success '__git_refs - simple' '
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	matching-tag
@@ -673,6 +674,7 @@ test_expect_success '__git_refs - full refs' '
 	cat >expected <<-EOF &&
 	refs/heads/main
 	refs/heads/matching-branch
+	refs/remotes/other/HEAD
 	refs/remotes/other/branch-in-other
 	refs/remotes/other/main-in-other
 	refs/tags/matching-tag
@@ -729,6 +731,7 @@ test_expect_success '__git_refs - remote on local file system - full refs' '
 test_expect_success '__git_refs - configured remote' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -756,6 +759,7 @@ test_expect_success '__git_refs - configured remote - full refs' '
 test_expect_success '__git_refs - configured remote - repo given on the command line' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -787,6 +791,7 @@ test_expect_success '__git_refs - configured remote - full refs - repo given on
 test_expect_success '__git_refs - configured remote - remote name matches a directory' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -875,12 +880,14 @@ test_expect_success '__git_refs - unique remote branches for git checkout DWIMer
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/ambiguous
 	other/branch-in-other
 	other/main-in-other
 	remote/ambiguous
 	remote/branch-in-remote
 	matching-tag
+	HEAD
 	branch-in-other
 	branch-in-remote
 	main-in-other
@@ -904,6 +911,7 @@ test_expect_success '__git_refs - after --opt=' '
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	matching-tag
@@ -919,6 +927,7 @@ test_expect_success '__git_refs - after --opt= - full refs' '
 	cat >expected <<-EOF &&
 	refs/heads/main
 	refs/heads/matching-branch
+	refs/remotes/other/HEAD
 	refs/remotes/other/branch-in-other
 	refs/remotes/other/main-in-other
 	refs/tags/matching-tag
@@ -935,6 +944,7 @@ test_expect_success '__git refs - excluding refs' '
 	^HEAD
 	^main
 	^matching-branch
+	^other/HEAD
 	^other/branch-in-other
 	^other/main-in-other
 	^matching-tag
@@ -950,6 +960,7 @@ test_expect_success '__git refs - excluding full refs' '
 	cat >expected <<-EOF &&
 	^refs/heads/main
 	^refs/heads/matching-branch
+	^refs/remotes/other/HEAD
 	^refs/remotes/other/branch-in-other
 	^refs/remotes/other/main-in-other
 	^refs/tags/matching-tag
@@ -975,6 +986,7 @@ test_expect_success '__git_refs - do not filter refs unless told so' '
 	main
 	matching-branch
 	matching/branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	other/matching/branch-in-other
@@ -1095,6 +1107,7 @@ test_expect_success '__git_complete_refs - simple' '
 	HEAD Z
 	main Z
 	matching-branch Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	matching-tag Z
@@ -1123,6 +1136,7 @@ test_expect_success '__git_complete_refs - matching' '
 test_expect_success '__git_complete_refs - remote' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main-in-other Z
 	EOF
@@ -1139,9 +1153,11 @@ test_expect_success '__git_complete_refs - track' '
 	HEAD Z
 	main Z
 	matching-branch Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	matching-tag Z
+	HEAD Z
 	branch-in-other Z
 	main-in-other Z
 	EOF
@@ -1184,6 +1200,7 @@ test_expect_success '__git_complete_refs - suffix' '
 	HEAD.
 	main.
 	matching-branch.
+	other/HEAD.
 	other/branch-in-other.
 	other/main-in-other.
 	matching-tag.
@@ -1199,6 +1216,7 @@ test_expect_success '__git_complete_refs - suffix' '
 test_expect_success '__git_complete_fetch_refspecs - simple' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	HEAD:HEAD Z
+	HEAD:HEAD Z
 	branch-in-other:branch-in-other Z
 	main-in-other:main-in-other Z
 	EOF
@@ -1225,6 +1243,7 @@ test_expect_success '__git_complete_fetch_refspecs - matching' '
 test_expect_success '__git_complete_fetch_refspecs - prefix' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	+HEAD:HEAD Z
+	+HEAD:HEAD Z
 	+branch-in-other:branch-in-other Z
 	+main-in-other:main-in-other Z
 	EOF
@@ -1289,6 +1308,7 @@ test_expect_success '__git_complete_worktree_paths with -C' '
 
 test_expect_success 'git switch - with no options, complete local branches and unique remote branch names for DWIM logic' '
 	test_completion "git switch " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1435,11 +1455,13 @@ test_expect_success 'git-bisect - existing view subcommand is recognized and ena
 test_expect_success 'git checkout - completes refs and unique remote branches for DWIM' '
 	test_completion "git checkout " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1461,6 +1483,7 @@ test_expect_success 'git switch - with GIT_COMPLETION_CHECKOUT_NO_GUESS=1, compl
 
 test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_GUESS=1, complete local branches and unique remote names for DWIM logic' '
 	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git switch --guess " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1470,6 +1493,7 @@ test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_G
 
 test_expect_success 'git switch - a later --guess overrides previous --no-guess, complete local and remote unique branches for DWIM' '
 	test_completion "git switch --no-guess --guess " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1490,6 +1514,7 @@ test_expect_success 'git checkout - with GIT_COMPLETION_NO_GUESS=1 only complete
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1498,11 +1523,13 @@ test_expect_success 'git checkout - with GIT_COMPLETION_NO_GUESS=1 only complete
 test_expect_success 'git checkout - --guess overrides GIT_COMPLETION_NO_GUESS=1, complete refs and unique remote branches for DWIM' '
 	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git checkout --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1514,6 +1541,7 @@ test_expect_success 'git checkout - with --no-guess, only completes refs' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1522,11 +1550,13 @@ test_expect_success 'git checkout - with --no-guess, only completes refs' '
 test_expect_success 'git checkout - a later --guess overrides previous --no-guess, complete refs and unique remote branches for DWIM' '
 	test_completion "git checkout --no-guess --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1538,6 +1568,7 @@ test_expect_success 'git checkout - a later --no-guess overrides previous --gues
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1550,6 +1581,7 @@ test_expect_success 'git checkout - with checkout.guess = false, only completes
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1559,11 +1591,13 @@ test_expect_success 'git checkout - with checkout.guess = true, completes refs a
 	test_config checkout.guess true &&
 	test_completion "git checkout " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1573,11 +1607,13 @@ test_expect_success 'git checkout - a later --guess overrides previous checkout.
 	test_config checkout.guess false &&
 	test_completion "git checkout --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1590,6 +1626,7 @@ test_expect_success 'git checkout - a later --no-guess overrides previous checko
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1601,6 +1638,7 @@ test_expect_success 'git switch - with --detach, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1612,6 +1650,7 @@ test_expect_success 'git checkout - with --detach, complete only references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1783,6 +1822,7 @@ test_expect_success 'git switch - with -d, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1794,6 +1834,7 @@ test_expect_success 'git checkout - with -d, complete only references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1801,10 +1842,12 @@ test_expect_success 'git checkout - with -d, complete only references' '
 
 test_expect_success 'git switch - with --track, complete only remote branches' '
 	test_completion "git switch --track " <<-\EOF &&
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
 	test_completion "git switch -t " <<-\EOF
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1812,10 +1855,12 @@ test_expect_success 'git switch - with --track, complete only remote branches' '
 
 test_expect_success 'git checkout - with --track, complete only remote branches' '
 	test_completion "git checkout --track " <<-\EOF &&
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
 	test_completion "git checkout -t " <<-\EOF
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1834,6 +1879,7 @@ test_expect_success 'git checkout - with --no-track, complete only local referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1845,6 +1891,7 @@ test_expect_success 'git switch - with -c, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1856,6 +1903,7 @@ test_expect_success 'git switch - with -C, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1867,6 +1915,7 @@ test_expect_success 'git switch - with -c and --track, complete all references'
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1878,6 +1927,7 @@ test_expect_success 'git switch - with -C and --track, complete all references'
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1889,6 +1939,7 @@ test_expect_success 'git switch - with -c and --no-track, complete all reference
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1900,6 +1951,7 @@ test_expect_success 'git switch - with -C and --no-track, complete all reference
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1911,6 +1963,7 @@ test_expect_success 'git checkout - with -b, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1922,6 +1975,7 @@ test_expect_success 'git checkout - with -B, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1933,6 +1987,7 @@ test_expect_success 'git checkout - with -b and --track, complete all references
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1944,6 +1999,7 @@ test_expect_success 'git checkout - with -B and --track, complete all references
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1955,6 +2011,7 @@ test_expect_success 'git checkout - with -b and --no-track, complete all referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1966,6 +2023,7 @@ test_expect_success 'git checkout - with -B and --no-track, complete all referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1973,6 +2031,7 @@ test_expect_success 'git checkout - with -B and --no-track, complete all referen
 
 test_expect_success 'git switch - for -c, complete local branches and unique remote branches' '
 	test_completion "git switch -c " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1982,6 +2041,7 @@ test_expect_success 'git switch - for -c, complete local branches and unique rem
 
 test_expect_success 'git switch - for -C, complete local branches and unique remote branches' '
 	test_completion "git switch -C " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2019,6 +2079,7 @@ test_expect_success 'git switch - for -C with --no-track, complete local branche
 
 test_expect_success 'git checkout - for -b, complete local branches and unique remote branches' '
 	test_completion "git checkout -b " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2028,6 +2089,7 @@ test_expect_success 'git checkout - for -b, complete local branches and unique r
 
 test_expect_success 'git checkout - for -B, complete local branches and unique remote branches' '
 	test_completion "git checkout -B " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2065,6 +2127,7 @@ test_expect_success 'git checkout - for -B with --no-track, complete local branc
 
 test_expect_success 'git switch - with --orphan completes local branch names and unique remote branch names' '
 	test_completion "git switch --orphan " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2080,6 +2143,7 @@ test_expect_success 'git switch - --orphan with branch already provided complete
 
 test_expect_success 'git checkout - with --orphan completes local branch names and unique remote branch names' '
 	test_completion "git checkout --orphan " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2093,6 +2157,7 @@ test_expect_success 'git checkout - --orphan with branch already provided comple
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -3005,7 +3070,7 @@ test_expect_success '__git_pseudoref_exists' '
 		# HEAD should exist, even if it points to an unborn branch.
 		__git_pseudoref_exists HEAD >output 2>&1 &&
 		test_must_be_empty output &&
-
+ 
 		# HEAD points to an existing branch, so it should exist.
 		test_commit A &&
 		__git_pseudoref_exists HEAD >output 2>&1 &&
-- 
2.47.0.6.g9542df9767
Junio C Hamano· Oct 9, 2024, 22:01 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v5 6/6] fetch: set remote/HEAD if it does not exist

Bence Ferdinandy <bence@ferdinandy.com> writes:
Show 15 quoted lines
> diff --git a/builtin/fetch.c b/builtin/fetch.c
> index 80a64d0d26..c3d3c05950 100644
> --- a/builtin/fetch.c
> +++ b/builtin/fetch.c
> @@ -1578,6 +1578,82 @@ static int backfill_tags(struct display_state *display_state,
>  	return retcode;
>  }
>  
> +static void report_set_head(const char *remote, const char *head_name,
> +			struct strbuf *buf_prev) {
> +	struct strbuf buf_prefix = STRBUF_INIT;
> +	const char *prev_head;
> +
> +	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
> +	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);

The same "uninitialized prev_head" problem I discussed earlier for a separate patch is here, I think.

Show 8 quoted lines
> +	if (prev_head && !strcmp(prev_head, head_name))
> +		;
> +	else if (prev_head) {
> +		printf("'HEAD' at '%s' has changed from '%s' to '%s'\n",
> +			remote, prev_head, head_name);
> +		printf("Run 'git remote set-head %s %s' to follow the change.\n",
> +			remote, head_name);
> +	}

If the condition is only for one arm, can't we do this without else/if? Would the condition become too contorted to read if we did so? Let's see.

	if (prev_head && strcmp(prev_head, head_name)) {
		HEAD has changed from prev_head to head_name;
	}

I guess it is a bit subjective, but as long as you are used to read "strcmp(a,b)" as "a and b are different", this should be easier to read than the original.

Show 34 quoted lines
> +static const char *strip_refshead(const char *name){
> +	skip_prefix(name, "refs/heads/", &name);
> +	return name;
> +}
> +
> +static int set_head(const struct ref *remote_refs)
> +{
> +	int result = 0;
> +	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
> +		b_local_head = STRBUF_INIT;
> +	const char *remote = gtransport->remote->name;
> +	char *head_name = NULL;
> +	struct ref *ref, *matches;
> +	struct ref *fetch_map = NULL, **fetch_map_tail = &fetch_map;
> +	struct refspec_item refspec = {
> +		.force = 0,
> +		.pattern = 1,
> +		.src = (char *) "refs/heads/*",
> +		.dst = (char *) "refs/heads/*",
> +	};
> +	struct string_list heads = STRING_LIST_INIT_DUP;
> +	struct ref_store *refs = get_main_ref_store(the_repository);
> +
> +	get_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);
> +	matches = guess_remote_head(find_ref_by_name(remote_refs, "HEAD"),
> +				    fetch_map, 1);
> +	for (ref = matches; ref; ref = ref->next) {
> +		string_list_append(&heads, strip_refshead(ref->name));
> +	}
> +
> +
> +	if (!heads.nr)
> +		result = 1;
> +	else if (heads.nr > 1) {
Curious use of extra {braces}.  In this case we can just lose them.
> +		result = 1;
> +	} else
> +		head_name = xstrdup(heads.items[0].string);
Show 9 quoted lines
> +	if (head_name) {
> +		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
> +		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote, head_name);
> +		/* make sure it's valid */
> +		if (!refs_ref_exists(refs, b_remote_head.buf))
> +			result |= error(_("Not a valid ref: %s"), b_remote_head.buf);
> +		else if (refs_update_symref(refs, b_head.buf, b_remote_head.buf,
> +					"remote set-head", &b_local_head, true))
> +			result |= error(_("Could not setup %s"), b_head.buf);
Two problems.
 * we do not capitalize the beginning of the sentence in an error()
   message, i.e. "Could not" -> "could not" (or "cannot").
 * "setup" is not a verb.

I know both were inherited from builtin/remote.c but existing breakage is not a good excuse to make new code worse.

> +		else {
Curious use of extra {braces}.  In this case we can just lose them.
Show 19 quoted lines
> +			report_set_head(remote, head_name, &b_local_head);
> +		}
> +		free(head_name);
> +	}
> +
> +	strbuf_release(&b_head);
> +	strbuf_release(&b_local_head);
> +	strbuf_release(&b_remote_head);
> +	return result;
> +}
> +
>  static int do_fetch(struct transport *transport,
>  		    struct refspec *rs,
>  		    const struct fetch_config *config)
> @@ -1647,6 +1723,8 @@ static int do_fetch(struct transport *transport,
>  				    "refs/tags/");
>  	}
>  
> +	strvec_push(&transport_ls_refs_options.ref_prefixes,"HEAD");
Missing SP after ",".
Show 10 quoted lines
>  	if (must_list_refs) {
>  		trace2_region_enter("fetch", "remote_refs", the_repository);
>  		remote_refs = transport_get_remote_refs(transport,
> @@ -1791,6 +1869,9 @@ static int do_fetch(struct transport *transport,
>  				  "you need to specify exactly one branch with the --set-upstream option"));
>  		}
>  	}
> +	if (set_head(remote_refs))
> +		; // Way too many cases where this can go wrong.
> +		  // Just fail silently.
	/*
	 * our multi-line comments
	 * look like this.
	 */

More importantly, we are not failing silently. As we saw in the implementation of set_head() above, a non-zero return came from ret that was assigned the return value of error() in the function, so an error message is already given, no?

Show 8 quoted lines
> @@ -2021,6 +2102,7 @@ static int fetch_multiple(struct string_list *list, int max_children,
>  	return !!result;
>  }
>  
> +
>  /*
>   * Fetching from the promisor remote should use the given filter-spec
>   * or inherit the default filter-spec from the config.
An unintended change?
Thanks.
Bence Ferdinandy· Oct 10, 2024, 13:29 UTC · re: Junio C Hamano · lore

[PATCH v6 1/6] refs_update_symref: atomically record overwritten ref

When updating a symref it's currently not possible to know for sure what was the previous value that was overwritten. Make use of ref_transaction's atomicity and record the previous value there. Add a new variable to refs_update_symref's signature to be able to pass this information back up to callers.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    
    v5: - added before_target to reftables backend
        - added an extra safety check for transaction's existence in refs.c
    
    v6: - no change
 builtin/branch.c          | 2 +-
 builtin/checkout.c        | 4 ++--
 builtin/clone.c           | 6 +++---
 builtin/notes.c           | 2 +-
 builtin/remote.c          | 6 +++---
 builtin/symbolic-ref.c    | 2 +-
 builtin/worktree.c        | 2 +-
 refs.c                    | 8 ++++++--
 refs.h                    | 3 ++-
 refs/files-backend.c      | 1 +
 refs/refs-internal.h      | 8 ++++++++
 refs/reftable-backend.c   | 1 +
 reset.c                   | 2 +-
 sequencer.c               | 2 +-
 setup.c                   | 2 +-
 t/helper/test-ref-store.c | 2 +-
 16 files changed, 34 insertions(+), 19 deletions(-)
Show changes to 16 files +34 −19

builtin/branch.c, builtin/checkout.c, builtin/clone.c, builtin/notes.c, builtin/remote.c, builtin/symbolic-ref.c, builtin/worktree.c, refs.c, refs.h, refs/files-backend.c, refs/refs-internal.h, refs/reftable-backend.c, reset.c, sequencer.c, setup.c, t/helper/test-ref-store.c

diff --git a/builtin/branch.c b/builtin/branch.c
index fd1611ebf5..6c87690b58 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -559,7 +559,7 @@ static int replace_each_worktree_head_symref(struct worktree **worktrees,
 			continue;
 
 		refs = get_worktree_ref_store(worktrees[i]);
-		if (refs_update_symref(refs, "HEAD", newref, logmsg))
+		if (refs_update_symref(refs, "HEAD", newref, logmsg, NULL))
 			ret = error(_("HEAD of working tree %s is not updated"),
 				    worktrees[i]->path);
 	}
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 9c30000d3a..356ee9bcde 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -1015,7 +1015,7 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 			describe_detached_head(_("HEAD is now at"), new_branch_info->commit);
 		}
 	} else if (new_branch_info->path) {	/* Switch branches. */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path, msg.buf) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path, msg.buf, NULL) < 0)
 			die(_("unable to update HEAD"));
 		if (!opts->quiet) {
 			if (old_branch_info->path && !strcmp(new_branch_info->path, old_branch_info->path)) {
@@ -1479,7 +1479,7 @@ static int switch_unborn_to_new_branch(const struct checkout_opts *opts)
 		die(_("You are on a branch yet to be born"));
 	strbuf_addf(&branch_ref, "refs/heads/%s", opts->new_branch);
 	status = refs_update_symref(get_main_ref_store(the_repository),
-				    "HEAD", branch_ref.buf, "checkout -b");
+				    "HEAD", branch_ref.buf, "checkout -b", NULL);
 	strbuf_release(&branch_ref);
 	if (!opts->quiet)
 		fprintf(stderr, _("Switched to a new branch '%s'\n"),
diff --git a/builtin/clone.c b/builtin/clone.c
index e77339c847..ead2af20ea 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -661,7 +661,7 @@ static void update_remote_refs(const struct ref *refs,
 		strbuf_addstr(&head_ref, "HEAD");
 		if (refs_update_symref(get_main_ref_store(the_repository), head_ref.buf,
 				       remote_head_points_at->peer_ref->name,
-				       msg) < 0)
+				       msg, NULL) < 0)
 			die(_("unable to update %s"), head_ref.buf);
 		strbuf_release(&head_ref);
 	}
@@ -673,7 +673,7 @@ static void update_head(const struct ref *our, const struct ref *remote,
 	const char *head;
 	if (our && skip_prefix(our->name, "refs/heads/", &head)) {
 		/* Local default branch link */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name, NULL, NULL) < 0)
 			die(_("unable to update HEAD"));
 		if (!option_bare) {
 			refs_update_ref(get_main_ref_store(the_repository),
@@ -702,7 +702,7 @@ static void update_head(const struct ref *our, const struct ref *remote,
 		 * Unborn head from remote; same as "our" case above except
 		 * that we have no ref to update.
 		 */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn, NULL, NULL) < 0)
 			die(_("unable to update HEAD"));
 		if (!option_bare)
 			install_branch_config(0, head, remote_name, unborn);
diff --git a/builtin/notes.c b/builtin/notes.c
index 8c26e45526..ba646f06ff 100644
--- a/builtin/notes.c
+++ b/builtin/notes.c
@@ -980,7 +980,7 @@ static int merge(int argc, const char **argv, const char *prefix)
 			die(_("a notes merge into %s is already in-progress at %s"),
 			    notes_ref, wt->path);
 		free_worktrees(worktrees);
-		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", notes_ref, NULL))
+		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", notes_ref, NULL, NULL))
 			die(_("failed to store link to current notes ref (%s)"),
 			    notes_ref);
 		fprintf(stderr, _("Automatic notes merge failed. Fix conflicts in %s "
diff --git a/builtin/remote.c b/builtin/remote.c
index 76670ddd8b..d8ff440027 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -244,7 +244,7 @@ static int add(int argc, const char **argv, const char *prefix)
 		strbuf_reset(&buf2);
 		strbuf_addf(&buf2, "refs/remotes/%s/%s", name, master);
 
-		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote add"))
+		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote add", NULL))
 			result = error(_("Could not setup master '%s'"), master);
 	}
 
@@ -864,7 +864,7 @@ static int mv(int argc, const char **argv, const char *prefix)
 		strbuf_reset(&buf3);
 		strbuf_addf(&buf3, "remote: renamed %s to %s",
 				item->string, buf.buf);
-		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, buf3.buf))
+		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, buf3.buf, NULL))
 			die(_("creating '%s' failed"), buf.buf);
 		display_progress(progress, ++refs_renamed_nr);
 	}
@@ -1444,7 +1444,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		/* make sure it's valid */
 		if (!refs_ref_exists(get_main_ref_store(the_repository), buf2.buf))
 			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head"))
+		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head", NULL))
 			result |= error(_("Could not setup %s"), buf.buf);
 		else if (opt_a)
 			printf("%s/HEAD set to %s\n", argv[0], head_name);
diff --git a/builtin/symbolic-ref.c b/builtin/symbolic-ref.c
index 299d23d76a..7728fbc3c1 100644
--- a/builtin/symbolic-ref.c
+++ b/builtin/symbolic-ref.c
@@ -88,7 +88,7 @@ int cmd_symbolic_ref(int argc,
 		if (check_refname_format(argv[1], REFNAME_ALLOW_ONELEVEL) < 0)
 			die("Refusing to set '%s' to invalid ref '%s'", argv[0], argv[1]);
 		ret = !!refs_update_symref(get_main_ref_store(the_repository),
-					   argv[0], argv[1], msg);
+					   argv[0], argv[1], msg, NULL);
 		break;
 	default:
 		usage_with_options(git_symbolic_ref_usage, options);
diff --git a/builtin/worktree.c b/builtin/worktree.c
index fc31d072a6..a7ab4193c1 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -517,7 +517,7 @@ static int add_worktree(const char *path, const char *refname,
 		ret = refs_update_ref(wt_refs, NULL, "HEAD", &commit->object.oid,
 				      NULL, 0, UPDATE_REFS_MSG_ON_ERR);
 	else
-		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, NULL);
+		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, NULL, NULL);
 	if (ret)
 		goto done;
 
diff --git a/refs.c b/refs.c
index 5f729ed412..91cacee6f9 100644
--- a/refs.c
+++ b/refs.c
@@ -2114,7 +2114,8 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
 }
 
 int refs_update_symref(struct ref_store *refs, const char *ref,
-		       const char *target, const char *logmsg)
+		       const char *target, const char *logmsg,
+		       struct strbuf *before_target)
 {
 	struct ref_transaction *transaction;
 	struct strbuf err = STRBUF_INIT;
@@ -2130,6 +2131,10 @@ int refs_update_symref(struct ref_store *refs, const char *ref,
 	}
 
 	strbuf_release(&err);
+
+	if (transaction && before_target && transaction->updates[0]->before_target)
+		strbuf_addstr(before_target, transaction->updates[0]->before_target);
+
 	if (transaction)
 		ref_transaction_free(transaction);
 
@@ -2948,4 +2953,3 @@ int ref_update_expects_existing_old_ref(struct ref_update *update)
 	return (update->flags & REF_HAVE_OLD) &&
 		(!is_null_oid(&update->old_oid) || update->old_target);
 }
-
diff --git a/refs.h b/refs.h
index 108dfc93b3..f38616db84 100644
--- a/refs.h
+++ b/refs.h
@@ -571,7 +571,8 @@ int refs_copy_existing_ref(struct ref_store *refs, const char *oldref,
 		    const char *newref, const char *logmsg);
 
 int refs_update_symref(struct ref_store *refs, const char *refname,
-		       const char *target, const char *logmsg);
+		       const char *target, const char *logmsg,
+		       struct strbuf *before_target);
 
 enum action_on_err {
 	UPDATE_REFS_MSG_ON_ERR,
diff --git a/refs/files-backend.c b/refs/files-backend.c
index 0824c0b8a9..8415f2d020 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -2577,6 +2577,7 @@ static int lock_ref_for_update(struct files_ref_store *refs,
 	}
 
 	update->backend_data = lock;
+	update->before_target = xstrdup_or_null(referent.buf);
 
 	if (update->type & REF_ISSYMREF) {
 		if (update->flags & REF_NO_DEREF) {
diff --git a/refs/refs-internal.h b/refs/refs-internal.h
index 2313c830d8..7df3e6271e 100644
--- a/refs/refs-internal.h
+++ b/refs/refs-internal.h
@@ -104,6 +104,14 @@ struct ref_update {
 	 */
 	const char *old_target;
 
+	/*
+	 * The previous target before applying new_target will be
+	 * written here, to be used by callers when they do not want to
+	 * check old_target during the transaction, but do want to know
+	 * what it was.
+	 */
+	const char *before_target;
+
 	/*
 	 * One or more of REF_NO_DEREF, REF_FORCE_CREATE_REFLOG,
 	 * REF_HAVE_NEW, REF_HAVE_OLD, or backend-specific flags.
diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
index 3c96fbf66f..32330b6bc6 100644
--- a/refs/reftable-backend.c
+++ b/refs/reftable-backend.c
@@ -1244,6 +1244,7 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 			if (ret)
 				goto done;
 		}
+		u->before_target = xstrdup_or_null(referent.buf);
 	}
 
 	transaction->backend_data = tx_data;
diff --git a/reset.c b/reset.c
index b22b1be792..cc36a9ed56 100644
--- a/reset.c
+++ b/reset.c
@@ -76,7 +76,7 @@ static int update_refs(const struct reset_head_opts *opts,
 		if (!ret)
 			ret = refs_update_symref(get_main_ref_store(the_repository),
 						 "HEAD", switch_to_branch,
-						 reflog_head);
+						 reflog_head, NULL);
 	}
 	if (!ret && run_hook)
 		run_hooks_l(the_repository, "post-checkout",
diff --git a/sequencer.c b/sequencer.c
index 8d01cd50ac..23b162924c 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -5107,7 +5107,7 @@ static int pick_commits(struct repository *r,
 			}
 			msg = reflog_message(opts, "finish", "returning to %s",
 				head_ref.buf);
-			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf, msg)) {
+			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf, msg, NULL)) {
 				res = error(_("could not update HEAD to %s"),
 					head_ref.buf);
 				goto cleanup_head_ref;
diff --git a/setup.c b/setup.c
index 94e79b2e48..d95f051465 100644
--- a/setup.c
+++ b/setup.c
@@ -2275,7 +2275,7 @@ void create_reference_database(enum ref_storage_format ref_storage_format,
 			die(_("invalid initial branch name: '%s'"),
 			    initial_branch);
 
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref, NULL, NULL) < 0)
 			exit(1);
 		free(ref);
 	}
diff --git a/t/helper/test-ref-store.c b/t/helper/test-ref-store.c
index 65346dee55..a911302bea 100644
--- a/t/helper/test-ref-store.c
+++ b/t/helper/test-ref-store.c
@@ -120,7 +120,7 @@ static int cmd_create_symref(struct ref_store *refs, const char **argv)
 	const char *target = notnull(*argv++, "target");
 	const char *logmsg = *argv++;
 
-	return refs_update_symref(refs, refname, target, logmsg);
+	return refs_update_symref(refs, refname, target, logmsg, NULL);
 }
 
 static struct flag_definition transaction_flags[] = {
-- 
2.47.0.6.g07cb02250a
Bence Ferdinandy· Oct 10, 2024, 13:29 UTC · re: Bence Ferdinandy · lore

[PATCH v6 2/6] set-head: add new variable for readability

Instead of calling get_main_ref_store(the_repository) multiple times, call it once and store in a new refs variable. Although this change probably offers some performance benefits, the main purpose is to shorten the line lengths of function calls using this variable for better readability.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v5: new patch (split from the next patch as a preparatory step)
    
    v6: no change
 builtin/remote.c | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)
Show changes to builtin/remote.c +4 −3
diff --git a/builtin/remote.c b/builtin/remote.c
index d8ff440027..353ffd2c43 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1404,6 +1404,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 	int i, opt_a = 0, opt_d = 0, result = 0;
 	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;
 	char *head_name = NULL;
+	struct ref_store *refs = get_main_ref_store(the_repository);
 
 	struct option options[] = {
 		OPT_BOOL('a', "auto", &opt_a,
@@ -1434,7 +1435,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 			head_name = xstrdup(states.heads.items[0].string);
 		free_remote_ref_states(&states);
 	} else if (opt_d && !opt_a && argc == 1) {
-		if (refs_delete_ref(get_main_ref_store(the_repository), NULL, buf.buf, NULL, REF_NO_DEREF))
+		if (refs_delete_ref(refs, NULL, buf.buf, NULL, REF_NO_DEREF))
 			result |= error(_("Could not delete %s"), buf.buf);
 	} else
 		usage_with_options(builtin_remote_sethead_usage, options);
@@ -1442,9 +1443,9 @@ static int set_head(int argc, const char **argv, const char *prefix)
 	if (head_name) {
 		strbuf_addf(&buf2, "refs/remotes/%s/%s", argv[0], head_name);
 		/* make sure it's valid */
-		if (!refs_ref_exists(get_main_ref_store(the_repository), buf2.buf))
+		if (!refs_ref_exists(refs, buf2.buf))
 			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head", NULL))
+		else if (refs_update_symref(refs, buf.buf, buf2.buf, "remote set-head", NULL))
 			result |= error(_("Could not setup %s"), buf.buf);
 		else if (opt_a)
 			printf("%s/HEAD set to %s\n", argv[0], head_name);
-- 
2.47.0.6.g07cb02250a
Bence Ferdinandy· Oct 10, 2024, 13:29 UTC · re: Bence Ferdinandy · lore

[PATCH v6 4/6] transaction: add TRANSACTION_CREATE_EXISTS error

Currently there is only one special error for transaction, for when there is a naming conflict, all other errors are dumped under a generic error. Add a new special error case for when the caller requests the reference to be updated only when it does not yet exist and the reference actually does exist.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    v5: no change
    v6: no change
 refs.h                  |  4 +++-
 refs/files-backend.c    | 28 ++++++++++++++++++++--------
 refs/reftable-backend.c |  6 ++++--
 3 files changed, 27 insertions(+), 11 deletions(-)
Show changes to 3 files +27 −11

refs.h, refs/files-backend.c, refs/reftable-backend.c

diff --git a/refs.h b/refs.h
index f38616db84..166affbc89 100644
--- a/refs.h
+++ b/refs.h
@@ -759,8 +759,10 @@ int ref_transaction_verify(struct ref_transaction *transaction,
 
 /* Naming conflict (for example, the ref names A and A/B conflict). */
 #define TRANSACTION_NAME_CONFLICT -1
+/* When only creation was requested, but the ref already exists. */
+#define TRANSACTION_CREATE_EXISTS -2
 /* All other errors. */
-#define TRANSACTION_GENERIC_ERROR -2
+#define TRANSACTION_GENERIC_ERROR -3
 
 /*
  * Perform the preparatory stages of committing `transaction`. Acquire
diff --git a/refs/files-backend.c b/refs/files-backend.c
index 8415f2d020..272ad81315 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -2502,14 +2502,18 @@ static int split_symref_update(struct ref_update *update,
 static int check_old_oid(struct ref_update *update, struct object_id *oid,
 			 struct strbuf *err)
 {
+	int ret = TRANSACTION_GENERIC_ERROR;
+
 	if (!(update->flags & REF_HAVE_OLD) ||
 		   oideq(oid, &update->old_oid))
 		return 0;
 
-	if (is_null_oid(&update->old_oid))
+	if (is_null_oid(&update->old_oid)) {
 		strbuf_addf(err, "cannot lock ref '%s': "
 			    "reference already exists",
 			    ref_update_original_update_refname(update));
+		ret = TRANSACTION_CREATE_EXISTS;
+	}
 	else if (is_null_oid(oid))
 		strbuf_addf(err, "cannot lock ref '%s': "
 			    "reference is missing but expected %s",
@@ -2522,7 +2526,7 @@ static int check_old_oid(struct ref_update *update, struct object_id *oid,
 			    oid_to_hex(oid),
 			    oid_to_hex(&update->old_oid));
 
-	return -1;
+	return ret;
 }
 
 /*
@@ -2603,9 +2607,13 @@ static int lock_ref_for_update(struct files_ref_store *refs,
 					ret = TRANSACTION_GENERIC_ERROR;
 					goto out;
 				}
-			} else if  (check_old_oid(update, &lock->old_oid, err)) {
-				ret = TRANSACTION_GENERIC_ERROR;
-				goto out;
+			} else {
+				int checkret;
+				checkret = check_old_oid(update, &lock->old_oid, err);
+				if  (checkret) {
+					ret = checkret;
+					goto out;
+				}
 			}
 		} else {
 			/*
@@ -2636,9 +2644,13 @@ static int lock_ref_for_update(struct files_ref_store *refs,
 				    update->old_target);
 			ret = TRANSACTION_GENERIC_ERROR;
 			goto out;
-		} else if  (check_old_oid(update, &lock->old_oid, err)) {
-			ret = TRANSACTION_GENERIC_ERROR;
-			goto out;
+		} else {
+			int checkret;
+			checkret = check_old_oid(update, &lock->old_oid, err);
+			if  (checkret) {
+				ret = checkret;
+				goto out;
+			}
 		}
 
 		/*
diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
index 32330b6bc6..c6b25ebac4 100644
--- a/refs/reftable-backend.c
+++ b/refs/reftable-backend.c
@@ -1206,10 +1206,13 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 				goto done;
 			}
 		} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {
-			if (is_null_oid(&u->old_oid))
+			ret = TRANSACTION_NAME_CONFLICT;
+			if (is_null_oid(&u->old_oid)) {
 				strbuf_addf(err, _("cannot lock ref '%s': "
 						   "reference already exists"),
 					    ref_update_original_update_refname(u));
+				ret = TRANSACTION_CREATE_EXISTS;
+			}
 			else if (is_null_oid(&current_oid))
 				strbuf_addf(err, _("cannot lock ref '%s': "
 						   "reference is missing but expected %s"),
@@ -1221,7 +1224,6 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 					    ref_update_original_update_refname(u),
 					    oid_to_hex(&current_oid),
 					    oid_to_hex(&u->old_oid));
-			ret = -1;
 			goto done;
 		}
 
-- 
2.47.0.6.g07cb02250a
karthik nayak· Oct 10, 2024, 21:12 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v6 4/6] transaction: add TRANSACTION_CREATE_EXISTS error

Bence Ferdinandy <bence@ferdinandy.com> writes:
> transaction: add TRANSACTION_CREATE_EXISTS error
Nit: but it would be nice to have s/transaction/refs here and similarly
in other patches. Phrasing 'Documentation/SubmittingPatches':
  It is also conventional in most cases to prefix the first line with
  "area: " where the area is a filename or identifier for the general
  area of the code being modified, e.g.
    * doc: clarify distinction between sign-off and pgp-signing
    * githooks.txt: improve the intro section
So in this sense, it would be the refs area, no?
Show 82 quoted lines
> Currently there is only one special error for transaction, for when
> there is a naming conflict, all other errors are dumped under a generic
> error. Add a new special error case for when the caller requests the
> reference to be updated only when it does not yet exist and the
> reference actually does exist.
>
> Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
> ---
>
> Notes:
>     v4: new patch
>     v5: no change
>     v6: no change
>
>  refs.h                  |  4 +++-
>  refs/files-backend.c    | 28 ++++++++++++++++++++--------
>  refs/reftable-backend.c |  6 ++++--
>  3 files changed, 27 insertions(+), 11 deletions(-)
>
> diff --git a/refs.h b/refs.h
> index f38616db84..166affbc89 100644
> --- a/refs.h
> +++ b/refs.h
> @@ -759,8 +759,10 @@ int ref_transaction_verify(struct ref_transaction *transaction,
>
>  /* Naming conflict (for example, the ref names A and A/B conflict). */
>  #define TRANSACTION_NAME_CONFLICT -1
> +/* When only creation was requested, but the ref already exists. */
> +#define TRANSACTION_CREATE_EXISTS -2
>  /* All other errors. */
> -#define TRANSACTION_GENERIC_ERROR -2
> +#define TRANSACTION_GENERIC_ERROR -3
>
>  /*
>   * Perform the preparatory stages of committing `transaction`. Acquire
> diff --git a/refs/files-backend.c b/refs/files-backend.c
> index 8415f2d020..272ad81315 100644
> --- a/refs/files-backend.c
> +++ b/refs/files-backend.c
> @@ -2502,14 +2502,18 @@ static int split_symref_update(struct ref_update *update,
>  static int check_old_oid(struct ref_update *update, struct object_id *oid,
>  			 struct strbuf *err)
>  {
> +	int ret = TRANSACTION_GENERIC_ERROR;
> +
>  	if (!(update->flags & REF_HAVE_OLD) ||
>  		   oideq(oid, &update->old_oid))
>  		return 0;
>
> -	if (is_null_oid(&update->old_oid))
> +	if (is_null_oid(&update->old_oid)) {
>  		strbuf_addf(err, "cannot lock ref '%s': "
>  			    "reference already exists",
>  			    ref_update_original_update_refname(update));
> +		ret = TRANSACTION_CREATE_EXISTS;
> +	}
>  	else if (is_null_oid(oid))
>  		strbuf_addf(err, "cannot lock ref '%s': "
>  			    "reference is missing but expected %s",
> @@ -2522,7 +2526,7 @@ static int check_old_oid(struct ref_update *update, struct object_id *oid,
>  			    oid_to_hex(oid),
>  			    oid_to_hex(&update->old_oid));
>
> -	return -1;
> +	return ret;
>  }
>
>  /*
> @@ -2603,9 +2607,13 @@ static int lock_ref_for_update(struct files_ref_store *refs,
>  					ret = TRANSACTION_GENERIC_ERROR;
>  					goto out;
>  				}
> -			} else if  (check_old_oid(update, &lock->old_oid, err)) {
> -				ret = TRANSACTION_GENERIC_ERROR;
> -				goto out;
> +			} else {
> +				int checkret;
> +				checkret = check_old_oid(update, &lock->old_oid, err);
> +				if  (checkret) {
> +					ret = checkret;
> +					goto out;
> +				}
Can't we simply do:
  ret = check_old_oid(update, &lock->old_oid, err);
  if (ret) {
     goto out
  }
if ret is '0', it shouldn't matter no?
[snip]
Junio C Hamano· Oct 11, 2024, 16:34 UTC · re: karthik nayak · lore

Re: [PATCH v6 4/6] transaction: add TRANSACTION_CREATE_EXISTS error

karthik nayak <karthik.188@gmail.com> writes:
Show 16 quoted lines
>> +			} else {
>> +				int checkret;
>> +				checkret = check_old_oid(update, &lock->old_oid, err);
>> +				if  (checkret) {
>> +					ret = checkret;
>> +					goto out;
>> +				}
>
> Can't we simply do:
>
>   ret = check_old_oid(update, &lock->old_oid, err);
>   if (ret) {
>      goto out
>   }
>
> if ret is '0', it shouldn't matter no?

That's nice. Yes, as long as "ret" has no useful information when we enter this "else" block, reusing it like you did is just fine.

This is one of these moments I tell myself "oh, why didn't *I* think of that" ;-).

Thanks.
Bence Ferdinandy· Oct 10, 2024, 13:29 UTC · re: Bence Ferdinandy · lore

[PATCH v6 3/6] set-head: better output for --auto

Currently, set-head --auto will print a message saying "remote/HEAD set to branch", which implies something was changed.

Change the output of --auto, so the output actually reflects what was done: a) set a previously unset HEAD, b) change HEAD because remote changed or c) no updates. As a fourth output, if HEAD is changed from a previous value that was not a remote branch, explicitly call attention to this fact.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v1-v2:
    
        was RFC in https://lore.kernel.org/git/20240910203835.2288291-1-bence@ferdinandy.com/
    
    v3:
    
        This patch was originally sent along when I thought set-head was
        going to be invoked by fetch, but the discussion on the RFC
        concluded that it should be not. This opened the possibility to make
        it more explicit.
    
        Note: although I feel both things the patch does are really just
        cosmetic, an argument could be made for breaking it into two, one
        for the no-op part and one for the --auto print update.
    
        Was sent in:
        https://lore.kernel.org/git/20240915221055.904107-1-bence@ferdinandy.com/
    
    v4:
        - changes are now handled atomically via the ref update transaction
        - outputs have changed along the lines of Junio's suggestion
        - minor refactor to set_head for improved legibility
    
    v5: - the minor refactor has been split out into its own patch
    
    v6: - fixed uninitialized prev_head
        - fixed case of unusual previous target
        - fixed a test that would have been actually broken at this patch
          (the output was only correct with the later update to fetch)
        - added 4 tests for the 4 output cases
 builtin/remote.c  | 34 +++++++++++++++++++++++++++----
 t/t5505-remote.sh | 52 ++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 81 insertions(+), 5 deletions(-)
Show changes to 2 files +81 −5

builtin/remote.c, t/t5505-remote.sh

diff --git a/builtin/remote.c b/builtin/remote.c
index 353ffd2c43..24f9caf149 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1399,10 +1399,34 @@ static int show(int argc, const char **argv, const char *prefix)
 	return result;
 }
 
+static void report_auto(const char *remote, const char *head_name,
+			struct strbuf *buf_prev) {
+	struct strbuf buf_prefix = STRBUF_INIT;
+	const char *prev_head = NULL;
+
+	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
+	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
+
+	if (prev_head && !strcmp(prev_head, head_name))
+		printf("'%s/HEAD' is unchanged and points to '%s'\n",
+			remote, head_name);
+	else if (prev_head)
+		printf("'%s/HEAD' has changed from '%s' and now points to '%s'\n",
+			remote, prev_head, head_name);
+	else if (buf_prev->len == 0)
+		printf("'%s/HEAD' is now created and points to '%s'\n",
+			remote, head_name);
+	else
+		printf("'%s/HEAD' used to point to '%s' "
+			"(which is unusual), but now points to '%s'\n",
+			remote, buf_prev->buf, head_name);
+}
+
 static int set_head(int argc, const char **argv, const char *prefix)
 {
 	int i, opt_a = 0, opt_d = 0, result = 0;
-	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;
+	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT,
+		buf_prev = STRBUF_INIT;
 	char *head_name = NULL;
 	struct ref_store *refs = get_main_ref_store(the_repository);
 
@@ -1445,15 +1469,17 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		/* make sure it's valid */
 		if (!refs_ref_exists(refs, buf2.buf))
 			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(refs, buf.buf, buf2.buf, "remote set-head", NULL))
+		else if (refs_update_symref(refs, buf.buf, buf2.buf, "remote set-head", &buf_prev))
 			result |= error(_("Could not setup %s"), buf.buf);
-		else if (opt_a)
-			printf("%s/HEAD set to %s\n", argv[0], head_name);
+		else if (opt_a) {
+			report_auto(argv[0], head_name, &buf_prev);
+		}
 		free(head_name);
 	}
 
 	strbuf_release(&buf);
 	strbuf_release(&buf2);
+	strbuf_release(&buf_prev);
 	return result;
 }
 
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 532035933f..77c12b8709 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -429,12 +429,51 @@ test_expect_success 'set-head --auto' '
 	)
 '
 
+test_expect_success 'set-head --auto detects creation' '
+	(
+		cd test &&
+		rm .git/refs/remotes/origin/HEAD &&
+		git remote set-head --auto origin >output &&
+		echo "'\''origin/HEAD'\'' is now created and points to '\''main'\''" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects no change' '
+	(
+		cd test &&
+		git remote set-head --auto origin >output &&
+		echo "'\''origin/HEAD'\'' is unchanged and points to '\''main'\''" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects change' '
+	(
+		cd test &&
+		echo "ref: refs/remotes/origin/ahead" >.git/refs/remotes/origin/HEAD &&
+		git remote set-head --auto origin >output &&
+		echo "'\''origin/HEAD'\'' has changed from '\''ahead'\'' and now points to '\''main'\''" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects strange ref' '
+	(
+		cd test &&
+		echo "ref: refs/heads/main" >.git/refs/remotes/origin/HEAD &&
+		git remote set-head --auto origin >output &&
+		echo "'\''origin/HEAD'\'' used to point to '\''refs/heads/main'\'' (which is unusual), but now points to '\''main'\''" >expect &&
+		test_cmp expect output
+	)
+'
+
 test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
 	(
 		cd test &&
 		git fetch two "refs/heads/*:refs/remotes/two/*" &&
 		git remote set-head --auto two >output 2>&1 &&
-		echo "two/HEAD set to main" >expect &&
+		echo "'\''two/HEAD'\'' is now created and points to '\''main'\''" >expect &&
 		test_cmp expect output
 	)
 '
@@ -453,6 +492,17 @@ test_expect_success 'set-head explicit' '
 	)
 '
 
+
+test_expect_success 'set-head --auto reports change' '
+	(
+		cd test &&
+		git remote set-head origin side2 &&
+		git remote set-head --auto origin >output 2>&1 &&
+		echo "'\''origin/HEAD'\'' has changed from '\''side2'\'' and now points to '\''main'\''" >expect &&
+		test_cmp expect output
+	)
+'
+
 cat >test/expect <<EOF
 Pruning origin
 URL: $(pwd)/one
-- 
2.47.0.6.g07cb02250a
karthik nayak· Oct 10, 2024, 21:05 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v6 3/6] set-head: better output for --auto

Bence Ferdinandy <bence@ferdinandy.com> writes: [snip]

Show 10 quoted lines
> diff --git a/builtin/remote.c b/builtin/remote.c
> index 353ffd2c43..24f9caf149 100644
> --- a/builtin/remote.c
> +++ b/builtin/remote.c
> @@ -1399,10 +1399,34 @@ static int show(int argc, const char **argv, const char *prefix)
>  	return result;
>  }
>
> +static void report_auto(const char *remote, const char *head_name,
> +			struct strbuf *buf_prev) {
Nit: would be nicer if this was called 'report_set_head_auto'
Show 13 quoted lines
> +	struct strbuf buf_prefix = STRBUF_INIT;
> +	const char *prev_head = NULL;
> +
> +	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
> +	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
> +
> +	if (prev_head && !strcmp(prev_head, head_name))
> +		printf("'%s/HEAD' is unchanged and points to '%s'\n",
> +			remote, head_name);
> +	else if (prev_head)
> +		printf("'%s/HEAD' has changed from '%s' and now points to '%s'\n",
> +			remote, prev_head, head_name);
> +	else if (buf_prev->len == 0)
Nit: we tend to do `if (!buf_prev->len)` in this project
Show 5 quoted lines
> +		printf("'%s/HEAD' is now created and points to '%s'\n",
> +			remote, head_name);
> +	else
> +		printf("'%s/HEAD' used to point to '%s' "
> +			"(which is unusual), but now points to '%s'\n",

Isn't "which is unusual" a bit vague? Wouldn't be nicer to say why it is unusual?

> +			remote, buf_prev->buf, head_name);
Let's clear up buf_prefix by calling `strbuf_release` here.
Show 44 quoted lines
> +}
> +
>  static int set_head(int argc, const char **argv, const char *prefix)
>  {
>  	int i, opt_a = 0, opt_d = 0, result = 0;
> -	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;
> +	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT,
> +		buf_prev = STRBUF_INIT;
>  	char *head_name = NULL;
>  	struct ref_store *refs = get_main_ref_store(the_repository);
>
> @@ -1445,15 +1469,17 @@ static int set_head(int argc, const char **argv, const char *prefix)
>  		/* make sure it's valid */
>  		if (!refs_ref_exists(refs, buf2.buf))
>  			result |= error(_("Not a valid ref: %s"), buf2.buf);
> -		else if (refs_update_symref(refs, buf.buf, buf2.buf, "remote set-head", NULL))
> +		else if (refs_update_symref(refs, buf.buf, buf2.buf, "remote set-head", &buf_prev))
>  			result |= error(_("Could not setup %s"), buf.buf);
> -		else if (opt_a)
> -			printf("%s/HEAD set to %s\n", argv[0], head_name);
> +		else if (opt_a) {
> +			report_auto(argv[0], head_name, &buf_prev);
> +		}
>  		free(head_name);
>  	}
>
>  	strbuf_release(&buf);
>  	strbuf_release(&buf2);
> +	strbuf_release(&buf_prev);
>  	return result;
>  }
>
> diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
> index 532035933f..77c12b8709 100755
> --- a/t/t5505-remote.sh
> +++ b/t/t5505-remote.sh
> @@ -429,12 +429,51 @@ test_expect_success 'set-head --auto' '
>  	)
>  '
>
> +test_expect_success 'set-head --auto detects creation' '
> +	(
> +		cd test &&
> +		rm .git/refs/remotes/origin/HEAD &&
does this work with reftables too?
> +		git remote set-head --auto origin >output &&
> +		echo "'\''origin/HEAD'\'' is now created and points to '\''main'\''" >expect &&
Nit: might be cleaner to use `${SQ}` here and below
[snip]
Bence Ferdinandy· Oct 11, 2024, 09:03 UTC · re: karthik nayak · lore

Re: [PATCH v6 3/6] set-head: better output for --auto

Thanks for the very helpful review on the series! Most of it is clear to me and I agree with, so for brevity I'll only comment where I have a question/comment.

On Thu Oct 10, 2024 at 23:05, karthik nayak <karthik.188@gmail.com> wrote: [snip]

Show 14 quoted lines
> > diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
> > index 532035933f..77c12b8709 100755
> > --- a/t/t5505-remote.sh
> > +++ b/t/t5505-remote.sh
> > @@ -429,12 +429,51 @@ test_expect_success 'set-head --auto' '
> >  	)
> >  '
> >
> > +test_expect_success 'set-head --auto detects creation' '
> > +	(
> > +		cd test &&
> > +		rm .git/refs/remotes/origin/HEAD &&
>
> does this work with reftables too?

You got me there, probably not. I'm guessing I should use git update-ref or something similar to set these values manually instead of editing the file.

On the other hand, is there a way to force the tests to run on a reftables backend? t/README does not mention anything about this and since in a previous round it already came up that I forgot to update the reftables backend, it would be nice if I could actually test everything with that as well.

Show 5 quoted lines
>
> > +		git remote set-head --auto origin >output &&
> > +		echo "'\''origin/HEAD'\'' is now created and points to '\''main'\''" >expect &&
>
> Nit: might be cleaner to use `${SQ}` here and below
You mean something like this?
	git remote set-head --auto origin >output &&
	HEAD="'\''origin/HEAD'\''" &&
	echo "${HEAD} is now created and points to '\''main'\''" >expect &&

I tried a few variations and this is what I could get working, but I may be simply missing something with the backtick.

Thanks, Bence

-- 
bence.ferdinandy.com
Junio C Hamano· Oct 11, 2024, 16:31 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v6 3/6] set-head: better output for --auto

"Bence Ferdinandy" <bence@ferdinandy.com> writes:
Show 23 quoted lines
> Thanks for the very helpful review on the series! Most of it is clear to me and
> I agree with, so for brevity I'll only comment where I have a question/comment.
>
> On Thu Oct 10, 2024 at 23:05, karthik nayak <karthik.188@gmail.com> wrote:
> [snip]
>
>> > diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
>> > index 532035933f..77c12b8709 100755
>> > --- a/t/t5505-remote.sh
>> > +++ b/t/t5505-remote.sh
>> > @@ -429,12 +429,51 @@ test_expect_success 'set-head --auto' '
>> >  	)
>> >  '
>> >
>> > +test_expect_success 'set-head --auto detects creation' '
>> > +	(
>> > +		cd test &&
>> > +		rm .git/refs/remotes/origin/HEAD &&
>>
>> does this work with reftables too?
>
> You got me there, probably not. I'm guessing I should use git update-ref or
> something similar to set these values manually instead of editing the file.
Yup, "git update-ref --no-deref -d refs/remotes/origin/HEAD"
> On the other hand, is there a way to force the tests to run on a reftables
> backend? t/README does not mention anything about this and since in a previous
> round it already came up that I forgot to update the reftables backend, it
> would be nice if I could actually test everything with that as well.
$ git grep GIT_TEST_DEFAULT_REF_FORMAT t/README
Thanks.
karthik nayak· Oct 11, 2024, 20:43 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v6 3/6] set-head: better output for --auto

"Bence Ferdinandy" <bence@ferdinandy.com> writes:
[snip]
Show 13 quoted lines
>>
>> > +		git remote set-head --auto origin >output &&
>> > +		echo "'\''origin/HEAD'\'' is now created and points to '\''main'\''" >expect &&
>>
>> Nit: might be cleaner to use `${SQ}` here and below
> You mean something like this?
>
> 	git remote set-head --auto origin >output &&
> 	HEAD="'\''origin/HEAD'\''" &&
> 	echo "${HEAD} is now created and points to '\''main'\''" >expect &&
>
> I tried a few variations and this is what I could get working, but I may be
> simply missing something with the backtick.
I mean simply this
    git remote set-head --auto origin >output &&
    echo "${SQ}origin/HEAD${SQ} is now created and points to
${SQ}main${SQ}" >expect &&
- Karthik
Bence Ferdinandy· Oct 12, 2024, 22:29 UTC · re: karthik nayak · lore

Re: [PATCH v6 3/6] set-head: better output for --auto

On Fri Oct 11, 2024 at 22:43, karthik nayak <karthik.188@gmail.com> wrote:
Show 23 quoted lines
> "Bence Ferdinandy" <bence@ferdinandy.com> writes:
>
> [snip]
>
>>>
>>> > +		git remote set-head --auto origin >output &&
>>> > +		echo "'\''origin/HEAD'\'' is now created and points to '\''main'\''" >expect &&
>>>
>>> Nit: might be cleaner to use `${SQ}` here and below
>> You mean something like this?
>>
>> 	git remote set-head --auto origin >output &&
>> 	HEAD="'\''origin/HEAD'\''" &&
>> 	echo "${HEAD} is now created and points to '\''main'\''" >expect &&
>>
>> I tried a few variations and this is what I could get working, but I may be
>> simply missing something with the backtick.
>
> I mean simply this
>
>     git remote set-head --auto origin >output &&
>     echo "${SQ}origin/HEAD${SQ} is now created and points to
> ${SQ}main${SQ}" >expect &&

Ah, I see in other tests this is used, but not in this particular test file. It's a bit hard to decide which is more cryptic, but ${SQ} is nicer on the eyes. On the other hand I would either switch the entire file in a separate patch or leave in the '\'' here as well. Or I guess one could go through the entire test base and switch everything to either one or the other for consistency.

karthik nayak· Oct 15, 2024, 07:51 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v6 3/6] set-head: better output for --auto

"Bence Ferdinandy" <bence@ferdinandy.com> writes:
Show 31 quoted lines
> On Fri Oct 11, 2024 at 22:43, karthik nayak <karthik.188@gmail.com> wrote:
>> "Bence Ferdinandy" <bence@ferdinandy.com> writes:
>>
>> [snip]
>>
>>>>
>>>> > +		git remote set-head --auto origin >output &&
>>>> > +		echo "'\''origin/HEAD'\'' is now created and points to '\''main'\''" >expect &&
>>>>
>>>> Nit: might be cleaner to use `${SQ}` here and below
>>> You mean something like this?
>>>
>>> 	git remote set-head --auto origin >output &&
>>> 	HEAD="'\''origin/HEAD'\''" &&
>>> 	echo "${HEAD} is now created and points to '\''main'\''" >expect &&
>>>
>>> I tried a few variations and this is what I could get working, but I may be
>>> simply missing something with the backtick.
>>
>> I mean simply this
>>
>>     git remote set-head --auto origin >output &&
>>     echo "${SQ}origin/HEAD${SQ} is now created and points to
>> ${SQ}main${SQ}" >expect &&
>
> Ah, I see in other tests this is used, but not in this particular test file.
> It's a bit hard to decide which is more cryptic, but ${SQ} is nicer on the
> eyes. On the other hand I would either switch the entire file in a separate
> patch or leave in the '\'' here as well. Or I guess one could go through the
> entire test base and switch everything to either one or the other for
> consistency.

I'm not sure I entirely agree with this sentiment. Consistency is a great goal to target, but it shouldn't hinder changes that are beneficial. In our case, if you make the first change, there is now reason for upcoming changes to the same file to also use '${SQ}' and eventually we can reach consistency of using '${SQ}' throughout the file.

- Karthik
Phillip Wood· Oct 15, 2024, 14:10 UTC · re: karthik nayak · lore

Re: [PATCH v6 3/6] set-head: better output for --auto

On 15/10/2024 08:51, karthik nayak wrote:
Show 19 quoted lines
> "Bence Ferdinandy" <bence@ferdinandy.com> writes:
>> On Fri Oct 11, 2024 at 22:43, karthik nayak <karthik.188@gmail.com> wrote:
>>> "Bence Ferdinandy" <bence@ferdinandy.com> writes:
>>> I mean simply this
>>>
>>>      git remote set-head --auto origin >output &&
>>>      echo "${SQ}origin/HEAD${SQ} is now created and points to
>>> ${SQ}main${SQ}" >expect &&
>>
>> Ah, I see in other tests this is used, but not in this particular test file.
>> It's a bit hard to decide which is more cryptic, but ${SQ} is nicer on the
>> eyes. On the other hand I would either switch the entire file in a separate
>> patch or leave in the '\'' here as well. Or I guess one could go through the
>> entire test base and switch everything to either one or the other for
>> consistency.
> 
> I'm not sure I entirely agree with this sentiment. Consistency is a
> great goal to target, but it shouldn't hinder changes that are
> beneficial.

Exactly - if we wait for an entire test file to be modernized before using our modern test idioms in it we'll be waiting forever. It is much better to introduce the use of things like ${SQ} that improve readability as we add new tests or modify existing ones.

Best Wishes
Phillip
  In our case, if you make the first change, there is now
> reason for upcoming changes to the same file to also use '${SQ}' and
> eventually we can reach consistency of using '${SQ}' throughout the file.
> 
> - Karthik
Bence Ferdinandy· Oct 15, 2024, 16:17 UTC · re: Phillip Wood · lore

Re: [PATCH v6 3/6] set-head: better output for --auto

On Tue Oct 15, 2024 at 16:10, Phillip Wood <phillip.wood123@gmail.com> wrote:
Show 12 quoted lines
> On 15/10/2024 08:51, karthik nayak wrote:
>> "Bence Ferdinandy" <bence@ferdinandy.com> writes:
>>> On Fri Oct 11, 2024 at 22:43, karthik nayak <karthik.188@gmail.com> wrote:
>> 
>> I'm not sure I entirely agree with this sentiment. Consistency is a
>> great goal to target, but it shouldn't hinder changes that are
>> beneficial.
>
> Exactly - if we wait for an entire test file to be modernized before 
> using our modern test idioms in it we'll be waiting forever. It is much 
> better to introduce the use of things like ${SQ} that improve 
> readability as we add new tests or modify existing ones.

Ok, I'll change the patch to use SQ. As I gather, it would be preferred if _all_ the tests would use ${SQ}? If this patch series is finally resolved I can send a patch for this style change, shouldn't be too complicated.

Bence Ferdinandy· Oct 10, 2024, 13:30 UTC · re: Bence Ferdinandy · lore

[PATCH v6 6/6] fetch: set remote/HEAD if it does not exist

If the user has remote/HEAD set already and it looks like it has changed on the server, then print a message, otherwise set it if we can. Silently pass if the user already has the same remote/HEAD set as reported by the server or if we encounter any errors along the way.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v3: - does not rely on remote set-head anymore so it only authenticates
        once
        - uses the new REF_CREATE_ONLY to atomically check if the ref exists
          and only write it if it doesn't
        - in all other cases the maximum it does is print a warning
    
    v4: - instead of the discarded REF_CREATE_ONLY, it uses the existing,
          but updated transaction api to request a silent create only
        - it now uses the atomic before_target to determine reporting
        - refactored for legibility
    
    v5: - instead of printing a not too useful message, it now fails
          silently, this in line with the objective to only set up
          remote/HEAD automatically if the right thing is trivial, for
          everything else there is remote set-head
        - fixed all failing tests
        - added two new tests, one for checking if remote/HEAD is set to the
          correct one, and one to test that we do not override remote/HEAD
          if it has changed on the server from what we have locally
    
    v6: - fixed style issues and unintended extra empty line
        - updated function call with bool to int from previous patch's
          change
        - removed calls to error(...) inherited from builtin/remote.c so we
          actually fail silently
        - set the test for remote set-head --auto to the correct value here,
          which was previously erronously set in the remote set-head patch
 builtin/fetch.c                  |  82 +++++++++++
 t/t4207-log-decoration-colors.sh |   3 +-
 t/t5505-remote.sh                |  13 +-
 t/t5510-fetch.sh                 | 229 ++++++++++++++++---------------
 t/t5512-ls-remote.sh             |   2 +
 t/t5514-fetch-multiple.sh        |  17 ++-
 t/t5516-fetch-push.sh            |   3 +-
 t/t5527-fetch-odd-refs.sh        |   3 +-
 t/t7900-maintenance.sh           |   3 +-
 t/t9210-scalar.sh                |   5 +-
 t/t9211-scalar-clone.sh          |   6 +-
 t/t9902-completion.sh            |  65 +++++++++
 12 files changed, 308 insertions(+), 123 deletions(-)
Show changes to 12 files +308 −123

builtin/fetch.c, t/t4207-log-decoration-colors.sh, t/t5505-remote.sh, t/t5510-fetch.sh, t/t5512-ls-remote.sh, t/t5514-fetch-multiple.sh, t/t5516-fetch-push.sh, t/t5527-fetch-odd-refs.sh, t/t7900-maintenance.sh, t/t9210-scalar.sh, t/t9211-scalar-clone.sh, t/t9902-completion.sh

diff --git a/builtin/fetch.c b/builtin/fetch.c
index 80a64d0d26..4c68f21be1 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1578,6 +1578,80 @@ static int backfill_tags(struct display_state *display_state,
 	return retcode;
 }
 
+static void report_set_head(const char *remote, const char *head_name,
+			struct strbuf *buf_prev) {
+	struct strbuf buf_prefix = STRBUF_INIT;
+	const char *prev_head = NULL;
+
+	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
+	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
+
+	if (prev_head && strcmp(prev_head, head_name)) {
+		printf("'HEAD' at '%s' has changed from '%s' to '%s'\n",
+			remote, prev_head, head_name);
+		printf("Run 'git remote set-head %s %s' to follow the change.\n",
+			remote, head_name);
+	}
+}
+
+static const char *strip_refshead(const char *name){
+	skip_prefix(name, "refs/heads/", &name);
+	return name;
+}
+
+static int set_head(const struct ref *remote_refs)
+{
+	int result = 0;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
+		b_local_head = STRBUF_INIT;
+	const char *remote = gtransport->remote->name;
+	char *head_name = NULL;
+	struct ref *ref, *matches;
+	struct ref *fetch_map = NULL, **fetch_map_tail = &fetch_map;
+	struct refspec_item refspec = {
+		.force = 0,
+		.pattern = 1,
+		.src = (char *) "refs/heads/*",
+		.dst = (char *) "refs/heads/*",
+	};
+	struct string_list heads = STRING_LIST_INIT_DUP;
+	struct ref_store *refs = get_main_ref_store(the_repository);
+
+	get_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);
+	matches = guess_remote_head(find_ref_by_name(remote_refs, "HEAD"),
+				    fetch_map, 1);
+	for (ref = matches; ref; ref = ref->next) {
+		string_list_append(&heads, strip_refshead(ref->name));
+	}
+
+
+	if (!heads.nr)
+		result = 1;
+	else if (heads.nr > 1)
+		result = 1;
+	else
+		head_name = xstrdup(heads.items[0].string);
+	if (head_name) {
+		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
+		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote, head_name);
+		/* make sure it's valid */
+		if (!refs_ref_exists(refs, b_remote_head.buf))
+			result = 1;
+		else if (refs_update_symref(refs, b_head.buf, b_remote_head.buf,
+					"remote set-head", &b_local_head, 1))
+			result = 1;
+		else
+			report_set_head(remote, head_name, &b_local_head);
+
+		free(head_name);
+	}
+
+	strbuf_release(&b_head);
+	strbuf_release(&b_local_head);
+	strbuf_release(&b_remote_head);
+	return result;
+}
+
 static int do_fetch(struct transport *transport,
 		    struct refspec *rs,
 		    const struct fetch_config *config)
@@ -1647,6 +1721,8 @@ static int do_fetch(struct transport *transport,
 				    "refs/tags/");
 	}
 
+	strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
+
 	if (must_list_refs) {
 		trace2_region_enter("fetch", "remote_refs", the_repository);
 		remote_refs = transport_get_remote_refs(transport,
@@ -1791,6 +1867,12 @@ static int do_fetch(struct transport *transport,
 				  "you need to specify exactly one branch with the --set-upstream option"));
 		}
 	}
+	if (set_head(remote_refs))
+		;
+		/*
+		 * Way too many cases where this can go wrong
+		 * so let's just fail silently for now.
+		 */
 
 cleanup:
 	if (retcode) {
diff --git a/t/t4207-log-decoration-colors.sh b/t/t4207-log-decoration-colors.sh
index 73ea9e5155..d55d22cb2f 100755
--- a/t/t4207-log-decoration-colors.sh
+++ b/t/t4207-log-decoration-colors.sh
@@ -59,7 +59,8 @@ ${c_reset}${c_tag}tag: ${c_reset}${c_tag}v1.0${c_reset}${c_commit}, \
 ${c_reset}${c_tag}tag: ${c_reset}${c_tag}B${c_reset}${c_commit})${c_reset} B
 ${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
 ${c_tag}tag: ${c_reset}${c_tag}A1${c_reset}${c_commit}, \
-${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit})${c_reset} A1
+${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit}, \
+${c_reset}${c_remoteBranch}other/HEAD${c_reset}${c_commit})${c_reset} A1
 	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
 ${c_stash}refs/stash${c_reset}${c_commit})${c_reset} On main: Changes to A.t
 	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 77c12b8709..4a62753af3 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -71,7 +71,7 @@ test_expect_success 'add another remote' '
 		cd test &&
 		git remote add -f second ../two &&
 		tokens_match "origin second" "$(git remote)" &&
-		check_tracking_branch second main side another &&
+		check_tracking_branch second main side another HEAD &&
 		git for-each-ref "--format=%(refname)" refs/remotes |
 		sed -e "/^refs\/remotes\/origin\//d" \
 		    -e "/^refs\/remotes\/second\//d" >actual &&
@@ -473,7 +473,7 @@ test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
 		cd test &&
 		git fetch two "refs/heads/*:refs/remotes/two/*" &&
 		git remote set-head --auto two >output 2>&1 &&
-		echo "'\''two/HEAD'\'' is now created and points to '\''main'\''" >expect &&
+		echo "'\''two/HEAD'\'' is unchanged and points to '\''main'\''" >expect &&
 		test_cmp expect output
 	)
 '
@@ -764,6 +764,7 @@ test_expect_success 'reject --no-no-tags' '
 cat >one/expect <<\EOF
   apis/main
   apis/side
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
@@ -781,6 +782,7 @@ test_expect_success 'update' '
 '
 
 cat >one/expect <<\EOF
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
@@ -793,7 +795,7 @@ EOF
 test_expect_success 'update with arguments' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -836,7 +838,7 @@ EOF
 test_expect_success 'update default' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -848,6 +850,7 @@ test_expect_success 'update default' '
 '
 
 cat >one/expect <<\EOF
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
@@ -870,7 +873,7 @@ test_expect_success 'update default (overridden, with funny whitespace)' '
 test_expect_success 'update (with remotes.default defined)' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index 0890b9f61c..dfc8d748ba 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -75,6 +75,30 @@ test_expect_success "fetch test for-merge" '
 	cut -f -2 .git/FETCH_HEAD >actual &&
 	test_cmp expected actual'
 
+test_expect_success "fetch test remote HEAD" '
+	cd "$D" &&
+	cd two &&
+	git fetch &&
+	git rev-parse --verify refs/remotes/origin/HEAD &&
+	git rev-parse --verify refs/remotes/origin/main &&
+	head=$(git rev-parse refs/remotes/origin/HEAD) &&
+	branch=$(git rev-parse refs/remotes/origin/main) &&
+	test "z$head" = "z$branch"'
+
+test_expect_success "fetch test remote HEAD change" '
+	cd "$D" &&
+	cd two &&
+	git switch -c other &&
+	git push -u origin other &&
+	git rev-parse --verify refs/remotes/origin/HEAD &&
+	git rev-parse --verify refs/remotes/origin/main &&
+	git rev-parse --verify refs/remotes/origin/other &&
+	git remote set-head origin other &&
+	git fetch &&
+	head=$(git rev-parse refs/remotes/origin/HEAD) &&
+	branch=$(git rev-parse refs/remotes/origin/other) &&
+	test "z$head" = "z$branch"'
+
 test_expect_success 'fetch --prune on its own works as expected' '
 	cd "$D" &&
 	git clone . prune &&
@@ -478,7 +502,6 @@ test_expect_success 'unbundle 1' '
 	test_must_fail git fetch "$D/bundle1" main:main
 '
 
-
 test_expect_success 'bundle 1 has only 3 files ' '
 	cd "$D" &&
 	test_bundle_object_count bundle1 3
@@ -819,7 +842,7 @@ test_expect_success 'fetch from multiple configured URLs in single remote' '
 
 # configured prune tests
 
-set_config_tristate () {
+set_config_tristate() {
 	# var=$1 val=$2
 	case "$2" in
 	unset)
@@ -833,12 +856,12 @@ set_config_tristate () {
 	esac
 }
 
-test_configured_prune () {
+test_configured_prune() {
 	test_configured_prune_type "$@" "name"
 	test_configured_prune_type "$@" "link"
 }
 
-test_configured_prune_type () {
+test_configured_prune_type() {
 	fetch_prune=$1
 	remote_origin_prune=$2
 	fetch_prune_tags=$3
@@ -848,8 +871,7 @@ test_configured_prune_type () {
 	cmdline=$7
 	mode=$8
 
-	if test -z "$cmdline_setup"
-	then
+	if test -z "$cmdline_setup"; then
 		test_expect_success 'setup cmdline_setup variable for subsequent test' '
 			remote_url="file://$(git -C one config remote.origin.url)" &&
 			remote_fetch="$(git -C one config remote.origin.fetch)" &&
@@ -857,12 +879,10 @@ test_configured_prune_type () {
 		'
 	fi
 
-	if test "$mode" = 'link'
-	then
+	if test "$mode" = 'link'; then
 		new_cmdline=""
 
-		if test "$cmdline" = ""
-		then
+		if test "$cmdline" = ""; then
 			new_cmdline=$cmdline_setup
 		else
 			new_cmdline=$(perl -e '
@@ -873,10 +893,8 @@ test_configured_prune_type () {
 		fi
 
 		if test "$fetch_prune_tags" = 'true' ||
-		   test "$remote_origin_prune_tags" = 'true'
-		then
-			if ! printf '%s' "$cmdline\n" | grep -q refs/remotes/origin/
-			then
+			test "$remote_origin_prune_tags" = 'true'; then
+			if ! printf '%s' "$cmdline\n" | grep -q refs/remotes/origin/; then
 				new_cmdline="$new_cmdline refs/tags/*:refs/tags/*"
 			fi
 		fi
@@ -946,100 +964,100 @@ test_configured_prune_type () {
 # $7 git-fetch $cmdline:
 #
 #                     $1    $2    $3    $4    $5     $6     $7
-test_configured_prune unset unset unset unset kept   kept   ""
-test_configured_prune unset unset unset unset kept   kept   "--no-prune"
-test_configured_prune unset unset unset unset pruned kept   "--prune"
-test_configured_prune unset unset unset unset kept   pruned \
+test_configured_prune unset unset unset unset kept kept ""
+test_configured_prune unset unset unset unset kept kept "--no-prune"
+test_configured_prune unset unset unset unset pruned kept "--prune"
+test_configured_prune unset unset unset unset kept pruned \
 	"--prune origin refs/tags/*:refs/tags/*"
 test_configured_prune unset unset unset unset pruned pruned \
 	"--prune origin refs/tags/*:refs/tags/* +refs/heads/*:refs/remotes/origin/*"
 
-test_configured_prune false unset unset unset kept   kept   ""
-test_configured_prune false unset unset unset kept   kept   "--no-prune"
-test_configured_prune false unset unset unset pruned kept   "--prune"
+test_configured_prune false unset unset unset kept kept ""
+test_configured_prune false unset unset unset kept kept "--no-prune"
+test_configured_prune false unset unset unset pruned kept "--prune"
 
-test_configured_prune true  unset unset unset pruned kept   ""
-test_configured_prune true  unset unset unset pruned kept   "--prune"
-test_configured_prune true  unset unset unset kept   kept   "--no-prune"
+test_configured_prune true unset unset unset pruned kept ""
+test_configured_prune true unset unset unset pruned kept "--prune"
+test_configured_prune true unset unset unset kept kept "--no-prune"
 
-test_configured_prune unset false unset unset kept   kept   ""
-test_configured_prune unset false unset unset kept   kept   "--no-prune"
-test_configured_prune unset false unset unset pruned kept   "--prune"
+test_configured_prune unset false unset unset kept kept ""
+test_configured_prune unset false unset unset kept kept "--no-prune"
+test_configured_prune unset false unset unset pruned kept "--prune"
 
-test_configured_prune false false unset unset kept   kept   ""
-test_configured_prune false false unset unset kept   kept   "--no-prune"
-test_configured_prune false false unset unset pruned kept   "--prune"
-test_configured_prune false false unset unset kept   pruned \
+test_configured_prune false false unset unset kept kept ""
+test_configured_prune false false unset unset kept kept "--no-prune"
+test_configured_prune false false unset unset pruned kept "--prune"
+test_configured_prune false false unset unset kept pruned \
 	"--prune origin refs/tags/*:refs/tags/*"
 test_configured_prune false false unset unset pruned pruned \
 	"--prune origin refs/tags/*:refs/tags/* +refs/heads/*:refs/remotes/origin/*"
 
-test_configured_prune true  false unset unset kept   kept   ""
-test_configured_prune true  false unset unset pruned kept   "--prune"
-test_configured_prune true  false unset unset kept   kept   "--no-prune"
+test_configured_prune true false unset unset kept kept ""
+test_configured_prune true false unset unset pruned kept "--prune"
+test_configured_prune true false unset unset kept kept "--no-prune"
 
-test_configured_prune unset true  unset unset pruned kept   ""
-test_configured_prune unset true  unset unset kept   kept   "--no-prune"
-test_configured_prune unset true  unset unset pruned kept   "--prune"
+test_configured_prune unset true unset unset pruned kept ""
+test_configured_prune unset true unset unset kept kept "--no-prune"
+test_configured_prune unset true unset unset pruned kept "--prune"
 
-test_configured_prune false true  unset unset pruned kept   ""
-test_configured_prune false true  unset unset kept   kept   "--no-prune"
-test_configured_prune false true  unset unset pruned kept   "--prune"
+test_configured_prune false true unset unset pruned kept ""
+test_configured_prune false true unset unset kept kept "--no-prune"
+test_configured_prune false true unset unset pruned kept "--prune"
 
-test_configured_prune true  true  unset unset pruned kept   ""
-test_configured_prune true  true  unset unset pruned kept   "--prune"
-test_configured_prune true  true  unset unset kept   kept   "--no-prune"
-test_configured_prune true  true  unset unset kept   pruned \
+test_configured_prune true true unset unset pruned kept ""
+test_configured_prune true true unset unset pruned kept "--prune"
+test_configured_prune true true unset unset kept kept "--no-prune"
+test_configured_prune true true unset unset kept pruned \
 	"--prune origin refs/tags/*:refs/tags/*"
-test_configured_prune true  true  unset unset pruned pruned \
+test_configured_prune true true unset unset pruned pruned \
 	"--prune origin refs/tags/*:refs/tags/* +refs/heads/*:refs/remotes/origin/*"
 
 # --prune-tags on its own does nothing, needs --prune as well, same
 # for fetch.pruneTags without fetch.prune
-test_configured_prune unset unset unset unset kept kept     "--prune-tags"
-test_configured_prune unset unset true unset  kept kept     ""
-test_configured_prune unset unset unset true  kept kept     ""
+test_configured_prune unset unset unset unset kept kept "--prune-tags"
+test_configured_prune unset unset true unset kept kept ""
+test_configured_prune unset unset unset true kept kept ""
 
 # These will prune the tags
 test_configured_prune unset unset unset unset pruned pruned "--prune --prune-tags"
-test_configured_prune true  unset true  unset pruned pruned ""
-test_configured_prune unset true  unset true  pruned pruned ""
+test_configured_prune true unset true unset pruned pruned ""
+test_configured_prune unset true unset true pruned pruned ""
 
 # remote.<name>.pruneTags overrides fetch.pruneTags, just like
 # remote.<name>.prune overrides fetch.prune if set.
-test_configured_prune true  unset true unset pruned pruned  ""
-test_configured_prune false true  false true  pruned pruned ""
-test_configured_prune true  false true  false kept   kept   ""
+test_configured_prune true unset true unset pruned pruned ""
+test_configured_prune false true false true pruned pruned ""
+test_configured_prune true false true false kept kept ""
 
 # When --prune-tags is supplied it's ignored if an explicit refspec is
 # given, same for the configuration options.
 test_configured_prune unset unset unset unset pruned kept \
 	"--prune --prune-tags origin +refs/heads/*:refs/remotes/origin/*"
-test_configured_prune unset unset true  unset pruned kept \
+test_configured_prune unset unset true unset pruned kept \
 	"--prune origin +refs/heads/*:refs/remotes/origin/*"
-test_configured_prune unset unset unset true pruned  kept \
+test_configured_prune unset unset unset true pruned kept \
 	"--prune origin +refs/heads/*:refs/remotes/origin/*"
 
 # Pruning that also takes place if a file:// url replaces a named
 # remote. However, because there's no implicit
 # +refs/heads/*:refs/remotes/origin/* refspec and supplying it on the
 # command-line negates --prune-tags, the branches will not be pruned.
-test_configured_prune_type unset unset unset unset kept   kept   "origin --prune-tags" "name"
-test_configured_prune_type unset unset unset unset kept   kept   "origin --prune-tags" "link"
+test_configured_prune_type unset unset unset unset kept kept "origin --prune-tags" "name"
+test_configured_prune_type unset unset unset unset kept kept "origin --prune-tags" "link"
 test_configured_prune_type unset unset unset unset pruned pruned "origin --prune --prune-tags" "name"
-test_configured_prune_type unset unset unset unset kept   pruned "origin --prune --prune-tags" "link"
+test_configured_prune_type unset unset unset unset kept pruned "origin --prune --prune-tags" "link"
 test_configured_prune_type unset unset unset unset pruned pruned "--prune --prune-tags origin" "name"
-test_configured_prune_type unset unset unset unset kept   pruned "--prune --prune-tags origin" "link"
-test_configured_prune_type unset unset true  unset pruned pruned "--prune origin" "name"
-test_configured_prune_type unset unset true  unset kept   pruned "--prune origin" "link"
-test_configured_prune_type unset unset unset true  pruned pruned "--prune origin" "name"
-test_configured_prune_type unset unset unset true  kept   pruned "--prune origin" "link"
-test_configured_prune_type true  unset true  unset pruned pruned "origin" "name"
-test_configured_prune_type true  unset true  unset kept   pruned "origin" "link"
-test_configured_prune_type unset  true true  unset pruned pruned "origin" "name"
-test_configured_prune_type unset  true true  unset kept   pruned "origin" "link"
-test_configured_prune_type unset  true unset true  pruned pruned "origin" "name"
-test_configured_prune_type unset  true unset true  kept   pruned "origin" "link"
+test_configured_prune_type unset unset unset unset kept pruned "--prune --prune-tags origin" "link"
+test_configured_prune_type unset unset true unset pruned pruned "--prune origin" "name"
+test_configured_prune_type unset unset true unset kept pruned "--prune origin" "link"
+test_configured_prune_type unset unset unset true pruned pruned "--prune origin" "name"
+test_configured_prune_type unset unset unset true kept pruned "--prune origin" "link"
+test_configured_prune_type true unset true unset pruned pruned "origin" "name"
+test_configured_prune_type true unset true unset kept pruned "origin" "link"
+test_configured_prune_type unset true true unset pruned pruned "origin" "name"
+test_configured_prune_type unset true true unset kept pruned "origin" "link"
+test_configured_prune_type unset true unset true pruned pruned "origin" "name"
+test_configured_prune_type unset true unset true kept pruned "origin" "link"
 
 # When all remote.origin.fetch settings are deleted a --prune
 # --prune-tags still implicitly supplies refs/tags/*:refs/tags/* so
@@ -1137,8 +1155,7 @@ test_expect_success 'fetching with auto-gc does not lock up' '
 	)
 '
 
-for section in fetch transfer
-do
+for section in fetch transfer; do
 	test_expect_success "$section.hideRefs affects connectivity check" '
 		GIT_TRACE="$PWD"/trace git -c $section.hideRefs=refs -c \
 			$section.hideRefs="!refs/tags/" fetch &&
@@ -1157,21 +1174,23 @@ test_expect_success 'prepare source branch' '
 	test 3 -le $(wc -l <actual)
 '
 
-validate_store_type () {
+validate_store_type() {
 	git -C dest count-objects -v >actual &&
-	case "$store_type" in
-	packed)
-		grep "^count: 0$" actual ;;
-	loose)
-		grep "^packs: 0$" actual ;;
-	esac || {
+		case "$store_type" in
+		packed)
+			grep "^count: 0$" actual
+			;;
+		loose)
+			grep "^packs: 0$" actual
+			;;
+		esac || {
 		echo "store_type is $store_type"
 		cat actual
 		false
 	}
 }
 
-test_unpack_limit () {
+test_unpack_limit() {
 	store_type=$1
 
 	case "$store_type" in
@@ -1192,43 +1211,39 @@ test_unpack_limit () {
 test_unpack_limit packed
 test_unpack_limit loose
 
-setup_negotiation_tip () {
+setup_negotiation_tip() {
 	SERVER="$1"
 	URL="$2"
 	USE_PROTOCOL_V2="$3"
 
 	rm -rf "$SERVER" client trace &&
-	git init -b main "$SERVER" &&
-	test_commit -C "$SERVER" alpha_1 &&
-	test_commit -C "$SERVER" alpha_2 &&
-	git -C "$SERVER" checkout --orphan beta &&
-	test_commit -C "$SERVER" beta_1 &&
-	test_commit -C "$SERVER" beta_2 &&
-
-	git clone "$URL" client &&
-
-	if test "$USE_PROTOCOL_V2" -eq 1
-	then
-		git -C "$SERVER" config protocol.version 2 &&
-		git -C client config protocol.version 2
-	fi &&
-
-	test_commit -C "$SERVER" beta_s &&
-	git -C "$SERVER" checkout main &&
-	test_commit -C "$SERVER" alpha_s &&
-	git -C "$SERVER" tag -d alpha_1 alpha_2 beta_1 beta_2
+		git init -b main "$SERVER" &&
+		test_commit -C "$SERVER" alpha_1 &&
+		test_commit -C "$SERVER" alpha_2 &&
+		git -C "$SERVER" checkout --orphan beta &&
+		test_commit -C "$SERVER" beta_1 &&
+		test_commit -C "$SERVER" beta_2 &&
+		git clone "$URL" client &&
+		if test "$USE_PROTOCOL_V2" -eq 1; then
+			git -C "$SERVER" config protocol.version 2 &&
+				git -C client config protocol.version 2
+		fi &&
+		test_commit -C "$SERVER" beta_s &&
+		git -C "$SERVER" checkout main &&
+		test_commit -C "$SERVER" alpha_s &&
+		git -C "$SERVER" tag -d alpha_1 alpha_2 beta_1 beta_2
 }
 
-check_negotiation_tip () {
+check_negotiation_tip() {
 	# Ensure that {alpha,beta}_1 are sent as "have", but not {alpha_beta}_2
 	ALPHA_1=$(git -C client rev-parse alpha_1) &&
-	grep "fetch> have $ALPHA_1" trace &&
-	BETA_1=$(git -C client rev-parse beta_1) &&
-	grep "fetch> have $BETA_1" trace &&
-	ALPHA_2=$(git -C client rev-parse alpha_2) &&
-	! grep "fetch> have $ALPHA_2" trace &&
-	BETA_2=$(git -C client rev-parse beta_2) &&
-	! grep "fetch> have $BETA_2" trace
+		grep "fetch> have $ALPHA_1" trace &&
+		BETA_1=$(git -C client rev-parse beta_1) &&
+		grep "fetch> have $BETA_1" trace &&
+		ALPHA_2=$(git -C client rev-parse alpha_2) &&
+		! grep "fetch> have $ALPHA_2" trace &&
+		BETA_2=$(git -C client rev-parse beta_2) &&
+		! grep "fetch> have $BETA_2" trace
 }
 
 test_expect_success '--negotiation-tip limits "have" lines sent' '
diff --git a/t/t5512-ls-remote.sh b/t/t5512-ls-remote.sh
index 64b3491e4e..1b3865e154 100755
--- a/t/t5512-ls-remote.sh
+++ b/t/t5512-ls-remote.sh
@@ -293,6 +293,8 @@ test_expect_success 'ls-remote with filtered symref (refname)' '
 	cat >expect <<-EOF &&
 	ref: refs/heads/main	HEAD
 	$rev	HEAD
+	ref: refs/remotes/origin/main	refs/remotes/origin/HEAD
+	$rev	refs/remotes/origin/HEAD
 	EOF
 	git ls-remote --symref . HEAD >actual &&
 	test_cmp expect actual
diff --git a/t/t5514-fetch-multiple.sh b/t/t5514-fetch-multiple.sh
index 579872c258..e3482b27b2 100755
--- a/t/t5514-fetch-multiple.sh
+++ b/t/t5514-fetch-multiple.sh
@@ -45,14 +45,17 @@ test_expect_success setup '
 '
 
 cat > test/expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
   origin/HEAD -> origin/main
   origin/main
   origin/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -97,6 +100,7 @@ cat > expect << EOF
   origin/HEAD -> origin/main
   origin/main
   origin/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
@@ -112,8 +116,10 @@ test_expect_success 'git fetch --multiple (but only one remote)' '
 '
 
 cat > expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -141,7 +147,7 @@ test_expect_success 'git fetch --multiple (bad remote names)' '
 
 test_expect_success 'git fetch --all (skipFetchAll)' '
 	(cd test4 &&
-	 for b in $(git branch -r)
+	 for b in $(git branch -r | grep -v HEAD)
 	 do
 		git branch -r -d $b || exit 1
 	 done &&
@@ -153,11 +159,14 @@ test_expect_success 'git fetch --all (skipFetchAll)' '
 '
 
 cat > expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -165,7 +174,7 @@ EOF
 
 test_expect_success 'git fetch --multiple (ignoring skipFetchAll)' '
 	(cd test4 &&
-	 for b in $(git branch -r)
+	 for b in $(git branch -r | grep -v HEAD)
 	 do
 		git branch -r -d $b || exit 1
 	 done &&
@@ -221,14 +230,17 @@ test_expect_success 'git fetch --multiple --jobs=0 picks a default' '
 
 create_fetch_all_expect () {
 	cat >expect <<-\EOF
+	  one/HEAD -> one/main
 	  one/main
 	  one/side
 	  origin/HEAD -> origin/main
 	  origin/main
 	  origin/side
+	  three/HEAD -> three/main
 	  three/another
 	  three/main
 	  three/side
+	  two/HEAD -> two/main
 	  two/another
 	  two/main
 	  two/side
@@ -265,6 +277,7 @@ test_expect_success 'git fetch (fetch all remotes with fetch.all = true)' '
 
 create_fetch_one_expect () {
 	cat >expect <<-\EOF
+	  one/HEAD -> one/main
 	  one/main
 	  one/side
 	  origin/HEAD -> origin/main
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 331778bd42..5a051aa0c7 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1395,7 +1395,8 @@ test_expect_success 'fetch follows tags by default' '
 		git tag -m "annotated" tag &&
 		git for-each-ref >tmp1 &&
 		sed -n "p; s|refs/heads/main$|refs/remotes/origin/main|p" tmp1 |
-		sort -k 3 >../expect
+		sed -n "p; s|refs/heads/main$|refs/remotes/origin/HEAD|p"  |
+		sort -k 4 >../expect
 	) &&
 	test_when_finished "rm -rf dst" &&
 	git init dst &&
diff --git a/t/t5527-fetch-odd-refs.sh b/t/t5527-fetch-odd-refs.sh
index 98ece27c6a..d3996af6ee 100755
--- a/t/t5527-fetch-odd-refs.sh
+++ b/t/t5527-fetch-odd-refs.sh
@@ -52,7 +52,8 @@ test_expect_success LONG_REF 'fetch handles extremely long refname' '
 	long
 	main
 	EOF
-	git for-each-ref --format="%(subject)" refs/remotes/long >actual &&
+	git for-each-ref --format="%(subject)" refs/remotes/long \
+		--exclude=refs/remotes/long/HEAD >actual &&
 	test_cmp expect actual
 '
 
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index a66d0e089d..88331d1ceb 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -329,7 +329,8 @@ test_expect_success 'incremental-repack task' '
 
 	# Delete refs that have not been repacked in these packs.
 	git for-each-ref --format="delete %(refname)" \
-		refs/prefetch refs/tags refs/remotes >refs &&
+		refs/prefetch refs/tags refs/remotes \
+		--exclude=refs/remotes/*/HEAD >refs &&
 	git update-ref --stdin <refs &&
 
 	# Replace the object directory with this pack layout.
diff --git a/t/t9210-scalar.sh b/t/t9210-scalar.sh
index 027235d61a..a81662713e 100755
--- a/t/t9210-scalar.sh
+++ b/t/t9210-scalar.sh
@@ -150,7 +150,8 @@ test_expect_success 'scalar clone' '
 			"$(pwd)" &&
 
 		git for-each-ref --format="%(refname)" refs/remotes/origin/ >actual &&
-		echo "refs/remotes/origin/parallel" >expect &&
+		echo "refs/remotes/origin/HEAD" >>expect &&
+		echo "refs/remotes/origin/parallel" >>expect &&
 		test_cmp expect actual &&
 
 		test_path_is_missing 1/2 &&
@@ -219,7 +220,7 @@ test_expect_success 'scalar reconfigure --all with includeIf.onbranch' '
 	done
 '
 
- test_expect_success 'scalar reconfigure --all with detached HEADs' '
+test_expect_success 'scalar reconfigure --all with detached HEADs' '
 	repos="two three four" &&
 	for num in $repos
 	do
diff --git a/t/t9211-scalar-clone.sh b/t/t9211-scalar-clone.sh
index 7869f45ee6..01f71910f5 100755
--- a/t/t9211-scalar-clone.sh
+++ b/t/t9211-scalar-clone.sh
@@ -31,7 +31,7 @@ test_expect_success 'set up repository to clone' '
 	)
 '
 
-cleanup_clone () {
+cleanup_clone() {
 	rm -rf "$1"
 }
 
@@ -127,7 +127,7 @@ test_expect_success '--single-branch clones HEAD only' '
 	(
 		cd $enlistment/src &&
 		git for-each-ref refs/remotes/origin >out &&
-		test_line_count = 1 out &&
+		test_line_count = 2 out &&
 		grep "refs/remotes/origin/base" out
 	) &&
 
@@ -141,7 +141,7 @@ test_expect_success '--no-single-branch clones all branches' '
 	(
 		cd $enlistment/src &&
 		git for-each-ref refs/remotes/origin >out &&
-		test_line_count = 2 out &&
+		test_line_count = 3 out &&
 		grep "refs/remotes/origin/base" out &&
 		grep "refs/remotes/origin/parallel" out
 	) &&
diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh
index cc6aa9f0cd..b663c4609e 100755
--- a/t/t9902-completion.sh
+++ b/t/t9902-completion.sh
@@ -658,6 +658,7 @@ test_expect_success '__git_refs - simple' '
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	matching-tag
@@ -673,6 +674,7 @@ test_expect_success '__git_refs - full refs' '
 	cat >expected <<-EOF &&
 	refs/heads/main
 	refs/heads/matching-branch
+	refs/remotes/other/HEAD
 	refs/remotes/other/branch-in-other
 	refs/remotes/other/main-in-other
 	refs/tags/matching-tag
@@ -729,6 +731,7 @@ test_expect_success '__git_refs - remote on local file system - full refs' '
 test_expect_success '__git_refs - configured remote' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -756,6 +759,7 @@ test_expect_success '__git_refs - configured remote - full refs' '
 test_expect_success '__git_refs - configured remote - repo given on the command line' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -787,6 +791,7 @@ test_expect_success '__git_refs - configured remote - full refs - repo given on
 test_expect_success '__git_refs - configured remote - remote name matches a directory' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -875,12 +880,14 @@ test_expect_success '__git_refs - unique remote branches for git checkout DWIMer
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/ambiguous
 	other/branch-in-other
 	other/main-in-other
 	remote/ambiguous
 	remote/branch-in-remote
 	matching-tag
+	HEAD
 	branch-in-other
 	branch-in-remote
 	main-in-other
@@ -904,6 +911,7 @@ test_expect_success '__git_refs - after --opt=' '
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	matching-tag
@@ -919,6 +927,7 @@ test_expect_success '__git_refs - after --opt= - full refs' '
 	cat >expected <<-EOF &&
 	refs/heads/main
 	refs/heads/matching-branch
+	refs/remotes/other/HEAD
 	refs/remotes/other/branch-in-other
 	refs/remotes/other/main-in-other
 	refs/tags/matching-tag
@@ -935,6 +944,7 @@ test_expect_success '__git refs - excluding refs' '
 	^HEAD
 	^main
 	^matching-branch
+	^other/HEAD
 	^other/branch-in-other
 	^other/main-in-other
 	^matching-tag
@@ -950,6 +960,7 @@ test_expect_success '__git refs - excluding full refs' '
 	cat >expected <<-EOF &&
 	^refs/heads/main
 	^refs/heads/matching-branch
+	^refs/remotes/other/HEAD
 	^refs/remotes/other/branch-in-other
 	^refs/remotes/other/main-in-other
 	^refs/tags/matching-tag
@@ -975,6 +986,7 @@ test_expect_success '__git_refs - do not filter refs unless told so' '
 	main
 	matching-branch
 	matching/branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	other/matching/branch-in-other
@@ -1095,6 +1107,7 @@ test_expect_success '__git_complete_refs - simple' '
 	HEAD Z
 	main Z
 	matching-branch Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	matching-tag Z
@@ -1123,6 +1136,7 @@ test_expect_success '__git_complete_refs - matching' '
 test_expect_success '__git_complete_refs - remote' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main-in-other Z
 	EOF
@@ -1139,9 +1153,11 @@ test_expect_success '__git_complete_refs - track' '
 	HEAD Z
 	main Z
 	matching-branch Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	matching-tag Z
+	HEAD Z
 	branch-in-other Z
 	main-in-other Z
 	EOF
@@ -1184,6 +1200,7 @@ test_expect_success '__git_complete_refs - suffix' '
 	HEAD.
 	main.
 	matching-branch.
+	other/HEAD.
 	other/branch-in-other.
 	other/main-in-other.
 	matching-tag.
@@ -1199,6 +1216,7 @@ test_expect_success '__git_complete_refs - suffix' '
 test_expect_success '__git_complete_fetch_refspecs - simple' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	HEAD:HEAD Z
+	HEAD:HEAD Z
 	branch-in-other:branch-in-other Z
 	main-in-other:main-in-other Z
 	EOF
@@ -1225,6 +1243,7 @@ test_expect_success '__git_complete_fetch_refspecs - matching' '
 test_expect_success '__git_complete_fetch_refspecs - prefix' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	+HEAD:HEAD Z
+	+HEAD:HEAD Z
 	+branch-in-other:branch-in-other Z
 	+main-in-other:main-in-other Z
 	EOF
@@ -1289,6 +1308,7 @@ test_expect_success '__git_complete_worktree_paths with -C' '
 
 test_expect_success 'git switch - with no options, complete local branches and unique remote branch names for DWIM logic' '
 	test_completion "git switch " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1435,11 +1455,13 @@ test_expect_success 'git-bisect - existing view subcommand is recognized and ena
 test_expect_success 'git checkout - completes refs and unique remote branches for DWIM' '
 	test_completion "git checkout " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1461,6 +1483,7 @@ test_expect_success 'git switch - with GIT_COMPLETION_CHECKOUT_NO_GUESS=1, compl
 
 test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_GUESS=1, complete local branches and unique remote names for DWIM logic' '
 	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git switch --guess " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1470,6 +1493,7 @@ test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_G
 
 test_expect_success 'git switch - a later --guess overrides previous --no-guess, complete local and remote unique branches for DWIM' '
 	test_completion "git switch --no-guess --guess " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1490,6 +1514,7 @@ test_expect_success 'git checkout - with GIT_COMPLETION_NO_GUESS=1 only complete
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1498,11 +1523,13 @@ test_expect_success 'git checkout - with GIT_COMPLETION_NO_GUESS=1 only complete
 test_expect_success 'git checkout - --guess overrides GIT_COMPLETION_NO_GUESS=1, complete refs and unique remote branches for DWIM' '
 	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git checkout --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1514,6 +1541,7 @@ test_expect_success 'git checkout - with --no-guess, only completes refs' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1522,11 +1550,13 @@ test_expect_success 'git checkout - with --no-guess, only completes refs' '
 test_expect_success 'git checkout - a later --guess overrides previous --no-guess, complete refs and unique remote branches for DWIM' '
 	test_completion "git checkout --no-guess --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1538,6 +1568,7 @@ test_expect_success 'git checkout - a later --no-guess overrides previous --gues
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1550,6 +1581,7 @@ test_expect_success 'git checkout - with checkout.guess = false, only completes
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1559,11 +1591,13 @@ test_expect_success 'git checkout - with checkout.guess = true, completes refs a
 	test_config checkout.guess true &&
 	test_completion "git checkout " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1573,11 +1607,13 @@ test_expect_success 'git checkout - a later --guess overrides previous checkout.
 	test_config checkout.guess false &&
 	test_completion "git checkout --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1590,6 +1626,7 @@ test_expect_success 'git checkout - a later --no-guess overrides previous checko
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1601,6 +1638,7 @@ test_expect_success 'git switch - with --detach, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1612,6 +1650,7 @@ test_expect_success 'git checkout - with --detach, complete only references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1783,6 +1822,7 @@ test_expect_success 'git switch - with -d, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1794,6 +1834,7 @@ test_expect_success 'git checkout - with -d, complete only references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1801,10 +1842,12 @@ test_expect_success 'git checkout - with -d, complete only references' '
 
 test_expect_success 'git switch - with --track, complete only remote branches' '
 	test_completion "git switch --track " <<-\EOF &&
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
 	test_completion "git switch -t " <<-\EOF
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1812,10 +1855,12 @@ test_expect_success 'git switch - with --track, complete only remote branches' '
 
 test_expect_success 'git checkout - with --track, complete only remote branches' '
 	test_completion "git checkout --track " <<-\EOF &&
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
 	test_completion "git checkout -t " <<-\EOF
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1834,6 +1879,7 @@ test_expect_success 'git checkout - with --no-track, complete only local referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1845,6 +1891,7 @@ test_expect_success 'git switch - with -c, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1856,6 +1903,7 @@ test_expect_success 'git switch - with -C, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1867,6 +1915,7 @@ test_expect_success 'git switch - with -c and --track, complete all references'
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1878,6 +1927,7 @@ test_expect_success 'git switch - with -C and --track, complete all references'
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1889,6 +1939,7 @@ test_expect_success 'git switch - with -c and --no-track, complete all reference
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1900,6 +1951,7 @@ test_expect_success 'git switch - with -C and --no-track, complete all reference
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1911,6 +1963,7 @@ test_expect_success 'git checkout - with -b, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1922,6 +1975,7 @@ test_expect_success 'git checkout - with -B, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1933,6 +1987,7 @@ test_expect_success 'git checkout - with -b and --track, complete all references
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1944,6 +1999,7 @@ test_expect_success 'git checkout - with -B and --track, complete all references
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1955,6 +2011,7 @@ test_expect_success 'git checkout - with -b and --no-track, complete all referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1966,6 +2023,7 @@ test_expect_success 'git checkout - with -B and --no-track, complete all referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1973,6 +2031,7 @@ test_expect_success 'git checkout - with -B and --no-track, complete all referen
 
 test_expect_success 'git switch - for -c, complete local branches and unique remote branches' '
 	test_completion "git switch -c " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1982,6 +2041,7 @@ test_expect_success 'git switch - for -c, complete local branches and unique rem
 
 test_expect_success 'git switch - for -C, complete local branches and unique remote branches' '
 	test_completion "git switch -C " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2019,6 +2079,7 @@ test_expect_success 'git switch - for -C with --no-track, complete local branche
 
 test_expect_success 'git checkout - for -b, complete local branches and unique remote branches' '
 	test_completion "git checkout -b " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2028,6 +2089,7 @@ test_expect_success 'git checkout - for -b, complete local branches and unique r
 
 test_expect_success 'git checkout - for -B, complete local branches and unique remote branches' '
 	test_completion "git checkout -B " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2065,6 +2127,7 @@ test_expect_success 'git checkout - for -B with --no-track, complete local branc
 
 test_expect_success 'git switch - with --orphan completes local branch names and unique remote branch names' '
 	test_completion "git switch --orphan " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2080,6 +2143,7 @@ test_expect_success 'git switch - --orphan with branch already provided complete
 
 test_expect_success 'git checkout - with --orphan completes local branch names and unique remote branch names' '
 	test_completion "git checkout --orphan " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2093,6 +2157,7 @@ test_expect_success 'git checkout - --orphan with branch already provided comple
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
-- 
2.47.0.6.g07cb02250a
Bence Ferdinandy· Oct 10, 2024, 13:30 UTC · re: Bence Ferdinandy · lore

[PATCH v6 5/6] refs_update_symref: add create_only option

Allow the caller to specify that it only wants to update the symref if it does not already exist. Silently ignore the error from the transaction API if the symref already exists.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    v5: no change
    
    v6: - switched from bool to int for create_only
        - refactored logic in refs_update_symref with goto as layed out by
          Junio
 builtin/branch.c          |  2 +-
 builtin/checkout.c        |  5 +++--
 builtin/clone.c           |  8 +++++---
 builtin/notes.c           |  3 ++-
 builtin/remote.c          |  9 ++++++---
 builtin/symbolic-ref.c    |  2 +-
 builtin/worktree.c        |  2 +-
 refs.c                    | 36 +++++++++++++++++++++++++-----------
 refs.h                    |  2 +-
 reset.c                   |  2 +-
 sequencer.c               |  3 ++-
 setup.c                   |  3 ++-
 t/helper/test-ref-store.c |  2 +-
 13 files changed, 51 insertions(+), 28 deletions(-)
Show changes to 13 files +51 −28

builtin/branch.c, builtin/checkout.c, builtin/clone.c, builtin/notes.c, builtin/remote.c, builtin/symbolic-ref.c, builtin/worktree.c, refs.c, refs.h, reset.c, sequencer.c, setup.c, t/helper/test-ref-store.c

diff --git a/builtin/branch.c b/builtin/branch.c
index 6c87690b58..2fcbcbfd59 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -559,7 +559,7 @@ static int replace_each_worktree_head_symref(struct worktree **worktrees,
 			continue;
 
 		refs = get_worktree_ref_store(worktrees[i]);
-		if (refs_update_symref(refs, "HEAD", newref, logmsg, NULL))
+		if (refs_update_symref(refs, "HEAD", newref, logmsg, NULL, 0))
 			ret = error(_("HEAD of working tree %s is not updated"),
 				    worktrees[i]->path);
 	}
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 356ee9bcde..9abb71dc07 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -1015,7 +1015,8 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 			describe_detached_head(_("HEAD is now at"), new_branch_info->commit);
 		}
 	} else if (new_branch_info->path) {	/* Switch branches. */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path, msg.buf, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path,
+					msg.buf, NULL, 0) < 0)
 			die(_("unable to update HEAD"));
 		if (!opts->quiet) {
 			if (old_branch_info->path && !strcmp(new_branch_info->path, old_branch_info->path)) {
@@ -1479,7 +1480,7 @@ static int switch_unborn_to_new_branch(const struct checkout_opts *opts)
 		die(_("You are on a branch yet to be born"));
 	strbuf_addf(&branch_ref, "refs/heads/%s", opts->new_branch);
 	status = refs_update_symref(get_main_ref_store(the_repository),
-				    "HEAD", branch_ref.buf, "checkout -b", NULL);
+				    "HEAD", branch_ref.buf, "checkout -b", NULL, 0);
 	strbuf_release(&branch_ref);
 	if (!opts->quiet)
 		fprintf(stderr, _("Switched to a new branch '%s'\n"),
diff --git a/builtin/clone.c b/builtin/clone.c
index ead2af20ea..87e8f1421a 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -661,7 +661,7 @@ static void update_remote_refs(const struct ref *refs,
 		strbuf_addstr(&head_ref, "HEAD");
 		if (refs_update_symref(get_main_ref_store(the_repository), head_ref.buf,
 				       remote_head_points_at->peer_ref->name,
-				       msg, NULL) < 0)
+				       msg, NULL, 0) < 0)
 			die(_("unable to update %s"), head_ref.buf);
 		strbuf_release(&head_ref);
 	}
@@ -673,7 +673,8 @@ static void update_head(const struct ref *our, const struct ref *remote,
 	const char *head;
 	if (our && skip_prefix(our->name, "refs/heads/", &head)) {
 		/* Local default branch link */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name, NULL, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name,
+					NULL, NULL, 0) < 0)
 			die(_("unable to update HEAD"));
 		if (!option_bare) {
 			refs_update_ref(get_main_ref_store(the_repository),
@@ -702,7 +703,8 @@ static void update_head(const struct ref *our, const struct ref *remote,
 		 * Unborn head from remote; same as "our" case above except
 		 * that we have no ref to update.
 		 */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn, NULL, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn,
+					NULL, NULL, 0) < 0)
 			die(_("unable to update HEAD"));
 		if (!option_bare)
 			install_branch_config(0, head, remote_name, unborn);
diff --git a/builtin/notes.c b/builtin/notes.c
index ba646f06ff..7b3138e3c3 100644
--- a/builtin/notes.c
+++ b/builtin/notes.c
@@ -980,7 +980,8 @@ static int merge(int argc, const char **argv, const char *prefix)
 			die(_("a notes merge into %s is already in-progress at %s"),
 			    notes_ref, wt->path);
 		free_worktrees(worktrees);
-		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", notes_ref, NULL, NULL))
+		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", notes_ref,
+					NULL, NULL, 0))
 			die(_("failed to store link to current notes ref (%s)"),
 			    notes_ref);
 		fprintf(stderr, _("Automatic notes merge failed. Fix conflicts in %s "
diff --git a/builtin/remote.c b/builtin/remote.c
index 24f9caf149..9774d8ffe3 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -244,7 +244,8 @@ static int add(int argc, const char **argv, const char *prefix)
 		strbuf_reset(&buf2);
 		strbuf_addf(&buf2, "refs/remotes/%s/%s", name, master);
 
-		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote add", NULL))
+		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf,
+					"remote add", NULL, 0))
 			result = error(_("Could not setup master '%s'"), master);
 	}
 
@@ -864,7 +865,8 @@ static int mv(int argc, const char **argv, const char *prefix)
 		strbuf_reset(&buf3);
 		strbuf_addf(&buf3, "remote: renamed %s to %s",
 				item->string, buf.buf);
-		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, buf3.buf, NULL))
+		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf,
+					buf3.buf, NULL, 0))
 			die(_("creating '%s' failed"), buf.buf);
 		display_progress(progress, ++refs_renamed_nr);
 	}
@@ -1469,7 +1471,8 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		/* make sure it's valid */
 		if (!refs_ref_exists(refs, buf2.buf))
 			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(refs, buf.buf, buf2.buf, "remote set-head", &buf_prev))
+		else if (refs_update_symref(refs, buf.buf, buf2.buf,
+					"remote set-head", &buf_prev, 0))
 			result |= error(_("Could not setup %s"), buf.buf);
 		else if (opt_a) {
 			report_auto(argv[0], head_name, &buf_prev);
diff --git a/builtin/symbolic-ref.c b/builtin/symbolic-ref.c
index 7728fbc3c1..cfbb53b30c 100644
--- a/builtin/symbolic-ref.c
+++ b/builtin/symbolic-ref.c
@@ -88,7 +88,7 @@ int cmd_symbolic_ref(int argc,
 		if (check_refname_format(argv[1], REFNAME_ALLOW_ONELEVEL) < 0)
 			die("Refusing to set '%s' to invalid ref '%s'", argv[0], argv[1]);
 		ret = !!refs_update_symref(get_main_ref_store(the_repository),
-					   argv[0], argv[1], msg, NULL);
+					   argv[0], argv[1], msg, NULL, 0);
 		break;
 	default:
 		usage_with_options(git_symbolic_ref_usage, options);
diff --git a/builtin/worktree.c b/builtin/worktree.c
index a7ab4193c1..2e95367235 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -517,7 +517,7 @@ static int add_worktree(const char *path, const char *refname,
 		ret = refs_update_ref(wt_refs, NULL, "HEAD", &commit->object.oid,
 				      NULL, 0, UPDATE_REFS_MSG_ON_ERR);
 	else
-		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, NULL, NULL);
+		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, NULL, NULL, 0);
 	if (ret)
 		goto done;
 
diff --git a/refs.c b/refs.c
index 91cacee6f9..e23288b817 100644
--- a/refs.c
+++ b/refs.c
@@ -2115,29 +2115,43 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
 
 int refs_update_symref(struct ref_store *refs, const char *ref,
 		       const char *target, const char *logmsg,
-		       struct strbuf *before_target)
+		       struct strbuf *before_target, int create_only)
 {
 	struct ref_transaction *transaction;
 	struct strbuf err = STRBUF_INIT;
 	int ret = 0;
 
 	transaction = ref_store_transaction_begin(refs, &err);
-	if (!transaction ||
-	    ref_transaction_update(transaction, ref, NULL, NULL,
-				   target, NULL, REF_NO_DEREF,
-				   logmsg, &err) ||
-	    ref_transaction_commit(transaction, &err)) {
+	if (!transaction) {
+	error_return:
 		ret = error("%s", err.buf);
+		goto cleanup;
 	}
+	if (create_only) {
+		int create_ret = 0;
+		if (ref_transaction_create(transaction, ref, NULL, target,
+					   REF_NO_DEREF, logmsg, &err))
+			goto error_return;
+		create_ret = ref_transaction_commit(transaction, &err);
+		if (create_ret && create_ret != TRANSACTION_CREATE_EXISTS)
+			ret = error("%s", err.buf);
 
-	strbuf_release(&err);
+	} else {
+		if (ref_transaction_update(transaction, ref, NULL, NULL,
+					   target, NULL, REF_NO_DEREF,
+					   logmsg, &err) ||
+		    ref_transaction_commit(transaction, &err))
+			goto error_return;
+	}
 
-	if (transaction && before_target && transaction->updates[0]->before_target)
-		strbuf_addstr(before_target, transaction->updates[0]->before_target);
+cleanup:
+	strbuf_release(&err);
 
-	if (transaction)
+	if (transaction) {
+		if (before_target && transaction->updates[0]->before_target)
+			strbuf_addstr(before_target, transaction->updates[0]->before_target);
 		ref_transaction_free(transaction);
-
+	}
 	return ret;
 }
 
diff --git a/refs.h b/refs.h
index 166affbc89..35f5439cd1 100644
--- a/refs.h
+++ b/refs.h
@@ -572,7 +572,7 @@ int refs_copy_existing_ref(struct ref_store *refs, const char *oldref,
 
 int refs_update_symref(struct ref_store *refs, const char *refname,
 		       const char *target, const char *logmsg,
-		       struct strbuf *before_target);
+		       struct strbuf *before_target, int create_only);
 
 enum action_on_err {
 	UPDATE_REFS_MSG_ON_ERR,
diff --git a/reset.c b/reset.c
index cc36a9ed56..919fb84f5f 100644
--- a/reset.c
+++ b/reset.c
@@ -76,7 +76,7 @@ static int update_refs(const struct reset_head_opts *opts,
 		if (!ret)
 			ret = refs_update_symref(get_main_ref_store(the_repository),
 						 "HEAD", switch_to_branch,
-						 reflog_head, NULL);
+						 reflog_head, NULL, 0);
 	}
 	if (!ret && run_hook)
 		run_hooks_l(the_repository, "post-checkout",
diff --git a/sequencer.c b/sequencer.c
index 23b162924c..103d0ddfb2 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -5107,7 +5107,8 @@ static int pick_commits(struct repository *r,
 			}
 			msg = reflog_message(opts, "finish", "returning to %s",
 				head_ref.buf);
-			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf, msg, NULL)) {
+			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf,
+						msg, NULL, 0)) {
 				res = error(_("could not update HEAD to %s"),
 					head_ref.buf);
 				goto cleanup_head_ref;
diff --git a/setup.c b/setup.c
index d95f051465..9bbc655ee4 100644
--- a/setup.c
+++ b/setup.c
@@ -2275,7 +2275,8 @@ void create_reference_database(enum ref_storage_format ref_storage_format,
 			die(_("invalid initial branch name: '%s'"),
 			    initial_branch);
 
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref, NULL, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref,
+					NULL, NULL, 0) < 0)
 			exit(1);
 		free(ref);
 	}
diff --git a/t/helper/test-ref-store.c b/t/helper/test-ref-store.c
index a911302bea..f72b029e51 100644
--- a/t/helper/test-ref-store.c
+++ b/t/helper/test-ref-store.c
@@ -120,7 +120,7 @@ static int cmd_create_symref(struct ref_store *refs, const char **argv)
 	const char *target = notnull(*argv++, "target");
 	const char *logmsg = *argv++;
 
-	return refs_update_symref(refs, refname, target, logmsg, NULL);
+	return refs_update_symref(refs, refname, target, logmsg, NULL, 0);
 }
 
 static struct flag_definition transaction_flags[] = {
-- 
2.47.0.6.g07cb02250a
Bence Ferdinandy· Oct 12, 2024, 23:03 UTC · re: Bence Ferdinandy · lore

[PATCH v7 1/6] refs: atomically record overwritten ref in update_symref

When updating a symref it's currently not possible to know for sure what was the previous value that was overwritten. Record the value after the ref has been locked if the caller of refs_update_symref requests it via a new variable in the function call.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    
    v5: - added before_target to reftables backend
        - added an extra safety check for transaction's existence in refs.c
    
    v6: - no change
    
    v7: - remove the whole before_target concept from the backends and
          handle checking it in refs.c instead (thanks Karthik)
        - rename the before_target to referent which is how the same concept
          is called in the backends
        - change commit prefix to be more in line with project standards
 builtin/branch.c          |  2 +-
 builtin/checkout.c        |  4 ++--
 builtin/clone.c           |  6 +++---
 builtin/notes.c           |  2 +-
 builtin/remote.c          |  6 +++---
 builtin/symbolic-ref.c    |  2 +-
 builtin/worktree.c        |  2 +-
 refs.c                    | 16 +++++++++++-----
 refs.h                    |  3 ++-
 reset.c                   |  2 +-
 sequencer.c               |  2 +-
 setup.c                   |  2 +-
 t/helper/test-ref-store.c |  2 +-
 13 files changed, 29 insertions(+), 22 deletions(-)
Show changes to 13 files +29 −22

builtin/branch.c, builtin/checkout.c, builtin/clone.c, builtin/notes.c, builtin/remote.c, builtin/symbolic-ref.c, builtin/worktree.c, refs.c, refs.h, reset.c, sequencer.c, setup.c, t/helper/test-ref-store.c

diff --git a/builtin/branch.c b/builtin/branch.c
index fd1611ebf5..6c87690b58 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -559,7 +559,7 @@ static int replace_each_worktree_head_symref(struct worktree **worktrees,
 			continue;
 
 		refs = get_worktree_ref_store(worktrees[i]);
-		if (refs_update_symref(refs, "HEAD", newref, logmsg))
+		if (refs_update_symref(refs, "HEAD", newref, logmsg, NULL))
 			ret = error(_("HEAD of working tree %s is not updated"),
 				    worktrees[i]->path);
 	}
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 9c30000d3a..356ee9bcde 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -1015,7 +1015,7 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 			describe_detached_head(_("HEAD is now at"), new_branch_info->commit);
 		}
 	} else if (new_branch_info->path) {	/* Switch branches. */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path, msg.buf) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path, msg.buf, NULL) < 0)
 			die(_("unable to update HEAD"));
 		if (!opts->quiet) {
 			if (old_branch_info->path && !strcmp(new_branch_info->path, old_branch_info->path)) {
@@ -1479,7 +1479,7 @@ static int switch_unborn_to_new_branch(const struct checkout_opts *opts)
 		die(_("You are on a branch yet to be born"));
 	strbuf_addf(&branch_ref, "refs/heads/%s", opts->new_branch);
 	status = refs_update_symref(get_main_ref_store(the_repository),
-				    "HEAD", branch_ref.buf, "checkout -b");
+				    "HEAD", branch_ref.buf, "checkout -b", NULL);
 	strbuf_release(&branch_ref);
 	if (!opts->quiet)
 		fprintf(stderr, _("Switched to a new branch '%s'\n"),
diff --git a/builtin/clone.c b/builtin/clone.c
index e77339c847..ead2af20ea 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -661,7 +661,7 @@ static void update_remote_refs(const struct ref *refs,
 		strbuf_addstr(&head_ref, "HEAD");
 		if (refs_update_symref(get_main_ref_store(the_repository), head_ref.buf,
 				       remote_head_points_at->peer_ref->name,
-				       msg) < 0)
+				       msg, NULL) < 0)
 			die(_("unable to update %s"), head_ref.buf);
 		strbuf_release(&head_ref);
 	}
@@ -673,7 +673,7 @@ static void update_head(const struct ref *our, const struct ref *remote,
 	const char *head;
 	if (our && skip_prefix(our->name, "refs/heads/", &head)) {
 		/* Local default branch link */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name, NULL, NULL) < 0)
 			die(_("unable to update HEAD"));
 		if (!option_bare) {
 			refs_update_ref(get_main_ref_store(the_repository),
@@ -702,7 +702,7 @@ static void update_head(const struct ref *our, const struct ref *remote,
 		 * Unborn head from remote; same as "our" case above except
 		 * that we have no ref to update.
 		 */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn, NULL, NULL) < 0)
 			die(_("unable to update HEAD"));
 		if (!option_bare)
 			install_branch_config(0, head, remote_name, unborn);
diff --git a/builtin/notes.c b/builtin/notes.c
index 8c26e45526..ba646f06ff 100644
--- a/builtin/notes.c
+++ b/builtin/notes.c
@@ -980,7 +980,7 @@ static int merge(int argc, const char **argv, const char *prefix)
 			die(_("a notes merge into %s is already in-progress at %s"),
 			    notes_ref, wt->path);
 		free_worktrees(worktrees);
-		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", notes_ref, NULL))
+		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", notes_ref, NULL, NULL))
 			die(_("failed to store link to current notes ref (%s)"),
 			    notes_ref);
 		fprintf(stderr, _("Automatic notes merge failed. Fix conflicts in %s "
diff --git a/builtin/remote.c b/builtin/remote.c
index 76670ddd8b..d8ff440027 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -244,7 +244,7 @@ static int add(int argc, const char **argv, const char *prefix)
 		strbuf_reset(&buf2);
 		strbuf_addf(&buf2, "refs/remotes/%s/%s", name, master);
 
-		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote add"))
+		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote add", NULL))
 			result = error(_("Could not setup master '%s'"), master);
 	}
 
@@ -864,7 +864,7 @@ static int mv(int argc, const char **argv, const char *prefix)
 		strbuf_reset(&buf3);
 		strbuf_addf(&buf3, "remote: renamed %s to %s",
 				item->string, buf.buf);
-		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, buf3.buf))
+		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, buf3.buf, NULL))
 			die(_("creating '%s' failed"), buf.buf);
 		display_progress(progress, ++refs_renamed_nr);
 	}
@@ -1444,7 +1444,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		/* make sure it's valid */
 		if (!refs_ref_exists(get_main_ref_store(the_repository), buf2.buf))
 			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head"))
+		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head", NULL))
 			result |= error(_("Could not setup %s"), buf.buf);
 		else if (opt_a)
 			printf("%s/HEAD set to %s\n", argv[0], head_name);
diff --git a/builtin/symbolic-ref.c b/builtin/symbolic-ref.c
index 299d23d76a..7728fbc3c1 100644
--- a/builtin/symbolic-ref.c
+++ b/builtin/symbolic-ref.c
@@ -88,7 +88,7 @@ int cmd_symbolic_ref(int argc,
 		if (check_refname_format(argv[1], REFNAME_ALLOW_ONELEVEL) < 0)
 			die("Refusing to set '%s' to invalid ref '%s'", argv[0], argv[1]);
 		ret = !!refs_update_symref(get_main_ref_store(the_repository),
-					   argv[0], argv[1], msg);
+					   argv[0], argv[1], msg, NULL);
 		break;
 	default:
 		usage_with_options(git_symbolic_ref_usage, options);
diff --git a/builtin/worktree.c b/builtin/worktree.c
index fc31d072a6..a7ab4193c1 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -517,7 +517,7 @@ static int add_worktree(const char *path, const char *refname,
 		ret = refs_update_ref(wt_refs, NULL, "HEAD", &commit->object.oid,
 				      NULL, 0, UPDATE_REFS_MSG_ON_ERR);
 	else
-		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, NULL);
+		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, NULL, NULL);
 	if (ret)
 		goto done;
 
diff --git a/refs.c b/refs.c
index 5f729ed412..b964ac44d0 100644
--- a/refs.c
+++ b/refs.c
@@ -2114,7 +2114,8 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
 }
 
 int refs_update_symref(struct ref_store *refs, const char *ref,
-		       const char *target, const char *logmsg)
+		       const char *target, const char *logmsg,
+		       struct strbuf *referent)
 {
 	struct ref_transaction *transaction;
 	struct strbuf err = STRBUF_INIT;
@@ -2122,17 +2123,23 @@ int refs_update_symref(struct ref_store *refs, const char *ref,
 
 	transaction = ref_store_transaction_begin(refs, &err);
 	if (!transaction ||
-	    ref_transaction_update(transaction, ref, NULL, NULL,
+		ref_transaction_update(transaction, ref, NULL, NULL,
 				   target, NULL, REF_NO_DEREF,
 				   logmsg, &err) ||
-	    ref_transaction_commit(transaction, &err)) {
+		ref_transaction_prepare(transaction, &err)) {
 		ret = error("%s", err.buf);
+		goto cleanup;
 	}
+	if (referent)
+		refs_read_symbolic_ref(refs, ref, referent);
+
+	if (ref_transaction_commit(transaction, &err))
+		ret = error("%s", err.buf);
 
+cleanup:
 	strbuf_release(&err);
 	if (transaction)
 		ref_transaction_free(transaction);
-
 	return ret;
 }
 
@@ -2948,4 +2955,3 @@ int ref_update_expects_existing_old_ref(struct ref_update *update)
 	return (update->flags & REF_HAVE_OLD) &&
 		(!is_null_oid(&update->old_oid) || update->old_target);
 }
-
diff --git a/refs.h b/refs.h
index 108dfc93b3..b09a3a4384 100644
--- a/refs.h
+++ b/refs.h
@@ -571,7 +571,8 @@ int refs_copy_existing_ref(struct ref_store *refs, const char *oldref,
 		    const char *newref, const char *logmsg);
 
 int refs_update_symref(struct ref_store *refs, const char *refname,
-		       const char *target, const char *logmsg);
+		       const char *target, const char *logmsg,
+		       struct strbuf *referent);
 
 enum action_on_err {
 	UPDATE_REFS_MSG_ON_ERR,
diff --git a/reset.c b/reset.c
index b22b1be792..cc36a9ed56 100644
--- a/reset.c
+++ b/reset.c
@@ -76,7 +76,7 @@ static int update_refs(const struct reset_head_opts *opts,
 		if (!ret)
 			ret = refs_update_symref(get_main_ref_store(the_repository),
 						 "HEAD", switch_to_branch,
-						 reflog_head);
+						 reflog_head, NULL);
 	}
 	if (!ret && run_hook)
 		run_hooks_l(the_repository, "post-checkout",
diff --git a/sequencer.c b/sequencer.c
index 8d01cd50ac..23b162924c 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -5107,7 +5107,7 @@ static int pick_commits(struct repository *r,
 			}
 			msg = reflog_message(opts, "finish", "returning to %s",
 				head_ref.buf);
-			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf, msg)) {
+			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf, msg, NULL)) {
 				res = error(_("could not update HEAD to %s"),
 					head_ref.buf);
 				goto cleanup_head_ref;
diff --git a/setup.c b/setup.c
index 94e79b2e48..d95f051465 100644
--- a/setup.c
+++ b/setup.c
@@ -2275,7 +2275,7 @@ void create_reference_database(enum ref_storage_format ref_storage_format,
 			die(_("invalid initial branch name: '%s'"),
 			    initial_branch);
 
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref, NULL, NULL) < 0)
 			exit(1);
 		free(ref);
 	}
diff --git a/t/helper/test-ref-store.c b/t/helper/test-ref-store.c
index 65346dee55..a911302bea 100644
--- a/t/helper/test-ref-store.c
+++ b/t/helper/test-ref-store.c
@@ -120,7 +120,7 @@ static int cmd_create_symref(struct ref_store *refs, const char **argv)
 	const char *target = notnull(*argv++, "target");
 	const char *logmsg = *argv++;
 
-	return refs_update_symref(refs, refname, target, logmsg);
+	return refs_update_symref(refs, refname, target, logmsg, NULL);
 }
 
 static struct flag_definition transaction_flags[] = {
-- 
2.47.0.7.g072c39eddb.dirty
Bence Ferdinandy· Oct 12, 2024, 23:03 UTC · re: Bence Ferdinandy · lore

[PATCH v7 2/6] remote set-head: add new variable for readability

Instead of calling get_main_ref_store(the_repository) multiple times, call it once and store in a new refs variable. Although this change probably offers some performance benefits, the main purpose is to shorten the line lengths of function calls using this variable for better readability.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v5: new patch (split from the next patch as a preparatory step)
    
    v6: no change
    
    v7: - change commit prefix to be more in line with project standards
 builtin/remote.c | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)
Show changes to builtin/remote.c +4 −3
diff --git a/builtin/remote.c b/builtin/remote.c
index d8ff440027..353ffd2c43 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1404,6 +1404,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 	int i, opt_a = 0, opt_d = 0, result = 0;
 	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;
 	char *head_name = NULL;
+	struct ref_store *refs = get_main_ref_store(the_repository);
 
 	struct option options[] = {
 		OPT_BOOL('a', "auto", &opt_a,
@@ -1434,7 +1435,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 			head_name = xstrdup(states.heads.items[0].string);
 		free_remote_ref_states(&states);
 	} else if (opt_d && !opt_a && argc == 1) {
-		if (refs_delete_ref(get_main_ref_store(the_repository), NULL, buf.buf, NULL, REF_NO_DEREF))
+		if (refs_delete_ref(refs, NULL, buf.buf, NULL, REF_NO_DEREF))
 			result |= error(_("Could not delete %s"), buf.buf);
 	} else
 		usage_with_options(builtin_remote_sethead_usage, options);
@@ -1442,9 +1443,9 @@ static int set_head(int argc, const char **argv, const char *prefix)
 	if (head_name) {
 		strbuf_addf(&buf2, "refs/remotes/%s/%s", argv[0], head_name);
 		/* make sure it's valid */
-		if (!refs_ref_exists(get_main_ref_store(the_repository), buf2.buf))
+		if (!refs_ref_exists(refs, buf2.buf))
 			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head", NULL))
+		else if (refs_update_symref(refs, buf.buf, buf2.buf, "remote set-head", NULL))
 			result |= error(_("Could not setup %s"), buf.buf);
 		else if (opt_a)
 			printf("%s/HEAD set to %s\n", argv[0], head_name);
-- 
2.47.0.7.g072c39eddb.dirty
Bence Ferdinandy· Oct 12, 2024, 23:03 UTC · re: Bence Ferdinandy · lore

[PATCH v7 4/6] refs: add TRANSACTION_CREATE_EXISTS error

Currently there is only one special error for transaction, for when there is a naming conflict, all other errors are dumped under a generic error. Add a new special error case for when the caller requests the reference to be updated only when it does not yet exist and the reference actually does exist.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    v5: no change
    v6: no change
    
    v7: - change commit prefix to be more in line with project standards
        - changed error checking to Karthik's suggestion
 refs.h                  |  4 +++-
 refs/files-backend.c    | 24 ++++++++++++++++--------
 refs/reftable-backend.c |  6 ++++--
 3 files changed, 23 insertions(+), 11 deletions(-)
Show changes to 3 files +23 −11

refs.h, refs/files-backend.c, refs/reftable-backend.c

diff --git a/refs.h b/refs.h
index b09a3a4384..c83b1ec76e 100644
--- a/refs.h
+++ b/refs.h
@@ -759,8 +759,10 @@ int ref_transaction_verify(struct ref_transaction *transaction,
 
 /* Naming conflict (for example, the ref names A and A/B conflict). */
 #define TRANSACTION_NAME_CONFLICT -1
+/* When only creation was requested, but the ref already exists. */
+#define TRANSACTION_CREATE_EXISTS -2
 /* All other errors. */
-#define TRANSACTION_GENERIC_ERROR -2
+#define TRANSACTION_GENERIC_ERROR -3
 
 /*
  * Perform the preparatory stages of committing `transaction`. Acquire
diff --git a/refs/files-backend.c b/refs/files-backend.c
index 0824c0b8a9..e743ec44b5 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -2502,14 +2502,18 @@ static int split_symref_update(struct ref_update *update,
 static int check_old_oid(struct ref_update *update, struct object_id *oid,
 			 struct strbuf *err)
 {
+	int ret = TRANSACTION_GENERIC_ERROR;
+
 	if (!(update->flags & REF_HAVE_OLD) ||
 		   oideq(oid, &update->old_oid))
 		return 0;
 
-	if (is_null_oid(&update->old_oid))
+	if (is_null_oid(&update->old_oid)) {
 		strbuf_addf(err, "cannot lock ref '%s': "
 			    "reference already exists",
 			    ref_update_original_update_refname(update));
+		ret = TRANSACTION_CREATE_EXISTS;
+	}
 	else if (is_null_oid(oid))
 		strbuf_addf(err, "cannot lock ref '%s': "
 			    "reference is missing but expected %s",
@@ -2522,7 +2526,7 @@ static int check_old_oid(struct ref_update *update, struct object_id *oid,
 			    oid_to_hex(oid),
 			    oid_to_hex(&update->old_oid));
 
-	return -1;
+	return ret;
 }
 
 /*
@@ -2602,9 +2606,11 @@ static int lock_ref_for_update(struct files_ref_store *refs,
 					ret = TRANSACTION_GENERIC_ERROR;
 					goto out;
 				}
-			} else if  (check_old_oid(update, &lock->old_oid, err)) {
-				ret = TRANSACTION_GENERIC_ERROR;
-				goto out;
+			} else {
+				ret = check_old_oid(update, &lock->old_oid, err);
+				if  (ret) {
+					goto out;
+				}
 			}
 		} else {
 			/*
@@ -2635,9 +2641,11 @@ static int lock_ref_for_update(struct files_ref_store *refs,
 				    update->old_target);
 			ret = TRANSACTION_GENERIC_ERROR;
 			goto out;
-		} else if  (check_old_oid(update, &lock->old_oid, err)) {
-			ret = TRANSACTION_GENERIC_ERROR;
-			goto out;
+		} else {
+			ret = check_old_oid(update, &lock->old_oid, err);
+			if  (ret) {
+				goto out;
+			}
 		}
 
 		/*
diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
index 3c96fbf66f..ebf8e57fbc 100644
--- a/refs/reftable-backend.c
+++ b/refs/reftable-backend.c
@@ -1206,10 +1206,13 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 				goto done;
 			}
 		} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {
-			if (is_null_oid(&u->old_oid))
+			ret = TRANSACTION_NAME_CONFLICT;
+			if (is_null_oid(&u->old_oid)) {
 				strbuf_addf(err, _("cannot lock ref '%s': "
 						   "reference already exists"),
 					    ref_update_original_update_refname(u));
+				ret = TRANSACTION_CREATE_EXISTS;
+			}
 			else if (is_null_oid(&current_oid))
 				strbuf_addf(err, _("cannot lock ref '%s': "
 						   "reference is missing but expected %s"),
@@ -1221,7 +1224,6 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 					    ref_update_original_update_refname(u),
 					    oid_to_hex(&current_oid),
 					    oid_to_hex(&u->old_oid));
-			ret = -1;
 			goto done;
 		}
 
-- 
2.47.0.7.g072c39eddb.dirty
Phillip Wood· Oct 13, 2024, 14:03 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v7 4/6] refs: add TRANSACTION_CREATE_EXISTS error

Hi Bence
On 13/10/2024 00:03, Bence Ferdinandy wrote:
Show 5 quoted lines
> Currently there is only one special error for transaction, for when
> there is a naming conflict, all other errors are dumped under a generic
> error. Add a new special error case for when the caller requests the
> reference to be updated only when it does not yet exist and the
> reference actually does exist.

This looks like useful improvement. Are the changes to reftable-backend.c correct - it looks like where it previously returned TRANSACTION_GENERIC_ERR it now returns TRANSACTION_NAME_CONFLICT which I think is used to indicate a file/directory conflict (e.g. trying to create refs/heads/topic/one when refs/heads/topic exists)

Best Wishes
Phillip
Show 122 quoted lines
> Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
> ---
> 
> Notes:
>      v4: new patch
>      v5: no change
>      v6: no change
>      
>      v7: - change commit prefix to be more in line with project standards
>          - changed error checking to Karthik's suggestion
> 
>   refs.h                  |  4 +++-
>   refs/files-backend.c    | 24 ++++++++++++++++--------
>   refs/reftable-backend.c |  6 ++++--
>   3 files changed, 23 insertions(+), 11 deletions(-)
> 
> diff --git a/refs.h b/refs.h
> index b09a3a4384..c83b1ec76e 100644
> --- a/refs.h
> +++ b/refs.h
> @@ -759,8 +759,10 @@ int ref_transaction_verify(struct ref_transaction *transaction,
>   
>   /* Naming conflict (for example, the ref names A and A/B conflict). */
>   #define TRANSACTION_NAME_CONFLICT -1
> +/* When only creation was requested, but the ref already exists. */
> +#define TRANSACTION_CREATE_EXISTS -2
>   /* All other errors. */
> -#define TRANSACTION_GENERIC_ERROR -2
> +#define TRANSACTION_GENERIC_ERROR -3
>   
>   /*
>    * Perform the preparatory stages of committing `transaction`. Acquire
> diff --git a/refs/files-backend.c b/refs/files-backend.c
> index 0824c0b8a9..e743ec44b5 100644
> --- a/refs/files-backend.c
> +++ b/refs/files-backend.c
> @@ -2502,14 +2502,18 @@ static int split_symref_update(struct ref_update *update,
>   static int check_old_oid(struct ref_update *update, struct object_id *oid,
>   			 struct strbuf *err)
>   {
> +	int ret = TRANSACTION_GENERIC_ERROR;
> +
>   	if (!(update->flags & REF_HAVE_OLD) ||
>   		   oideq(oid, &update->old_oid))
>   		return 0;
>   
> -	if (is_null_oid(&update->old_oid))
> +	if (is_null_oid(&update->old_oid)) {
>   		strbuf_addf(err, "cannot lock ref '%s': "
>   			    "reference already exists",
>   			    ref_update_original_update_refname(update));
> +		ret = TRANSACTION_CREATE_EXISTS;
> +	}
>   	else if (is_null_oid(oid))
>   		strbuf_addf(err, "cannot lock ref '%s': "
>   			    "reference is missing but expected %s",
> @@ -2522,7 +2526,7 @@ static int check_old_oid(struct ref_update *update, struct object_id *oid,
>   			    oid_to_hex(oid),
>   			    oid_to_hex(&update->old_oid));
>   
> -	return -1;
> +	return ret;
>   }
>   
>   /*
> @@ -2602,9 +2606,11 @@ static int lock_ref_for_update(struct files_ref_store *refs,
>   					ret = TRANSACTION_GENERIC_ERROR;
>   					goto out;
>   				}
> -			} else if  (check_old_oid(update, &lock->old_oid, err)) {
> -				ret = TRANSACTION_GENERIC_ERROR;
> -				goto out;
> +			} else {
> +				ret = check_old_oid(update, &lock->old_oid, err);
> +				if  (ret) {
> +					goto out;
> +				}
>   			}
>   		} else {
>   			/*
> @@ -2635,9 +2641,11 @@ static int lock_ref_for_update(struct files_ref_store *refs,
>   				    update->old_target);
>   			ret = TRANSACTION_GENERIC_ERROR;
>   			goto out;
> -		} else if  (check_old_oid(update, &lock->old_oid, err)) {
> -			ret = TRANSACTION_GENERIC_ERROR;
> -			goto out;
> +		} else {
> +			ret = check_old_oid(update, &lock->old_oid, err);
> +			if  (ret) {
> +				goto out;
> +			}
>   		}
>   
>   		/*
> diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
> index 3c96fbf66f..ebf8e57fbc 100644
> --- a/refs/reftable-backend.c
> +++ b/refs/reftable-backend.c
> @@ -1206,10 +1206,13 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
>   				goto done;
>   			}
>   		} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {
> -			if (is_null_oid(&u->old_oid))
> +			ret = TRANSACTION_NAME_CONFLICT;
> +			if (is_null_oid(&u->old_oid)) {
>   				strbuf_addf(err, _("cannot lock ref '%s': "
>   						   "reference already exists"),
>   					    ref_update_original_update_refname(u));
> +				ret = TRANSACTION_CREATE_EXISTS;
> +			}
>   			else if (is_null_oid(&current_oid))
>   				strbuf_addf(err, _("cannot lock ref '%s': "
>   						   "reference is missing but expected %s"),
> @@ -1221,7 +1224,6 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
>   					    ref_update_original_update_refname(u),
>   					    oid_to_hex(&current_oid),
>   					    oid_to_hex(&u->old_oid));
> -			ret = -1;
>   			goto done;
>   		}
>   
Bence Ferdinandy· Oct 13, 2024, 20:52 UTC · re: Phillip Wood · lore

Re: [PATCH v7 4/6] refs: add TRANSACTION_CREATE_EXISTS error

On Sun Oct 13, 2024 at 16:03, Phillip Wood <phillip.wood123@gmail.com> wrote:
Show 14 quoted lines
> Hi Bence
>
> On 13/10/2024 00:03, Bence Ferdinandy wrote:
>> Currently there is only one special error for transaction, for when
>> there is a naming conflict, all other errors are dumped under a generic
>> error. Add a new special error case for when the caller requests the
>> reference to be updated only when it does not yet exist and the
>> reference actually does exist.
>
> This looks like useful improvement. Are the changes to 
> reftable-backend.c correct - it looks like where it previously returned 
> TRANSACTION_GENERIC_ERR it now returns TRANSACTION_NAME_CONFLICT which I 
> think is used to indicate a file/directory conflict (e.g. trying to 
> create refs/heads/topic/one when refs/heads/topic exists)

This passes: GIT_TEST_DEFAULT_REF_FORMAT=reftable prove --timer --jobs 8 ./t[0-9]*.sh so I'm hoping it's correct :)

[snip]
I guess you are referring to this part below:
Show 27 quoted lines
>> diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
>> index 3c96fbf66f..ebf8e57fbc 100644
>> --- a/refs/reftable-backend.c
>> +++ b/refs/reftable-backend.c
>> @@ -1206,10 +1206,13 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
>>   				goto done;
>>   			}
>>   		} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {
>> -			if (is_null_oid(&u->old_oid))
>> +			ret = TRANSACTION_NAME_CONFLICT;
>> +			if (is_null_oid(&u->old_oid)) {
>>   				strbuf_addf(err, _("cannot lock ref '%s': "
>>   						   "reference already exists"),
>>   					    ref_update_original_update_refname(u));
>> +				ret = TRANSACTION_CREATE_EXISTS;
>> +			}
>>   			else if (is_null_oid(&current_oid))
>>   				strbuf_addf(err, _("cannot lock ref '%s': "
>>   						   "reference is missing but expected %s"),
>> @@ -1221,7 +1224,6 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
>>   					    ref_update_original_update_refname(u),
>>   					    oid_to_hex(&current_oid),
>>   					    oid_to_hex(&u->old_oid));
>> -			ret = -1;
>>   			goto done;
>>   		}
>>   

This originally returned -1, and it still returns that if it doesn't return -2, I just used the named variable instead of the integer itself. It might still be that this should be -3 GENERIC_ERROR, but if that is the case I think fixing that should be a different patch? I didn't check if changing that -1 to something else breaks anything or not.

Best, Bence

-- 
bence.ferdinandy.com
Phillip Wood· Oct 14, 2024, 08:48 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v7 4/6] refs: add TRANSACTION_CREATE_EXISTS error

Hi Bence
On 13/10/2024 21:52, Bence Ferdinandy wrote:
> On Sun Oct 13, 2024 at 16:03, Phillip Wood <phillip.wood123@gmail.com> wrote:
Show 33 quoted lines
>>> diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
>>> index 3c96fbf66f..ebf8e57fbc 100644
>>> --- a/refs/reftable-backend.c
>>> +++ b/refs/reftable-backend.c
>>> @@ -1206,10 +1206,13 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
>>>    				goto done;
>>>    			}
>>>    		} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {
>>> -			if (is_null_oid(&u->old_oid))
>>> +			ret = TRANSACTION_NAME_CONFLICT;
>>> +			if (is_null_oid(&u->old_oid)) {
>>>    				strbuf_addf(err, _("cannot lock ref '%s': "
>>>    						   "reference already exists"),
>>>    					    ref_update_original_update_refname(u));
>>> +				ret = TRANSACTION_CREATE_EXISTS;
>>> +			}
>>>    			else if (is_null_oid(&current_oid))
>>>    				strbuf_addf(err, _("cannot lock ref '%s': "
>>>    						   "reference is missing but expected %s"),
>>> @@ -1221,7 +1224,6 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
>>>    					    ref_update_original_update_refname(u),
>>>    					    oid_to_hex(&current_oid),
>>>    					    oid_to_hex(&u->old_oid));
>>> -			ret = -1;
>>>    			goto done;
>>>    		}
>>>    
> 
> This originally returned -1, and it still returns that if it doesn't return -2,
> I just used the named variable instead of the integer itself. It might still be
> that this should be -3 GENERIC_ERROR, but if that is the case I think fixing
> that should be a different patch? I didn't check if changing that -1 to
> something else breaks anything or not.
Oh sorry I was confused and thought TRANSACTION_GENERIC_ERROR was -1.
Best Wishes
Phillip
> Best,
> Bence
> 
Bence Ferdinandy· Oct 12, 2024, 23:03 UTC · re: Bence Ferdinandy · lore

[PATCH v7 3/6] remote set-head: better output for --auto

Currently, set-head --auto will print a message saying "remote/HEAD set to branch", which implies something was changed.

Change the output of --auto, so the output actually reflects what was done: a) set a previously unset HEAD, b) change HEAD because remote changed or c) no updates. As a fourth output, if HEAD is changed from a previous value that was not a remote branch, explicitly call attention to this fact.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v1-v2:
    
        was RFC in https://lore.kernel.org/git/20240910203835.2288291-1-bence@ferdinandy.com/
    
    v3:
    
        This patch was originally sent along when I thought set-head was
        going to be invoked by fetch, but the discussion on the RFC
        concluded that it should be not. This opened the possibility to make
        it more explicit.
    
        Note: although I feel both things the patch does are really just
        cosmetic, an argument could be made for breaking it into two, one
        for the no-op part and one for the --auto print update.
    
        Was sent in:
        https://lore.kernel.org/git/20240915221055.904107-1-bence@ferdinandy.com/
    
    v4:
        - changes are now handled atomically via the ref update transaction
        - outputs have changed along the lines of Junio's suggestion
        - minor refactor to set_head for improved legibility
    
    v5: - the minor refactor has been split out into its own patch
    
    v6: - fixed uninitialized prev_head
        - fixed case of unusual previous target
        - fixed a test that would have been actually broken at this patch
          (the output was only correct with the later update to fetch)
        - added 4 tests for the 4 output cases
    
    v7: - change commit prefix to be more in line with project standards
        - fixed tests to also work with the reftable backend
        - renamed report function, fixed style issue with checking buf len
        - fixed not releasing an strbuf
 builtin/remote.c  | 35 ++++++++++++++++++++++++++++----
 t/t5505-remote.sh | 51 ++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 81 insertions(+), 5 deletions(-)
Show changes to 2 files +81 −5

builtin/remote.c, t/t5505-remote.sh

diff --git a/builtin/remote.c b/builtin/remote.c
index 353ffd2c43..2b6948439f 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1399,10 +1399,35 @@ static int show(int argc, const char **argv, const char *prefix)
 	return result;
 }
 
+static void report_set_head_auto(const char *remote, const char *head_name,
+			struct strbuf *buf_prev) {
+	struct strbuf buf_prefix = STRBUF_INIT;
+	const char *prev_head = NULL;
+
+	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
+	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
+
+	if (prev_head && !strcmp(prev_head, head_name))
+		printf("'%s/HEAD' is unchanged and points to '%s'\n",
+			remote, head_name);
+	else if (prev_head)
+		printf("'%s/HEAD' has changed from '%s' and now points to '%s'\n",
+			remote, prev_head, head_name);
+	else if (!buf_prev->len)
+		printf("'%s/HEAD' is now created and points to '%s'\n",
+			remote, head_name);
+	else
+		printf("'%s/HEAD' used to point to '%s' "
+			"(which is not a remote branch), but now points to '%s'\n",
+			remote, buf_prev->buf, head_name);
+	strbuf_release(&buf_prefix);
+}
+
 static int set_head(int argc, const char **argv, const char *prefix)
 {
 	int i, opt_a = 0, opt_d = 0, result = 0;
-	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;
+	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT,
+		buf_prev = STRBUF_INIT;
 	char *head_name = NULL;
 	struct ref_store *refs = get_main_ref_store(the_repository);
 
@@ -1445,15 +1470,17 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		/* make sure it's valid */
 		if (!refs_ref_exists(refs, buf2.buf))
 			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(refs, buf.buf, buf2.buf, "remote set-head", NULL))
+		else if (refs_update_symref(refs, buf.buf, buf2.buf, "remote set-head", &buf_prev))
 			result |= error(_("Could not setup %s"), buf.buf);
-		else if (opt_a)
-			printf("%s/HEAD set to %s\n", argv[0], head_name);
+		else if (opt_a) {
+			report_set_head_auto(argv[0], head_name, &buf_prev);
+		}
 		free(head_name);
 	}
 
 	strbuf_release(&buf);
 	strbuf_release(&buf2);
+	strbuf_release(&buf_prev);
 	return result;
 }
 
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 532035933f..d99cd1d0aa 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -429,12 +429,51 @@ test_expect_success 'set-head --auto' '
 	)
 '
 
+test_expect_success 'set-head --auto detects creation' '
+	(
+		cd test &&
+		git symbolic-ref -d refs/remotes/origin/HEAD &&
+		git remote set-head --auto origin >output &&
+		echo "'\''origin/HEAD'\'' is now created and points to '\''main'\''" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects no change' '
+	(
+		cd test &&
+		git remote set-head --auto origin >output &&
+		echo "'\''origin/HEAD'\'' is unchanged and points to '\''main'\''" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects change' '
+	(
+		cd test &&
+		git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/ahead &&
+		git remote set-head --auto origin >output &&
+		echo "'\''origin/HEAD'\'' has changed from '\''ahead'\'' and now points to '\''main'\''" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects strange ref' '
+	(
+		cd test &&
+		git symbolic-ref refs/remotes/origin/HEAD refs/heads/main &&
+		git remote set-head --auto origin >output &&
+		echo "'\''origin/HEAD'\'' used to point to '\''refs/heads/main'\'' (which is not a remote branch), but now points to '\''main'\''" >expect &&
+		test_cmp expect output
+	)
+'
+
 test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
 	(
 		cd test &&
 		git fetch two "refs/heads/*:refs/remotes/two/*" &&
 		git remote set-head --auto two >output 2>&1 &&
-		echo "two/HEAD set to main" >expect &&
+		echo "'\''two/HEAD'\'' is now created and points to '\''main'\''" >expect &&
 		test_cmp expect output
 	)
 '
@@ -453,6 +492,16 @@ test_expect_success 'set-head explicit' '
 	)
 '
 
+test_expect_success 'set-head --auto reports change' '
+	(
+		cd test &&
+		git remote set-head origin side2 &&
+		git remote set-head --auto origin >output 2>&1 &&
+		echo "'\''origin/HEAD'\'' has changed from '\''side2'\'' and now points to '\''main'\''" >expect &&
+		test_cmp expect output
+	)
+'
+
 cat >test/expect <<EOF
 Pruning origin
 URL: $(pwd)/one
-- 
2.47.0.7.g072c39eddb.dirty
Bence Ferdinandy· Oct 12, 2024, 23:03 UTC · re: Bence Ferdinandy · lore

[PATCH v7 5/6] refs: add create_only option to refs_update_symref

Allow the caller to specify that it only wants to update the symref if it does not already exist. Silently ignore the error from the transaction API if the symref already exists.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    v5: no change
    
    v6: - switched from bool to int for create_only
        - refactored logic in refs_update_symref with goto as layed out by
          Junio
    
    v7: - change commit prefix to be more in line with project standards
        - refactored code to accommodate changes in the first patch, but
          otherwise no change
 builtin/branch.c          |  2 +-
 builtin/checkout.c        |  5 +++--
 builtin/clone.c           |  8 +++++---
 builtin/notes.c           |  3 ++-
 builtin/remote.c          |  9 ++++++---
 builtin/symbolic-ref.c    |  2 +-
 builtin/worktree.c        |  2 +-
 refs.c                    | 30 ++++++++++++++++++++++--------
 refs.h                    |  2 +-
 reset.c                   |  2 +-
 sequencer.c               |  3 ++-
 setup.c                   |  3 ++-
 t/helper/test-ref-store.c |  2 +-
 13 files changed, 48 insertions(+), 25 deletions(-)
Show changes to 13 files +48 −25

builtin/branch.c, builtin/checkout.c, builtin/clone.c, builtin/notes.c, builtin/remote.c, builtin/symbolic-ref.c, builtin/worktree.c, refs.c, refs.h, reset.c, sequencer.c, setup.c, t/helper/test-ref-store.c

diff --git a/builtin/branch.c b/builtin/branch.c
index 6c87690b58..2fcbcbfd59 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -559,7 +559,7 @@ static int replace_each_worktree_head_symref(struct worktree **worktrees,
 			continue;
 
 		refs = get_worktree_ref_store(worktrees[i]);
-		if (refs_update_symref(refs, "HEAD", newref, logmsg, NULL))
+		if (refs_update_symref(refs, "HEAD", newref, logmsg, NULL, 0))
 			ret = error(_("HEAD of working tree %s is not updated"),
 				    worktrees[i]->path);
 	}
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 356ee9bcde..9abb71dc07 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -1015,7 +1015,8 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 			describe_detached_head(_("HEAD is now at"), new_branch_info->commit);
 		}
 	} else if (new_branch_info->path) {	/* Switch branches. */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path, msg.buf, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path,
+					msg.buf, NULL, 0) < 0)
 			die(_("unable to update HEAD"));
 		if (!opts->quiet) {
 			if (old_branch_info->path && !strcmp(new_branch_info->path, old_branch_info->path)) {
@@ -1479,7 +1480,7 @@ static int switch_unborn_to_new_branch(const struct checkout_opts *opts)
 		die(_("You are on a branch yet to be born"));
 	strbuf_addf(&branch_ref, "refs/heads/%s", opts->new_branch);
 	status = refs_update_symref(get_main_ref_store(the_repository),
-				    "HEAD", branch_ref.buf, "checkout -b", NULL);
+				    "HEAD", branch_ref.buf, "checkout -b", NULL, 0);
 	strbuf_release(&branch_ref);
 	if (!opts->quiet)
 		fprintf(stderr, _("Switched to a new branch '%s'\n"),
diff --git a/builtin/clone.c b/builtin/clone.c
index ead2af20ea..87e8f1421a 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -661,7 +661,7 @@ static void update_remote_refs(const struct ref *refs,
 		strbuf_addstr(&head_ref, "HEAD");
 		if (refs_update_symref(get_main_ref_store(the_repository), head_ref.buf,
 				       remote_head_points_at->peer_ref->name,
-				       msg, NULL) < 0)
+				       msg, NULL, 0) < 0)
 			die(_("unable to update %s"), head_ref.buf);
 		strbuf_release(&head_ref);
 	}
@@ -673,7 +673,8 @@ static void update_head(const struct ref *our, const struct ref *remote,
 	const char *head;
 	if (our && skip_prefix(our->name, "refs/heads/", &head)) {
 		/* Local default branch link */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name, NULL, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name,
+					NULL, NULL, 0) < 0)
 			die(_("unable to update HEAD"));
 		if (!option_bare) {
 			refs_update_ref(get_main_ref_store(the_repository),
@@ -702,7 +703,8 @@ static void update_head(const struct ref *our, const struct ref *remote,
 		 * Unborn head from remote; same as "our" case above except
 		 * that we have no ref to update.
 		 */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn, NULL, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn,
+					NULL, NULL, 0) < 0)
 			die(_("unable to update HEAD"));
 		if (!option_bare)
 			install_branch_config(0, head, remote_name, unborn);
diff --git a/builtin/notes.c b/builtin/notes.c
index ba646f06ff..7b3138e3c3 100644
--- a/builtin/notes.c
+++ b/builtin/notes.c
@@ -980,7 +980,8 @@ static int merge(int argc, const char **argv, const char *prefix)
 			die(_("a notes merge into %s is already in-progress at %s"),
 			    notes_ref, wt->path);
 		free_worktrees(worktrees);
-		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", notes_ref, NULL, NULL))
+		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", notes_ref,
+					NULL, NULL, 0))
 			die(_("failed to store link to current notes ref (%s)"),
 			    notes_ref);
 		fprintf(stderr, _("Automatic notes merge failed. Fix conflicts in %s "
diff --git a/builtin/remote.c b/builtin/remote.c
index 2b6948439f..d60ec6e907 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -244,7 +244,8 @@ static int add(int argc, const char **argv, const char *prefix)
 		strbuf_reset(&buf2);
 		strbuf_addf(&buf2, "refs/remotes/%s/%s", name, master);
 
-		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote add", NULL))
+		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf,
+					"remote add", NULL, 0))
 			result = error(_("Could not setup master '%s'"), master);
 	}
 
@@ -864,7 +865,8 @@ static int mv(int argc, const char **argv, const char *prefix)
 		strbuf_reset(&buf3);
 		strbuf_addf(&buf3, "remote: renamed %s to %s",
 				item->string, buf.buf);
-		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, buf3.buf, NULL))
+		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf,
+					buf3.buf, NULL, 0))
 			die(_("creating '%s' failed"), buf.buf);
 		display_progress(progress, ++refs_renamed_nr);
 	}
@@ -1470,7 +1472,8 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		/* make sure it's valid */
 		if (!refs_ref_exists(refs, buf2.buf))
 			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(refs, buf.buf, buf2.buf, "remote set-head", &buf_prev))
+		else if (refs_update_symref(refs, buf.buf, buf2.buf,
+					"remote set-head", &buf_prev, 0))
 			result |= error(_("Could not setup %s"), buf.buf);
 		else if (opt_a) {
 			report_set_head_auto(argv[0], head_name, &buf_prev);
diff --git a/builtin/symbolic-ref.c b/builtin/symbolic-ref.c
index 7728fbc3c1..cfbb53b30c 100644
--- a/builtin/symbolic-ref.c
+++ b/builtin/symbolic-ref.c
@@ -88,7 +88,7 @@ int cmd_symbolic_ref(int argc,
 		if (check_refname_format(argv[1], REFNAME_ALLOW_ONELEVEL) < 0)
 			die("Refusing to set '%s' to invalid ref '%s'", argv[0], argv[1]);
 		ret = !!refs_update_symref(get_main_ref_store(the_repository),
-					   argv[0], argv[1], msg, NULL);
+					   argv[0], argv[1], msg, NULL, 0);
 		break;
 	default:
 		usage_with_options(git_symbolic_ref_usage, options);
diff --git a/builtin/worktree.c b/builtin/worktree.c
index a7ab4193c1..2e95367235 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -517,7 +517,7 @@ static int add_worktree(const char *path, const char *refname,
 		ret = refs_update_ref(wt_refs, NULL, "HEAD", &commit->object.oid,
 				      NULL, 0, UPDATE_REFS_MSG_ON_ERR);
 	else
-		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, NULL, NULL);
+		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, NULL, NULL, 0);
 	if (ret)
 		goto done;
 
diff --git a/refs.c b/refs.c
index b964ac44d0..29c020e78e 100644
--- a/refs.c
+++ b/refs.c
@@ -2115,26 +2115,40 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
 
 int refs_update_symref(struct ref_store *refs, const char *ref,
 		       const char *target, const char *logmsg,
-		       struct strbuf *referent)
+		       struct strbuf *referent, int create_only)
 {
 	struct ref_transaction *transaction;
 	struct strbuf err = STRBUF_INIT;
-	int ret = 0;
+	int ret = 0, prepret = 0;
 
 	transaction = ref_store_transaction_begin(refs, &err);
-	if (!transaction ||
-		ref_transaction_update(transaction, ref, NULL, NULL,
-				   target, NULL, REF_NO_DEREF,
-				   logmsg, &err) ||
-		ref_transaction_prepare(transaction, &err)) {
+	if (!transaction) {
+	error_return:
 		ret = error("%s", err.buf);
 		goto cleanup;
 	}
+	if (create_only) {
+		if (ref_transaction_create(transaction, ref, NULL, target,
+					   REF_NO_DEREF, logmsg, &err))
+			goto error_return;
+		prepret = ref_transaction_prepare(transaction, &err);
+		if (prepret && prepret != TRANSACTION_CREATE_EXISTS)
+			goto error_return;
+	} else {
+		if (ref_transaction_update(transaction, ref, NULL, NULL,
+					   target, NULL, REF_NO_DEREF,
+					   logmsg, &err) ||
+			ref_transaction_prepare(transaction, &err))
+			goto error_return;
+	}
+
 	if (referent)
 		refs_read_symbolic_ref(refs, ref, referent);
 
+	if (prepret == TRANSACTION_CREATE_EXISTS)
+		goto cleanup;
 	if (ref_transaction_commit(transaction, &err))
-		ret = error("%s", err.buf);
+		goto error_return;
 
 cleanup:
 	strbuf_release(&err);
diff --git a/refs.h b/refs.h
index c83b1ec76e..41ae384f11 100644
--- a/refs.h
+++ b/refs.h
@@ -572,7 +572,7 @@ int refs_copy_existing_ref(struct ref_store *refs, const char *oldref,
 
 int refs_update_symref(struct ref_store *refs, const char *refname,
 		       const char *target, const char *logmsg,
-		       struct strbuf *referent);
+		       struct strbuf *referent, int create_only);
 
 enum action_on_err {
 	UPDATE_REFS_MSG_ON_ERR,
diff --git a/reset.c b/reset.c
index cc36a9ed56..919fb84f5f 100644
--- a/reset.c
+++ b/reset.c
@@ -76,7 +76,7 @@ static int update_refs(const struct reset_head_opts *opts,
 		if (!ret)
 			ret = refs_update_symref(get_main_ref_store(the_repository),
 						 "HEAD", switch_to_branch,
-						 reflog_head, NULL);
+						 reflog_head, NULL, 0);
 	}
 	if (!ret && run_hook)
 		run_hooks_l(the_repository, "post-checkout",
diff --git a/sequencer.c b/sequencer.c
index 23b162924c..103d0ddfb2 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -5107,7 +5107,8 @@ static int pick_commits(struct repository *r,
 			}
 			msg = reflog_message(opts, "finish", "returning to %s",
 				head_ref.buf);
-			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf, msg, NULL)) {
+			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf,
+						msg, NULL, 0)) {
 				res = error(_("could not update HEAD to %s"),
 					head_ref.buf);
 				goto cleanup_head_ref;
diff --git a/setup.c b/setup.c
index d95f051465..9bbc655ee4 100644
--- a/setup.c
+++ b/setup.c
@@ -2275,7 +2275,8 @@ void create_reference_database(enum ref_storage_format ref_storage_format,
 			die(_("invalid initial branch name: '%s'"),
 			    initial_branch);
 
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref, NULL, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref,
+					NULL, NULL, 0) < 0)
 			exit(1);
 		free(ref);
 	}
diff --git a/t/helper/test-ref-store.c b/t/helper/test-ref-store.c
index a911302bea..f72b029e51 100644
--- a/t/helper/test-ref-store.c
+++ b/t/helper/test-ref-store.c
@@ -120,7 +120,7 @@ static int cmd_create_symref(struct ref_store *refs, const char **argv)
 	const char *target = notnull(*argv++, "target");
 	const char *logmsg = *argv++;
 
-	return refs_update_symref(refs, refname, target, logmsg, NULL);
+	return refs_update_symref(refs, refname, target, logmsg, NULL, 0);
 }
 
 static struct flag_definition transaction_flags[] = {
-- 
2.47.0.7.g072c39eddb.dirty
Bence Ferdinandy· Oct 12, 2024, 23:03 UTC · re: Bence Ferdinandy · lore

[PATCH v7 6/6] fetch: set remote/HEAD if it does not exist

If the user has remote/HEAD set already and it looks like it has changed on the server, then print a message, otherwise set it if we can. Silently pass if the user already has the same remote/HEAD set as reported by the server or if we encounter any errors along the way.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v3: - does not rely on remote set-head anymore so it only authenticates
        once
        - uses the new REF_CREATE_ONLY to atomically check if the ref exists
          and only write it if it doesn't
        - in all other cases the maximum it does is print a warning
    
    v4: - instead of the discarded REF_CREATE_ONLY, it uses the existing,
          but updated transaction api to request a silent create only
        - it now uses the atomic before_target to determine reporting
        - refactored for legibility
    
    v5: - instead of printing a not too useful message, it now fails
          silently, this in line with the objective to only set up
          remote/HEAD automatically if the right thing is trivial, for
          everything else there is remote set-head
        - fixed all failing tests
        - added two new tests, one for checking if remote/HEAD is set to the
          correct one, and one to test that we do not override remote/HEAD
          if it has changed on the server from what we have locally
    
    v6: - fixed style issues and unintended extra empty line
        - updated function call with bool to int from previous patch's
          change
        - removed calls to error(...) inherited from builtin/remote.c so we
          actually fail silently
        - set the test for remote set-head --auto to the correct value here,
          which was previously erronously set in the remote set-head patch
    
    v7: - no change
 builtin/fetch.c                  |  82 +++++++++++
 t/t4207-log-decoration-colors.sh |   3 +-
 t/t5505-remote.sh                |  13 +-
 t/t5510-fetch.sh                 | 229 ++++++++++++++++---------------
 t/t5512-ls-remote.sh             |   2 +
 t/t5514-fetch-multiple.sh        |  17 ++-
 t/t5516-fetch-push.sh            |   3 +-
 t/t5527-fetch-odd-refs.sh        |   3 +-
 t/t7900-maintenance.sh           |   3 +-
 t/t9210-scalar.sh                |   5 +-
 t/t9211-scalar-clone.sh          |   6 +-
 t/t9902-completion.sh            |  65 +++++++++
 12 files changed, 308 insertions(+), 123 deletions(-)
Show changes to 12 files +308 −123

builtin/fetch.c, t/t4207-log-decoration-colors.sh, t/t5505-remote.sh, t/t5510-fetch.sh, t/t5512-ls-remote.sh, t/t5514-fetch-multiple.sh, t/t5516-fetch-push.sh, t/t5527-fetch-odd-refs.sh, t/t7900-maintenance.sh, t/t9210-scalar.sh, t/t9211-scalar-clone.sh, t/t9902-completion.sh

diff --git a/builtin/fetch.c b/builtin/fetch.c
index 80a64d0d26..4c68f21be1 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1578,6 +1578,80 @@ static int backfill_tags(struct display_state *display_state,
 	return retcode;
 }
 
+static void report_set_head(const char *remote, const char *head_name,
+			struct strbuf *buf_prev) {
+	struct strbuf buf_prefix = STRBUF_INIT;
+	const char *prev_head = NULL;
+
+	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
+	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
+
+	if (prev_head && strcmp(prev_head, head_name)) {
+		printf("'HEAD' at '%s' has changed from '%s' to '%s'\n",
+			remote, prev_head, head_name);
+		printf("Run 'git remote set-head %s %s' to follow the change.\n",
+			remote, head_name);
+	}
+}
+
+static const char *strip_refshead(const char *name){
+	skip_prefix(name, "refs/heads/", &name);
+	return name;
+}
+
+static int set_head(const struct ref *remote_refs)
+{
+	int result = 0;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
+		b_local_head = STRBUF_INIT;
+	const char *remote = gtransport->remote->name;
+	char *head_name = NULL;
+	struct ref *ref, *matches;
+	struct ref *fetch_map = NULL, **fetch_map_tail = &fetch_map;
+	struct refspec_item refspec = {
+		.force = 0,
+		.pattern = 1,
+		.src = (char *) "refs/heads/*",
+		.dst = (char *) "refs/heads/*",
+	};
+	struct string_list heads = STRING_LIST_INIT_DUP;
+	struct ref_store *refs = get_main_ref_store(the_repository);
+
+	get_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);
+	matches = guess_remote_head(find_ref_by_name(remote_refs, "HEAD"),
+				    fetch_map, 1);
+	for (ref = matches; ref; ref = ref->next) {
+		string_list_append(&heads, strip_refshead(ref->name));
+	}
+
+
+	if (!heads.nr)
+		result = 1;
+	else if (heads.nr > 1)
+		result = 1;
+	else
+		head_name = xstrdup(heads.items[0].string);
+	if (head_name) {
+		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
+		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote, head_name);
+		/* make sure it's valid */
+		if (!refs_ref_exists(refs, b_remote_head.buf))
+			result = 1;
+		else if (refs_update_symref(refs, b_head.buf, b_remote_head.buf,
+					"remote set-head", &b_local_head, 1))
+			result = 1;
+		else
+			report_set_head(remote, head_name, &b_local_head);
+
+		free(head_name);
+	}
+
+	strbuf_release(&b_head);
+	strbuf_release(&b_local_head);
+	strbuf_release(&b_remote_head);
+	return result;
+}
+
 static int do_fetch(struct transport *transport,
 		    struct refspec *rs,
 		    const struct fetch_config *config)
@@ -1647,6 +1721,8 @@ static int do_fetch(struct transport *transport,
 				    "refs/tags/");
 	}
 
+	strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
+
 	if (must_list_refs) {
 		trace2_region_enter("fetch", "remote_refs", the_repository);
 		remote_refs = transport_get_remote_refs(transport,
@@ -1791,6 +1867,12 @@ static int do_fetch(struct transport *transport,
 				  "you need to specify exactly one branch with the --set-upstream option"));
 		}
 	}
+	if (set_head(remote_refs))
+		;
+		/*
+		 * Way too many cases where this can go wrong
+		 * so let's just fail silently for now.
+		 */
 
 cleanup:
 	if (retcode) {
diff --git a/t/t4207-log-decoration-colors.sh b/t/t4207-log-decoration-colors.sh
index 73ea9e5155..d55d22cb2f 100755
--- a/t/t4207-log-decoration-colors.sh
+++ b/t/t4207-log-decoration-colors.sh
@@ -59,7 +59,8 @@ ${c_reset}${c_tag}tag: ${c_reset}${c_tag}v1.0${c_reset}${c_commit}, \
 ${c_reset}${c_tag}tag: ${c_reset}${c_tag}B${c_reset}${c_commit})${c_reset} B
 ${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
 ${c_tag}tag: ${c_reset}${c_tag}A1${c_reset}${c_commit}, \
-${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit})${c_reset} A1
+${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit}, \
+${c_reset}${c_remoteBranch}other/HEAD${c_reset}${c_commit})${c_reset} A1
 	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
 ${c_stash}refs/stash${c_reset}${c_commit})${c_reset} On main: Changes to A.t
 	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index d99cd1d0aa..cb45f22232 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -71,7 +71,7 @@ test_expect_success 'add another remote' '
 		cd test &&
 		git remote add -f second ../two &&
 		tokens_match "origin second" "$(git remote)" &&
-		check_tracking_branch second main side another &&
+		check_tracking_branch second main side another HEAD &&
 		git for-each-ref "--format=%(refname)" refs/remotes |
 		sed -e "/^refs\/remotes\/origin\//d" \
 		    -e "/^refs\/remotes\/second\//d" >actual &&
@@ -473,7 +473,7 @@ test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
 		cd test &&
 		git fetch two "refs/heads/*:refs/remotes/two/*" &&
 		git remote set-head --auto two >output 2>&1 &&
-		echo "'\''two/HEAD'\'' is now created and points to '\''main'\''" >expect &&
+		echo "'\''two/HEAD'\'' is unchanged and points to '\''main'\''" >expect &&
 		test_cmp expect output
 	)
 '
@@ -763,6 +763,7 @@ test_expect_success 'reject --no-no-tags' '
 cat >one/expect <<\EOF
   apis/main
   apis/side
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
@@ -780,6 +781,7 @@ test_expect_success 'update' '
 '
 
 cat >one/expect <<\EOF
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
@@ -792,7 +794,7 @@ EOF
 test_expect_success 'update with arguments' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -835,7 +837,7 @@ EOF
 test_expect_success 'update default' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -847,6 +849,7 @@ test_expect_success 'update default' '
 '
 
 cat >one/expect <<\EOF
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
@@ -869,7 +872,7 @@ test_expect_success 'update default (overridden, with funny whitespace)' '
 test_expect_success 'update (with remotes.default defined)' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index 0890b9f61c..dfc8d748ba 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -75,6 +75,30 @@ test_expect_success "fetch test for-merge" '
 	cut -f -2 .git/FETCH_HEAD >actual &&
 	test_cmp expected actual'
 
+test_expect_success "fetch test remote HEAD" '
+	cd "$D" &&
+	cd two &&
+	git fetch &&
+	git rev-parse --verify refs/remotes/origin/HEAD &&
+	git rev-parse --verify refs/remotes/origin/main &&
+	head=$(git rev-parse refs/remotes/origin/HEAD) &&
+	branch=$(git rev-parse refs/remotes/origin/main) &&
+	test "z$head" = "z$branch"'
+
+test_expect_success "fetch test remote HEAD change" '
+	cd "$D" &&
+	cd two &&
+	git switch -c other &&
+	git push -u origin other &&
+	git rev-parse --verify refs/remotes/origin/HEAD &&
+	git rev-parse --verify refs/remotes/origin/main &&
+	git rev-parse --verify refs/remotes/origin/other &&
+	git remote set-head origin other &&
+	git fetch &&
+	head=$(git rev-parse refs/remotes/origin/HEAD) &&
+	branch=$(git rev-parse refs/remotes/origin/other) &&
+	test "z$head" = "z$branch"'
+
 test_expect_success 'fetch --prune on its own works as expected' '
 	cd "$D" &&
 	git clone . prune &&
@@ -478,7 +502,6 @@ test_expect_success 'unbundle 1' '
 	test_must_fail git fetch "$D/bundle1" main:main
 '
 
-
 test_expect_success 'bundle 1 has only 3 files ' '
 	cd "$D" &&
 	test_bundle_object_count bundle1 3
@@ -819,7 +842,7 @@ test_expect_success 'fetch from multiple configured URLs in single remote' '
 
 # configured prune tests
 
-set_config_tristate () {
+set_config_tristate() {
 	# var=$1 val=$2
 	case "$2" in
 	unset)
@@ -833,12 +856,12 @@ set_config_tristate () {
 	esac
 }
 
-test_configured_prune () {
+test_configured_prune() {
 	test_configured_prune_type "$@" "name"
 	test_configured_prune_type "$@" "link"
 }
 
-test_configured_prune_type () {
+test_configured_prune_type() {
 	fetch_prune=$1
 	remote_origin_prune=$2
 	fetch_prune_tags=$3
@@ -848,8 +871,7 @@ test_configured_prune_type () {
 	cmdline=$7
 	mode=$8
 
-	if test -z "$cmdline_setup"
-	then
+	if test -z "$cmdline_setup"; then
 		test_expect_success 'setup cmdline_setup variable for subsequent test' '
 			remote_url="file://$(git -C one config remote.origin.url)" &&
 			remote_fetch="$(git -C one config remote.origin.fetch)" &&
@@ -857,12 +879,10 @@ test_configured_prune_type () {
 		'
 	fi
 
-	if test "$mode" = 'link'
-	then
+	if test "$mode" = 'link'; then
 		new_cmdline=""
 
-		if test "$cmdline" = ""
-		then
+		if test "$cmdline" = ""; then
 			new_cmdline=$cmdline_setup
 		else
 			new_cmdline=$(perl -e '
@@ -873,10 +893,8 @@ test_configured_prune_type () {
 		fi
 
 		if test "$fetch_prune_tags" = 'true' ||
-		   test "$remote_origin_prune_tags" = 'true'
-		then
-			if ! printf '%s' "$cmdline\n" | grep -q refs/remotes/origin/
-			then
+			test "$remote_origin_prune_tags" = 'true'; then
+			if ! printf '%s' "$cmdline\n" | grep -q refs/remotes/origin/; then
 				new_cmdline="$new_cmdline refs/tags/*:refs/tags/*"
 			fi
 		fi
@@ -946,100 +964,100 @@ test_configured_prune_type () {
 # $7 git-fetch $cmdline:
 #
 #                     $1    $2    $3    $4    $5     $6     $7
-test_configured_prune unset unset unset unset kept   kept   ""
-test_configured_prune unset unset unset unset kept   kept   "--no-prune"
-test_configured_prune unset unset unset unset pruned kept   "--prune"
-test_configured_prune unset unset unset unset kept   pruned \
+test_configured_prune unset unset unset unset kept kept ""
+test_configured_prune unset unset unset unset kept kept "--no-prune"
+test_configured_prune unset unset unset unset pruned kept "--prune"
+test_configured_prune unset unset unset unset kept pruned \
 	"--prune origin refs/tags/*:refs/tags/*"
 test_configured_prune unset unset unset unset pruned pruned \
 	"--prune origin refs/tags/*:refs/tags/* +refs/heads/*:refs/remotes/origin/*"
 
-test_configured_prune false unset unset unset kept   kept   ""
-test_configured_prune false unset unset unset kept   kept   "--no-prune"
-test_configured_prune false unset unset unset pruned kept   "--prune"
+test_configured_prune false unset unset unset kept kept ""
+test_configured_prune false unset unset unset kept kept "--no-prune"
+test_configured_prune false unset unset unset pruned kept "--prune"
 
-test_configured_prune true  unset unset unset pruned kept   ""
-test_configured_prune true  unset unset unset pruned kept   "--prune"
-test_configured_prune true  unset unset unset kept   kept   "--no-prune"
+test_configured_prune true unset unset unset pruned kept ""
+test_configured_prune true unset unset unset pruned kept "--prune"
+test_configured_prune true unset unset unset kept kept "--no-prune"
 
-test_configured_prune unset false unset unset kept   kept   ""
-test_configured_prune unset false unset unset kept   kept   "--no-prune"
-test_configured_prune unset false unset unset pruned kept   "--prune"
+test_configured_prune unset false unset unset kept kept ""
+test_configured_prune unset false unset unset kept kept "--no-prune"
+test_configured_prune unset false unset unset pruned kept "--prune"
 
-test_configured_prune false false unset unset kept   kept   ""
-test_configured_prune false false unset unset kept   kept   "--no-prune"
-test_configured_prune false false unset unset pruned kept   "--prune"
-test_configured_prune false false unset unset kept   pruned \
+test_configured_prune false false unset unset kept kept ""
+test_configured_prune false false unset unset kept kept "--no-prune"
+test_configured_prune false false unset unset pruned kept "--prune"
+test_configured_prune false false unset unset kept pruned \
 	"--prune origin refs/tags/*:refs/tags/*"
 test_configured_prune false false unset unset pruned pruned \
 	"--prune origin refs/tags/*:refs/tags/* +refs/heads/*:refs/remotes/origin/*"
 
-test_configured_prune true  false unset unset kept   kept   ""
-test_configured_prune true  false unset unset pruned kept   "--prune"
-test_configured_prune true  false unset unset kept   kept   "--no-prune"
+test_configured_prune true false unset unset kept kept ""
+test_configured_prune true false unset unset pruned kept "--prune"
+test_configured_prune true false unset unset kept kept "--no-prune"
 
-test_configured_prune unset true  unset unset pruned kept   ""
-test_configured_prune unset true  unset unset kept   kept   "--no-prune"
-test_configured_prune unset true  unset unset pruned kept   "--prune"
+test_configured_prune unset true unset unset pruned kept ""
+test_configured_prune unset true unset unset kept kept "--no-prune"
+test_configured_prune unset true unset unset pruned kept "--prune"
 
-test_configured_prune false true  unset unset pruned kept   ""
-test_configured_prune false true  unset unset kept   kept   "--no-prune"
-test_configured_prune false true  unset unset pruned kept   "--prune"
+test_configured_prune false true unset unset pruned kept ""
+test_configured_prune false true unset unset kept kept "--no-prune"
+test_configured_prune false true unset unset pruned kept "--prune"
 
-test_configured_prune true  true  unset unset pruned kept   ""
-test_configured_prune true  true  unset unset pruned kept   "--prune"
-test_configured_prune true  true  unset unset kept   kept   "--no-prune"
-test_configured_prune true  true  unset unset kept   pruned \
+test_configured_prune true true unset unset pruned kept ""
+test_configured_prune true true unset unset pruned kept "--prune"
+test_configured_prune true true unset unset kept kept "--no-prune"
+test_configured_prune true true unset unset kept pruned \
 	"--prune origin refs/tags/*:refs/tags/*"
-test_configured_prune true  true  unset unset pruned pruned \
+test_configured_prune true true unset unset pruned pruned \
 	"--prune origin refs/tags/*:refs/tags/* +refs/heads/*:refs/remotes/origin/*"
 
 # --prune-tags on its own does nothing, needs --prune as well, same
 # for fetch.pruneTags without fetch.prune
-test_configured_prune unset unset unset unset kept kept     "--prune-tags"
-test_configured_prune unset unset true unset  kept kept     ""
-test_configured_prune unset unset unset true  kept kept     ""
+test_configured_prune unset unset unset unset kept kept "--prune-tags"
+test_configured_prune unset unset true unset kept kept ""
+test_configured_prune unset unset unset true kept kept ""
 
 # These will prune the tags
 test_configured_prune unset unset unset unset pruned pruned "--prune --prune-tags"
-test_configured_prune true  unset true  unset pruned pruned ""
-test_configured_prune unset true  unset true  pruned pruned ""
+test_configured_prune true unset true unset pruned pruned ""
+test_configured_prune unset true unset true pruned pruned ""
 
 # remote.<name>.pruneTags overrides fetch.pruneTags, just like
 # remote.<name>.prune overrides fetch.prune if set.
-test_configured_prune true  unset true unset pruned pruned  ""
-test_configured_prune false true  false true  pruned pruned ""
-test_configured_prune true  false true  false kept   kept   ""
+test_configured_prune true unset true unset pruned pruned ""
+test_configured_prune false true false true pruned pruned ""
+test_configured_prune true false true false kept kept ""
 
 # When --prune-tags is supplied it's ignored if an explicit refspec is
 # given, same for the configuration options.
 test_configured_prune unset unset unset unset pruned kept \
 	"--prune --prune-tags origin +refs/heads/*:refs/remotes/origin/*"
-test_configured_prune unset unset true  unset pruned kept \
+test_configured_prune unset unset true unset pruned kept \
 	"--prune origin +refs/heads/*:refs/remotes/origin/*"
-test_configured_prune unset unset unset true pruned  kept \
+test_configured_prune unset unset unset true pruned kept \
 	"--prune origin +refs/heads/*:refs/remotes/origin/*"
 
 # Pruning that also takes place if a file:// url replaces a named
 # remote. However, because there's no implicit
 # +refs/heads/*:refs/remotes/origin/* refspec and supplying it on the
 # command-line negates --prune-tags, the branches will not be pruned.
-test_configured_prune_type unset unset unset unset kept   kept   "origin --prune-tags" "name"
-test_configured_prune_type unset unset unset unset kept   kept   "origin --prune-tags" "link"
+test_configured_prune_type unset unset unset unset kept kept "origin --prune-tags" "name"
+test_configured_prune_type unset unset unset unset kept kept "origin --prune-tags" "link"
 test_configured_prune_type unset unset unset unset pruned pruned "origin --prune --prune-tags" "name"
-test_configured_prune_type unset unset unset unset kept   pruned "origin --prune --prune-tags" "link"
+test_configured_prune_type unset unset unset unset kept pruned "origin --prune --prune-tags" "link"
 test_configured_prune_type unset unset unset unset pruned pruned "--prune --prune-tags origin" "name"
-test_configured_prune_type unset unset unset unset kept   pruned "--prune --prune-tags origin" "link"
-test_configured_prune_type unset unset true  unset pruned pruned "--prune origin" "name"
-test_configured_prune_type unset unset true  unset kept   pruned "--prune origin" "link"
-test_configured_prune_type unset unset unset true  pruned pruned "--prune origin" "name"
-test_configured_prune_type unset unset unset true  kept   pruned "--prune origin" "link"
-test_configured_prune_type true  unset true  unset pruned pruned "origin" "name"
-test_configured_prune_type true  unset true  unset kept   pruned "origin" "link"
-test_configured_prune_type unset  true true  unset pruned pruned "origin" "name"
-test_configured_prune_type unset  true true  unset kept   pruned "origin" "link"
-test_configured_prune_type unset  true unset true  pruned pruned "origin" "name"
-test_configured_prune_type unset  true unset true  kept   pruned "origin" "link"
+test_configured_prune_type unset unset unset unset kept pruned "--prune --prune-tags origin" "link"
+test_configured_prune_type unset unset true unset pruned pruned "--prune origin" "name"
+test_configured_prune_type unset unset true unset kept pruned "--prune origin" "link"
+test_configured_prune_type unset unset unset true pruned pruned "--prune origin" "name"
+test_configured_prune_type unset unset unset true kept pruned "--prune origin" "link"
+test_configured_prune_type true unset true unset pruned pruned "origin" "name"
+test_configured_prune_type true unset true unset kept pruned "origin" "link"
+test_configured_prune_type unset true true unset pruned pruned "origin" "name"
+test_configured_prune_type unset true true unset kept pruned "origin" "link"
+test_configured_prune_type unset true unset true pruned pruned "origin" "name"
+test_configured_prune_type unset true unset true kept pruned "origin" "link"
 
 # When all remote.origin.fetch settings are deleted a --prune
 # --prune-tags still implicitly supplies refs/tags/*:refs/tags/* so
@@ -1137,8 +1155,7 @@ test_expect_success 'fetching with auto-gc does not lock up' '
 	)
 '
 
-for section in fetch transfer
-do
+for section in fetch transfer; do
 	test_expect_success "$section.hideRefs affects connectivity check" '
 		GIT_TRACE="$PWD"/trace git -c $section.hideRefs=refs -c \
 			$section.hideRefs="!refs/tags/" fetch &&
@@ -1157,21 +1174,23 @@ test_expect_success 'prepare source branch' '
 	test 3 -le $(wc -l <actual)
 '
 
-validate_store_type () {
+validate_store_type() {
 	git -C dest count-objects -v >actual &&
-	case "$store_type" in
-	packed)
-		grep "^count: 0$" actual ;;
-	loose)
-		grep "^packs: 0$" actual ;;
-	esac || {
+		case "$store_type" in
+		packed)
+			grep "^count: 0$" actual
+			;;
+		loose)
+			grep "^packs: 0$" actual
+			;;
+		esac || {
 		echo "store_type is $store_type"
 		cat actual
 		false
 	}
 }
 
-test_unpack_limit () {
+test_unpack_limit() {
 	store_type=$1
 
 	case "$store_type" in
@@ -1192,43 +1211,39 @@ test_unpack_limit () {
 test_unpack_limit packed
 test_unpack_limit loose
 
-setup_negotiation_tip () {
+setup_negotiation_tip() {
 	SERVER="$1"
 	URL="$2"
 	USE_PROTOCOL_V2="$3"
 
 	rm -rf "$SERVER" client trace &&
-	git init -b main "$SERVER" &&
-	test_commit -C "$SERVER" alpha_1 &&
-	test_commit -C "$SERVER" alpha_2 &&
-	git -C "$SERVER" checkout --orphan beta &&
-	test_commit -C "$SERVER" beta_1 &&
-	test_commit -C "$SERVER" beta_2 &&
-
-	git clone "$URL" client &&
-
-	if test "$USE_PROTOCOL_V2" -eq 1
-	then
-		git -C "$SERVER" config protocol.version 2 &&
-		git -C client config protocol.version 2
-	fi &&
-
-	test_commit -C "$SERVER" beta_s &&
-	git -C "$SERVER" checkout main &&
-	test_commit -C "$SERVER" alpha_s &&
-	git -C "$SERVER" tag -d alpha_1 alpha_2 beta_1 beta_2
+		git init -b main "$SERVER" &&
+		test_commit -C "$SERVER" alpha_1 &&
+		test_commit -C "$SERVER" alpha_2 &&
+		git -C "$SERVER" checkout --orphan beta &&
+		test_commit -C "$SERVER" beta_1 &&
+		test_commit -C "$SERVER" beta_2 &&
+		git clone "$URL" client &&
+		if test "$USE_PROTOCOL_V2" -eq 1; then
+			git -C "$SERVER" config protocol.version 2 &&
+				git -C client config protocol.version 2
+		fi &&
+		test_commit -C "$SERVER" beta_s &&
+		git -C "$SERVER" checkout main &&
+		test_commit -C "$SERVER" alpha_s &&
+		git -C "$SERVER" tag -d alpha_1 alpha_2 beta_1 beta_2
 }
 
-check_negotiation_tip () {
+check_negotiation_tip() {
 	# Ensure that {alpha,beta}_1 are sent as "have", but not {alpha_beta}_2
 	ALPHA_1=$(git -C client rev-parse alpha_1) &&
-	grep "fetch> have $ALPHA_1" trace &&
-	BETA_1=$(git -C client rev-parse beta_1) &&
-	grep "fetch> have $BETA_1" trace &&
-	ALPHA_2=$(git -C client rev-parse alpha_2) &&
-	! grep "fetch> have $ALPHA_2" trace &&
-	BETA_2=$(git -C client rev-parse beta_2) &&
-	! grep "fetch> have $BETA_2" trace
+		grep "fetch> have $ALPHA_1" trace &&
+		BETA_1=$(git -C client rev-parse beta_1) &&
+		grep "fetch> have $BETA_1" trace &&
+		ALPHA_2=$(git -C client rev-parse alpha_2) &&
+		! grep "fetch> have $ALPHA_2" trace &&
+		BETA_2=$(git -C client rev-parse beta_2) &&
+		! grep "fetch> have $BETA_2" trace
 }
 
 test_expect_success '--negotiation-tip limits "have" lines sent' '
diff --git a/t/t5512-ls-remote.sh b/t/t5512-ls-remote.sh
index 64b3491e4e..1b3865e154 100755
--- a/t/t5512-ls-remote.sh
+++ b/t/t5512-ls-remote.sh
@@ -293,6 +293,8 @@ test_expect_success 'ls-remote with filtered symref (refname)' '
 	cat >expect <<-EOF &&
 	ref: refs/heads/main	HEAD
 	$rev	HEAD
+	ref: refs/remotes/origin/main	refs/remotes/origin/HEAD
+	$rev	refs/remotes/origin/HEAD
 	EOF
 	git ls-remote --symref . HEAD >actual &&
 	test_cmp expect actual
diff --git a/t/t5514-fetch-multiple.sh b/t/t5514-fetch-multiple.sh
index 579872c258..e3482b27b2 100755
--- a/t/t5514-fetch-multiple.sh
+++ b/t/t5514-fetch-multiple.sh
@@ -45,14 +45,17 @@ test_expect_success setup '
 '
 
 cat > test/expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
   origin/HEAD -> origin/main
   origin/main
   origin/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -97,6 +100,7 @@ cat > expect << EOF
   origin/HEAD -> origin/main
   origin/main
   origin/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
@@ -112,8 +116,10 @@ test_expect_success 'git fetch --multiple (but only one remote)' '
 '
 
 cat > expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -141,7 +147,7 @@ test_expect_success 'git fetch --multiple (bad remote names)' '
 
 test_expect_success 'git fetch --all (skipFetchAll)' '
 	(cd test4 &&
-	 for b in $(git branch -r)
+	 for b in $(git branch -r | grep -v HEAD)
 	 do
 		git branch -r -d $b || exit 1
 	 done &&
@@ -153,11 +159,14 @@ test_expect_success 'git fetch --all (skipFetchAll)' '
 '
 
 cat > expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -165,7 +174,7 @@ EOF
 
 test_expect_success 'git fetch --multiple (ignoring skipFetchAll)' '
 	(cd test4 &&
-	 for b in $(git branch -r)
+	 for b in $(git branch -r | grep -v HEAD)
 	 do
 		git branch -r -d $b || exit 1
 	 done &&
@@ -221,14 +230,17 @@ test_expect_success 'git fetch --multiple --jobs=0 picks a default' '
 
 create_fetch_all_expect () {
 	cat >expect <<-\EOF
+	  one/HEAD -> one/main
 	  one/main
 	  one/side
 	  origin/HEAD -> origin/main
 	  origin/main
 	  origin/side
+	  three/HEAD -> three/main
 	  three/another
 	  three/main
 	  three/side
+	  two/HEAD -> two/main
 	  two/another
 	  two/main
 	  two/side
@@ -265,6 +277,7 @@ test_expect_success 'git fetch (fetch all remotes with fetch.all = true)' '
 
 create_fetch_one_expect () {
 	cat >expect <<-\EOF
+	  one/HEAD -> one/main
 	  one/main
 	  one/side
 	  origin/HEAD -> origin/main
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 331778bd42..5a051aa0c7 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1395,7 +1395,8 @@ test_expect_success 'fetch follows tags by default' '
 		git tag -m "annotated" tag &&
 		git for-each-ref >tmp1 &&
 		sed -n "p; s|refs/heads/main$|refs/remotes/origin/main|p" tmp1 |
-		sort -k 3 >../expect
+		sed -n "p; s|refs/heads/main$|refs/remotes/origin/HEAD|p"  |
+		sort -k 4 >../expect
 	) &&
 	test_when_finished "rm -rf dst" &&
 	git init dst &&
diff --git a/t/t5527-fetch-odd-refs.sh b/t/t5527-fetch-odd-refs.sh
index 98ece27c6a..d3996af6ee 100755
--- a/t/t5527-fetch-odd-refs.sh
+++ b/t/t5527-fetch-odd-refs.sh
@@ -52,7 +52,8 @@ test_expect_success LONG_REF 'fetch handles extremely long refname' '
 	long
 	main
 	EOF
-	git for-each-ref --format="%(subject)" refs/remotes/long >actual &&
+	git for-each-ref --format="%(subject)" refs/remotes/long \
+		--exclude=refs/remotes/long/HEAD >actual &&
 	test_cmp expect actual
 '
 
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index a66d0e089d..88331d1ceb 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -329,7 +329,8 @@ test_expect_success 'incremental-repack task' '
 
 	# Delete refs that have not been repacked in these packs.
 	git for-each-ref --format="delete %(refname)" \
-		refs/prefetch refs/tags refs/remotes >refs &&
+		refs/prefetch refs/tags refs/remotes \
+		--exclude=refs/remotes/*/HEAD >refs &&
 	git update-ref --stdin <refs &&
 
 	# Replace the object directory with this pack layout.
diff --git a/t/t9210-scalar.sh b/t/t9210-scalar.sh
index 027235d61a..a81662713e 100755
--- a/t/t9210-scalar.sh
+++ b/t/t9210-scalar.sh
@@ -150,7 +150,8 @@ test_expect_success 'scalar clone' '
 			"$(pwd)" &&
 
 		git for-each-ref --format="%(refname)" refs/remotes/origin/ >actual &&
-		echo "refs/remotes/origin/parallel" >expect &&
+		echo "refs/remotes/origin/HEAD" >>expect &&
+		echo "refs/remotes/origin/parallel" >>expect &&
 		test_cmp expect actual &&
 
 		test_path_is_missing 1/2 &&
@@ -219,7 +220,7 @@ test_expect_success 'scalar reconfigure --all with includeIf.onbranch' '
 	done
 '
 
- test_expect_success 'scalar reconfigure --all with detached HEADs' '
+test_expect_success 'scalar reconfigure --all with detached HEADs' '
 	repos="two three four" &&
 	for num in $repos
 	do
diff --git a/t/t9211-scalar-clone.sh b/t/t9211-scalar-clone.sh
index 7869f45ee6..01f71910f5 100755
--- a/t/t9211-scalar-clone.sh
+++ b/t/t9211-scalar-clone.sh
@@ -31,7 +31,7 @@ test_expect_success 'set up repository to clone' '
 	)
 '
 
-cleanup_clone () {
+cleanup_clone() {
 	rm -rf "$1"
 }
 
@@ -127,7 +127,7 @@ test_expect_success '--single-branch clones HEAD only' '
 	(
 		cd $enlistment/src &&
 		git for-each-ref refs/remotes/origin >out &&
-		test_line_count = 1 out &&
+		test_line_count = 2 out &&
 		grep "refs/remotes/origin/base" out
 	) &&
 
@@ -141,7 +141,7 @@ test_expect_success '--no-single-branch clones all branches' '
 	(
 		cd $enlistment/src &&
 		git for-each-ref refs/remotes/origin >out &&
-		test_line_count = 2 out &&
+		test_line_count = 3 out &&
 		grep "refs/remotes/origin/base" out &&
 		grep "refs/remotes/origin/parallel" out
 	) &&
diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh
index cc6aa9f0cd..b663c4609e 100755
--- a/t/t9902-completion.sh
+++ b/t/t9902-completion.sh
@@ -658,6 +658,7 @@ test_expect_success '__git_refs - simple' '
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	matching-tag
@@ -673,6 +674,7 @@ test_expect_success '__git_refs - full refs' '
 	cat >expected <<-EOF &&
 	refs/heads/main
 	refs/heads/matching-branch
+	refs/remotes/other/HEAD
 	refs/remotes/other/branch-in-other
 	refs/remotes/other/main-in-other
 	refs/tags/matching-tag
@@ -729,6 +731,7 @@ test_expect_success '__git_refs - remote on local file system - full refs' '
 test_expect_success '__git_refs - configured remote' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -756,6 +759,7 @@ test_expect_success '__git_refs - configured remote - full refs' '
 test_expect_success '__git_refs - configured remote - repo given on the command line' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -787,6 +791,7 @@ test_expect_success '__git_refs - configured remote - full refs - repo given on
 test_expect_success '__git_refs - configured remote - remote name matches a directory' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -875,12 +880,14 @@ test_expect_success '__git_refs - unique remote branches for git checkout DWIMer
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/ambiguous
 	other/branch-in-other
 	other/main-in-other
 	remote/ambiguous
 	remote/branch-in-remote
 	matching-tag
+	HEAD
 	branch-in-other
 	branch-in-remote
 	main-in-other
@@ -904,6 +911,7 @@ test_expect_success '__git_refs - after --opt=' '
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	matching-tag
@@ -919,6 +927,7 @@ test_expect_success '__git_refs - after --opt= - full refs' '
 	cat >expected <<-EOF &&
 	refs/heads/main
 	refs/heads/matching-branch
+	refs/remotes/other/HEAD
 	refs/remotes/other/branch-in-other
 	refs/remotes/other/main-in-other
 	refs/tags/matching-tag
@@ -935,6 +944,7 @@ test_expect_success '__git refs - excluding refs' '
 	^HEAD
 	^main
 	^matching-branch
+	^other/HEAD
 	^other/branch-in-other
 	^other/main-in-other
 	^matching-tag
@@ -950,6 +960,7 @@ test_expect_success '__git refs - excluding full refs' '
 	cat >expected <<-EOF &&
 	^refs/heads/main
 	^refs/heads/matching-branch
+	^refs/remotes/other/HEAD
 	^refs/remotes/other/branch-in-other
 	^refs/remotes/other/main-in-other
 	^refs/tags/matching-tag
@@ -975,6 +986,7 @@ test_expect_success '__git_refs - do not filter refs unless told so' '
 	main
 	matching-branch
 	matching/branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	other/matching/branch-in-other
@@ -1095,6 +1107,7 @@ test_expect_success '__git_complete_refs - simple' '
 	HEAD Z
 	main Z
 	matching-branch Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	matching-tag Z
@@ -1123,6 +1136,7 @@ test_expect_success '__git_complete_refs - matching' '
 test_expect_success '__git_complete_refs - remote' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main-in-other Z
 	EOF
@@ -1139,9 +1153,11 @@ test_expect_success '__git_complete_refs - track' '
 	HEAD Z
 	main Z
 	matching-branch Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	matching-tag Z
+	HEAD Z
 	branch-in-other Z
 	main-in-other Z
 	EOF
@@ -1184,6 +1200,7 @@ test_expect_success '__git_complete_refs - suffix' '
 	HEAD.
 	main.
 	matching-branch.
+	other/HEAD.
 	other/branch-in-other.
 	other/main-in-other.
 	matching-tag.
@@ -1199,6 +1216,7 @@ test_expect_success '__git_complete_refs - suffix' '
 test_expect_success '__git_complete_fetch_refspecs - simple' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	HEAD:HEAD Z
+	HEAD:HEAD Z
 	branch-in-other:branch-in-other Z
 	main-in-other:main-in-other Z
 	EOF
@@ -1225,6 +1243,7 @@ test_expect_success '__git_complete_fetch_refspecs - matching' '
 test_expect_success '__git_complete_fetch_refspecs - prefix' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	+HEAD:HEAD Z
+	+HEAD:HEAD Z
 	+branch-in-other:branch-in-other Z
 	+main-in-other:main-in-other Z
 	EOF
@@ -1289,6 +1308,7 @@ test_expect_success '__git_complete_worktree_paths with -C' '
 
 test_expect_success 'git switch - with no options, complete local branches and unique remote branch names for DWIM logic' '
 	test_completion "git switch " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1435,11 +1455,13 @@ test_expect_success 'git-bisect - existing view subcommand is recognized and ena
 test_expect_success 'git checkout - completes refs and unique remote branches for DWIM' '
 	test_completion "git checkout " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1461,6 +1483,7 @@ test_expect_success 'git switch - with GIT_COMPLETION_CHECKOUT_NO_GUESS=1, compl
 
 test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_GUESS=1, complete local branches and unique remote names for DWIM logic' '
 	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git switch --guess " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1470,6 +1493,7 @@ test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_G
 
 test_expect_success 'git switch - a later --guess overrides previous --no-guess, complete local and remote unique branches for DWIM' '
 	test_completion "git switch --no-guess --guess " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1490,6 +1514,7 @@ test_expect_success 'git checkout - with GIT_COMPLETION_NO_GUESS=1 only complete
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1498,11 +1523,13 @@ test_expect_success 'git checkout - with GIT_COMPLETION_NO_GUESS=1 only complete
 test_expect_success 'git checkout - --guess overrides GIT_COMPLETION_NO_GUESS=1, complete refs and unique remote branches for DWIM' '
 	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git checkout --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1514,6 +1541,7 @@ test_expect_success 'git checkout - with --no-guess, only completes refs' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1522,11 +1550,13 @@ test_expect_success 'git checkout - with --no-guess, only completes refs' '
 test_expect_success 'git checkout - a later --guess overrides previous --no-guess, complete refs and unique remote branches for DWIM' '
 	test_completion "git checkout --no-guess --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1538,6 +1568,7 @@ test_expect_success 'git checkout - a later --no-guess overrides previous --gues
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1550,6 +1581,7 @@ test_expect_success 'git checkout - with checkout.guess = false, only completes
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1559,11 +1591,13 @@ test_expect_success 'git checkout - with checkout.guess = true, completes refs a
 	test_config checkout.guess true &&
 	test_completion "git checkout " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1573,11 +1607,13 @@ test_expect_success 'git checkout - a later --guess overrides previous checkout.
 	test_config checkout.guess false &&
 	test_completion "git checkout --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1590,6 +1626,7 @@ test_expect_success 'git checkout - a later --no-guess overrides previous checko
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1601,6 +1638,7 @@ test_expect_success 'git switch - with --detach, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1612,6 +1650,7 @@ test_expect_success 'git checkout - with --detach, complete only references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1783,6 +1822,7 @@ test_expect_success 'git switch - with -d, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1794,6 +1834,7 @@ test_expect_success 'git checkout - with -d, complete only references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1801,10 +1842,12 @@ test_expect_success 'git checkout - with -d, complete only references' '
 
 test_expect_success 'git switch - with --track, complete only remote branches' '
 	test_completion "git switch --track " <<-\EOF &&
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
 	test_completion "git switch -t " <<-\EOF
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1812,10 +1855,12 @@ test_expect_success 'git switch - with --track, complete only remote branches' '
 
 test_expect_success 'git checkout - with --track, complete only remote branches' '
 	test_completion "git checkout --track " <<-\EOF &&
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
 	test_completion "git checkout -t " <<-\EOF
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1834,6 +1879,7 @@ test_expect_success 'git checkout - with --no-track, complete only local referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1845,6 +1891,7 @@ test_expect_success 'git switch - with -c, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1856,6 +1903,7 @@ test_expect_success 'git switch - with -C, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1867,6 +1915,7 @@ test_expect_success 'git switch - with -c and --track, complete all references'
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1878,6 +1927,7 @@ test_expect_success 'git switch - with -C and --track, complete all references'
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1889,6 +1939,7 @@ test_expect_success 'git switch - with -c and --no-track, complete all reference
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1900,6 +1951,7 @@ test_expect_success 'git switch - with -C and --no-track, complete all reference
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1911,6 +1963,7 @@ test_expect_success 'git checkout - with -b, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1922,6 +1975,7 @@ test_expect_success 'git checkout - with -B, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1933,6 +1987,7 @@ test_expect_success 'git checkout - with -b and --track, complete all references
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1944,6 +1999,7 @@ test_expect_success 'git checkout - with -B and --track, complete all references
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1955,6 +2011,7 @@ test_expect_success 'git checkout - with -b and --no-track, complete all referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1966,6 +2023,7 @@ test_expect_success 'git checkout - with -B and --no-track, complete all referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1973,6 +2031,7 @@ test_expect_success 'git checkout - with -B and --no-track, complete all referen
 
 test_expect_success 'git switch - for -c, complete local branches and unique remote branches' '
 	test_completion "git switch -c " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1982,6 +2041,7 @@ test_expect_success 'git switch - for -c, complete local branches and unique rem
 
 test_expect_success 'git switch - for -C, complete local branches and unique remote branches' '
 	test_completion "git switch -C " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2019,6 +2079,7 @@ test_expect_success 'git switch - for -C with --no-track, complete local branche
 
 test_expect_success 'git checkout - for -b, complete local branches and unique remote branches' '
 	test_completion "git checkout -b " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2028,6 +2089,7 @@ test_expect_success 'git checkout - for -b, complete local branches and unique r
 
 test_expect_success 'git checkout - for -B, complete local branches and unique remote branches' '
 	test_completion "git checkout -B " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2065,6 +2127,7 @@ test_expect_success 'git checkout - for -B with --no-track, complete local branc
 
 test_expect_success 'git switch - with --orphan completes local branch names and unique remote branch names' '
 	test_completion "git switch --orphan " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2080,6 +2143,7 @@ test_expect_success 'git switch - --orphan with branch already provided complete
 
 test_expect_success 'git checkout - with --orphan completes local branch names and unique remote branch names' '
 	test_completion "git checkout --orphan " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2093,6 +2157,7 @@ test_expect_success 'git checkout - --orphan with branch already provided comple
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
-- 
2.47.0.7.g072c39eddb.dirty
Phillip Wood· Oct 13, 2024, 13:52 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v7 1/6] refs: atomically record overwritten ref in update_symref

Hi Bence
On 13/10/2024 00:03, Bence Ferdinandy wrote:
> When updating a symref it's currently not possible to know for sure what
> was the previous value that was overwritten.

It is if you use a ref transaction rather than call refs_update_symref() and query the ref after calling ref_transaction_prepare() and before calling ref_transaction_commit() which is what the code below does.

> Record the value after the
> ref has been locked if the caller of refs_update_symref requests it via
> a new variable in the function call.

To me this patch and patch 5 feel quite disruptive to all the existing callers which don't need this specialized functionality. I think it would be less disruptive over all if you used a ref transaction rather than calling refs_update_symref() in the final patch. That would enable us to keep the simpler interface for refs_update_symref().

I'm also not sure about the proposed interface I would have thought it would be simpler to take a "char**" rather than an "struct strbuf*" if we do decide that it is useful for callers of refs_update_symref() to query the old value.

Best Wishes
Phillip
Show 280 quoted lines
> Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
> ---
> 
> Notes:
>      v4: new patch
>      
>      v5: - added before_target to reftables backend
>          - added an extra safety check for transaction's existence in refs.c
>      
>      v6: - no change
>      
>      v7: - remove the whole before_target concept from the backends and
>            handle checking it in refs.c instead (thanks Karthik)
>          - rename the before_target to referent which is how the same concept
>            is called in the backends
>          - change commit prefix to be more in line with project standards
> 
>   builtin/branch.c          |  2 +-
>   builtin/checkout.c        |  4 ++--
>   builtin/clone.c           |  6 +++---
>   builtin/notes.c           |  2 +-
>   builtin/remote.c          |  6 +++---
>   builtin/symbolic-ref.c    |  2 +-
>   builtin/worktree.c        |  2 +-
>   refs.c                    | 16 +++++++++++-----
>   refs.h                    |  3 ++-
>   reset.c                   |  2 +-
>   sequencer.c               |  2 +-
>   setup.c                   |  2 +-
>   t/helper/test-ref-store.c |  2 +-
>   13 files changed, 29 insertions(+), 22 deletions(-)
> 
> diff --git a/builtin/branch.c b/builtin/branch.c
> index fd1611ebf5..6c87690b58 100644
> --- a/builtin/branch.c
> +++ b/builtin/branch.c
> @@ -559,7 +559,7 @@ static int replace_each_worktree_head_symref(struct worktree **worktrees,
>   			continue;
>   
>   		refs = get_worktree_ref_store(worktrees[i]);
> -		if (refs_update_symref(refs, "HEAD", newref, logmsg))
> +		if (refs_update_symref(refs, "HEAD", newref, logmsg, NULL))
>   			ret = error(_("HEAD of working tree %s is not updated"),
>   				    worktrees[i]->path);
>   	}
> diff --git a/builtin/checkout.c b/builtin/checkout.c
> index 9c30000d3a..356ee9bcde 100644
> --- a/builtin/checkout.c
> +++ b/builtin/checkout.c
> @@ -1015,7 +1015,7 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
>   			describe_detached_head(_("HEAD is now at"), new_branch_info->commit);
>   		}
>   	} else if (new_branch_info->path) {	/* Switch branches. */
> -		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path, msg.buf) < 0)
> +		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path, msg.buf, NULL) < 0)
>   			die(_("unable to update HEAD"));
>   		if (!opts->quiet) {
>   			if (old_branch_info->path && !strcmp(new_branch_info->path, old_branch_info->path)) {
> @@ -1479,7 +1479,7 @@ static int switch_unborn_to_new_branch(const struct checkout_opts *opts)
>   		die(_("You are on a branch yet to be born"));
>   	strbuf_addf(&branch_ref, "refs/heads/%s", opts->new_branch);
>   	status = refs_update_symref(get_main_ref_store(the_repository),
> -				    "HEAD", branch_ref.buf, "checkout -b");
> +				    "HEAD", branch_ref.buf, "checkout -b", NULL);
>   	strbuf_release(&branch_ref);
>   	if (!opts->quiet)
>   		fprintf(stderr, _("Switched to a new branch '%s'\n"),
> diff --git a/builtin/clone.c b/builtin/clone.c
> index e77339c847..ead2af20ea 100644
> --- a/builtin/clone.c
> +++ b/builtin/clone.c
> @@ -661,7 +661,7 @@ static void update_remote_refs(const struct ref *refs,
>   		strbuf_addstr(&head_ref, "HEAD");
>   		if (refs_update_symref(get_main_ref_store(the_repository), head_ref.buf,
>   				       remote_head_points_at->peer_ref->name,
> -				       msg) < 0)
> +				       msg, NULL) < 0)
>   			die(_("unable to update %s"), head_ref.buf);
>   		strbuf_release(&head_ref);
>   	}
> @@ -673,7 +673,7 @@ static void update_head(const struct ref *our, const struct ref *remote,
>   	const char *head;
>   	if (our && skip_prefix(our->name, "refs/heads/", &head)) {
>   		/* Local default branch link */
> -		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name, NULL) < 0)
> +		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name, NULL, NULL) < 0)
>   			die(_("unable to update HEAD"));
>   		if (!option_bare) {
>   			refs_update_ref(get_main_ref_store(the_repository),
> @@ -702,7 +702,7 @@ static void update_head(const struct ref *our, const struct ref *remote,
>   		 * Unborn head from remote; same as "our" case above except
>   		 * that we have no ref to update.
>   		 */
> -		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn, NULL) < 0)
> +		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn, NULL, NULL) < 0)
>   			die(_("unable to update HEAD"));
>   		if (!option_bare)
>   			install_branch_config(0, head, remote_name, unborn);
> diff --git a/builtin/notes.c b/builtin/notes.c
> index 8c26e45526..ba646f06ff 100644
> --- a/builtin/notes.c
> +++ b/builtin/notes.c
> @@ -980,7 +980,7 @@ static int merge(int argc, const char **argv, const char *prefix)
>   			die(_("a notes merge into %s is already in-progress at %s"),
>   			    notes_ref, wt->path);
>   		free_worktrees(worktrees);
> -		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", notes_ref, NULL))
> +		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", notes_ref, NULL, NULL))
>   			die(_("failed to store link to current notes ref (%s)"),
>   			    notes_ref);
>   		fprintf(stderr, _("Automatic notes merge failed. Fix conflicts in %s "
> diff --git a/builtin/remote.c b/builtin/remote.c
> index 76670ddd8b..d8ff440027 100644
> --- a/builtin/remote.c
> +++ b/builtin/remote.c
> @@ -244,7 +244,7 @@ static int add(int argc, const char **argv, const char *prefix)
>   		strbuf_reset(&buf2);
>   		strbuf_addf(&buf2, "refs/remotes/%s/%s", name, master);
>   
> -		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote add"))
> +		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote add", NULL))
>   			result = error(_("Could not setup master '%s'"), master);
>   	}
>   
> @@ -864,7 +864,7 @@ static int mv(int argc, const char **argv, const char *prefix)
>   		strbuf_reset(&buf3);
>   		strbuf_addf(&buf3, "remote: renamed %s to %s",
>   				item->string, buf.buf);
> -		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, buf3.buf))
> +		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, buf3.buf, NULL))
>   			die(_("creating '%s' failed"), buf.buf);
>   		display_progress(progress, ++refs_renamed_nr);
>   	}
> @@ -1444,7 +1444,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
>   		/* make sure it's valid */
>   		if (!refs_ref_exists(get_main_ref_store(the_repository), buf2.buf))
>   			result |= error(_("Not a valid ref: %s"), buf2.buf);
> -		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head"))
> +		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head", NULL))
>   			result |= error(_("Could not setup %s"), buf.buf);
>   		else if (opt_a)
>   			printf("%s/HEAD set to %s\n", argv[0], head_name);
> diff --git a/builtin/symbolic-ref.c b/builtin/symbolic-ref.c
> index 299d23d76a..7728fbc3c1 100644
> --- a/builtin/symbolic-ref.c
> +++ b/builtin/symbolic-ref.c
> @@ -88,7 +88,7 @@ int cmd_symbolic_ref(int argc,
>   		if (check_refname_format(argv[1], REFNAME_ALLOW_ONELEVEL) < 0)
>   			die("Refusing to set '%s' to invalid ref '%s'", argv[0], argv[1]);
>   		ret = !!refs_update_symref(get_main_ref_store(the_repository),
> -					   argv[0], argv[1], msg);
> +					   argv[0], argv[1], msg, NULL);
>   		break;
>   	default:
>   		usage_with_options(git_symbolic_ref_usage, options);
> diff --git a/builtin/worktree.c b/builtin/worktree.c
> index fc31d072a6..a7ab4193c1 100644
> --- a/builtin/worktree.c
> +++ b/builtin/worktree.c
> @@ -517,7 +517,7 @@ static int add_worktree(const char *path, const char *refname,
>   		ret = refs_update_ref(wt_refs, NULL, "HEAD", &commit->object.oid,
>   				      NULL, 0, UPDATE_REFS_MSG_ON_ERR);
>   	else
> -		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, NULL);
> +		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, NULL, NULL);
>   	if (ret)
>   		goto done;
>   
> diff --git a/refs.c b/refs.c
> index 5f729ed412..b964ac44d0 100644
> --- a/refs.c
> +++ b/refs.c
> @@ -2114,7 +2114,8 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
>   }
>   
>   int refs_update_symref(struct ref_store *refs, const char *ref,
> -		       const char *target, const char *logmsg)
> +		       const char *target, const char *logmsg,
> +		       struct strbuf *referent)
>   {
>   	struct ref_transaction *transaction;
>   	struct strbuf err = STRBUF_INIT;
> @@ -2122,17 +2123,23 @@ int refs_update_symref(struct ref_store *refs, const char *ref,
>   
>   	transaction = ref_store_transaction_begin(refs, &err);
>   	if (!transaction ||
> -	    ref_transaction_update(transaction, ref, NULL, NULL,
> +		ref_transaction_update(transaction, ref, NULL, NULL,
>   				   target, NULL, REF_NO_DEREF,
>   				   logmsg, &err) ||
> -	    ref_transaction_commit(transaction, &err)) {
> +		ref_transaction_prepare(transaction, &err)) {
>   		ret = error("%s", err.buf);
> +		goto cleanup;
>   	}
> +	if (referent)
> +		refs_read_symbolic_ref(refs, ref, referent);
> +
> +	if (ref_transaction_commit(transaction, &err))
> +		ret = error("%s", err.buf);
>   
> +cleanup:
>   	strbuf_release(&err);
>   	if (transaction)
>   		ref_transaction_free(transaction);
> -
>   	return ret;
>   }
>   
> @@ -2948,4 +2955,3 @@ int ref_update_expects_existing_old_ref(struct ref_update *update)
>   	return (update->flags & REF_HAVE_OLD) &&
>   		(!is_null_oid(&update->old_oid) || update->old_target);
>   }
> -
> diff --git a/refs.h b/refs.h
> index 108dfc93b3..b09a3a4384 100644
> --- a/refs.h
> +++ b/refs.h
> @@ -571,7 +571,8 @@ int refs_copy_existing_ref(struct ref_store *refs, const char *oldref,
>   		    const char *newref, const char *logmsg);
>   
>   int refs_update_symref(struct ref_store *refs, const char *refname,
> -		       const char *target, const char *logmsg);
> +		       const char *target, const char *logmsg,
> +		       struct strbuf *referent);
>   
>   enum action_on_err {
>   	UPDATE_REFS_MSG_ON_ERR,
> diff --git a/reset.c b/reset.c
> index b22b1be792..cc36a9ed56 100644
> --- a/reset.c
> +++ b/reset.c
> @@ -76,7 +76,7 @@ static int update_refs(const struct reset_head_opts *opts,
>   		if (!ret)
>   			ret = refs_update_symref(get_main_ref_store(the_repository),
>   						 "HEAD", switch_to_branch,
> -						 reflog_head);
> +						 reflog_head, NULL);
>   	}
>   	if (!ret && run_hook)
>   		run_hooks_l(the_repository, "post-checkout",
> diff --git a/sequencer.c b/sequencer.c
> index 8d01cd50ac..23b162924c 100644
> --- a/sequencer.c
> +++ b/sequencer.c
> @@ -5107,7 +5107,7 @@ static int pick_commits(struct repository *r,
>   			}
>   			msg = reflog_message(opts, "finish", "returning to %s",
>   				head_ref.buf);
> -			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf, msg)) {
> +			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf, msg, NULL)) {
>   				res = error(_("could not update HEAD to %s"),
>   					head_ref.buf);
>   				goto cleanup_head_ref;
> diff --git a/setup.c b/setup.c
> index 94e79b2e48..d95f051465 100644
> --- a/setup.c
> +++ b/setup.c
> @@ -2275,7 +2275,7 @@ void create_reference_database(enum ref_storage_format ref_storage_format,
>   			die(_("invalid initial branch name: '%s'"),
>   			    initial_branch);
>   
> -		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref, NULL) < 0)
> +		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref, NULL, NULL) < 0)
>   			exit(1);
>   		free(ref);
>   	}
> diff --git a/t/helper/test-ref-store.c b/t/helper/test-ref-store.c
> index 65346dee55..a911302bea 100644
> --- a/t/helper/test-ref-store.c
> +++ b/t/helper/test-ref-store.c
> @@ -120,7 +120,7 @@ static int cmd_create_symref(struct ref_store *refs, const char **argv)
>   	const char *target = notnull(*argv++, "target");
>   	const char *logmsg = *argv++;
>   
> -	return refs_update_symref(refs, refname, target, logmsg);
> +	return refs_update_symref(refs, refname, target, logmsg, NULL);
>   }
>   
>   static struct flag_definition transaction_flags[] = {
Bence Ferdinandy· Oct 13, 2024, 21:24 UTC · re: Phillip Wood · lore

Re: [PATCH v7 1/6] refs: atomically record overwritten ref in update_symref

On Sun Oct 13, 2024 at 15:52, Phillip Wood <phillip.wood123@gmail.com> wrote:
Show 9 quoted lines
> Hi Bence
>
> On 13/10/2024 00:03, Bence Ferdinandy wrote:
>> When updating a symref it's currently not possible to know for sure what
>> was the previous value that was overwritten.
>
> It is if you use a ref transaction rather than call refs_update_symref() 
> and query the ref after calling ref_transaction_prepare() and before 
> calling ref_transaction_commit() which is what the code below does.

Yeah, it would be more clear if that sentence would say "when using update_symref".

Show 10 quoted lines
>
>> Record the value after the
>> ref has been locked if the caller of refs_update_symref requests it via
>> a new variable in the function call.
>
> To me this patch and patch 5 feel quite disruptive to all the existing 
> callers which don't need this specialized functionality. I think it 
> would be less disruptive over all if you used a ref transaction rather 
> than calling refs_update_symref() in the final patch. That would enable 
> us to keep the simpler interface for refs_update_symref().

The extra parameter introduced here is actually used in two places by the end of the series, in remote set-head and fetch (of course you could make a similar argument for the functionality added in 5/6 which is only used in fetch by the final patch). To avoid code duplication I think even if we did not touch refs_update_symref() it would make sense to create a "refs_update_symref_extended()" and make refs_update_symref() a wrapper around that with a few less parameters. That would be similar to how refs_update_symref() and refs_update_ref() predetermine a couple of parameters to say transaction_update().

Currently there are 15 calls to refs_update_symref() in total, of these 5 do not use the complete functionality of the function (they pass NULL as logmsg), so the current implementation would not be completely unprecedented. (This tally did make me catch an error on my side: the logmsg in fetch's set_head should be "fetch" not "remote set-head", I'll fix that in a v8).

Imho, even if I manage to come up with a better name than "refs_update_symref_extended()" wouldn't it be more confusing to have two ways to update symrefs via a function call, rather than have one, where _usually_ you pass two NULL-s at the end?

Show 5 quoted lines
>
> I'm also not sure about the proposed interface I would have thought it 
> would be simpler to take a "char**" rather than an "struct strbuf*" if 
> we do decide that it is useful for callers of refs_update_symref() to 
> query the old value.

refs_read_symbolic_ref requires a strbuf, so one would need to be created anyway and I also sort of got the feeling that the project likes to handle refs in strbufs (which may be wrong). Are there any downsides I'm not seeing?

Thanks, Bence

Phillip Wood· Oct 15, 2024, 14:05 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v7 1/6] refs: atomically record overwritten ref in update_symref

Hi Bence
On 13/10/2024 22:24, Bence Ferdinandy wrote:
Show 26 quoted lines
> 
> On Sun Oct 13, 2024 at 15:52, Phillip Wood <phillip.wood123@gmail.com> wrote:
>> On 13/10/2024 00:03, Bence Ferdinandy wrote:
>>> Record the value after the
>>> ref has been locked if the caller of refs_update_symref requests it via
>>> a new variable in the function call.
>>
>> To me this patch and patch 5 feel quite disruptive to all the existing
>> callers which don't need this specialized functionality. I think it
>> would be less disruptive over all if you used a ref transaction rather
>> than calling refs_update_symref() in the final patch. That would enable
>> us to keep the simpler interface for refs_update_symref().
> 
> The extra parameter introduced here is actually used in two places by the end
> of the series, in remote set-head and fetch (of course you could make a similar
> argument for the functionality added in 5/6 which is only used in fetch by the
> final patch). To avoid code duplication I think even if we did not touch
> refs_update_symref() it would make sense to create
> a "refs_update_symref_extended()" and make refs_update_symref() a wrapper
> around that with a few less parameters. That would be similar to how
> refs_update_symref() and refs_update_ref() predetermine a couple of parameters
> to say transaction_update().
> 
> Currently there are 15 calls to refs_update_symref() in total, of these
> 5 do not use the complete functionality of the function (they pass NULL as
> logmsg), so the current implementation would not be completely unprecedented.

As those figures show it's pretty unusual not to pass a reflog message when updating a ref, on the other hand it is very unusual to want the old value so I don't think the two are comparable. At a high level the two callers that want to be able to check the old value are both doing essentially the same thing so can we create a specialized function that encapsulates the functionality needed by --set-head and uses a ref transaction?

Show 17 quoted lines
> (This tally did make me catch an error on my side: the logmsg in fetch's
> set_head should be "fetch" not "remote set-head", I'll fix that in a v8).
> 
> Imho, even if I manage to come up with a better name than
> "refs_update_symref_extended()" wouldn't it be more confusing to have two ways
> to update symrefs via a function call, rather than have one, where _usually_
> you pass two NULL-s at the end?
> 
>>
>> I'm also not sure about the proposed interface I would have thought it
>> would be simpler to take a "char**" rather than an "struct strbuf*" if
>> we do decide that it is useful for callers of refs_update_symref() to
>> query the old value.
> 
> refs_read_symbolic_ref requires a strbuf, so one would need to be created
> anyway and I also sort of got the feeling that the project likes to handle refs
> in strbufs (which may be wrong). Are there any downsides I'm not seeing?

It's true that refs_read_symbolic_ref takes and strbuf. I'd argue that's a mistake for a function that is just returning a string in an "out" parameter as I think it is more continent for the caller not to have to initialize an strbuf just to retrieve the target of a symbolic ref. I alse think that it is inconsistent with functions like refs_resolve_refdup() that return a string.

Best Wishes
Phillip
Bence Ferdinandy· Oct 15, 2024, 17:25 UTC · re: Phillip Wood · lore

Re: [PATCH v7 1/6] refs: atomically record overwritten ref in update_symref

On Tue Oct 15, 2024 at 16:05, Phillip Wood <phillip.wood123@gmail.com> wrote:
Show 37 quoted lines
> Hi Bence
>
> On 13/10/2024 22:24, Bence Ferdinandy wrote:
>> 
>> On Sun Oct 13, 2024 at 15:52, Phillip Wood <phillip.wood123@gmail.com> wrote:
>>> On 13/10/2024 00:03, Bence Ferdinandy wrote:
>>>> Record the value after the
>>>> ref has been locked if the caller of refs_update_symref requests it via
>>>> a new variable in the function call.
>>>
>>> To me this patch and patch 5 feel quite disruptive to all the existing
>>> callers which don't need this specialized functionality. I think it
>>> would be less disruptive over all if you used a ref transaction rather
>>> than calling refs_update_symref() in the final patch. That would enable
>>> us to keep the simpler interface for refs_update_symref().
>> 
>> The extra parameter introduced here is actually used in two places by the end
>> of the series, in remote set-head and fetch (of course you could make a similar
>> argument for the functionality added in 5/6 which is only used in fetch by the
>> final patch). To avoid code duplication I think even if we did not touch
>> refs_update_symref() it would make sense to create
>> a "refs_update_symref_extended()" and make refs_update_symref() a wrapper
>> around that with a few less parameters. That would be similar to how
>> refs_update_symref() and refs_update_ref() predetermine a couple of parameters
>> to say transaction_update().
>> 
>> Currently there are 15 calls to refs_update_symref() in total, of these
>> 5 do not use the complete functionality of the function (they pass NULL as
>> logmsg), so the current implementation would not be completely unprecedented.
>
> As those figures show it's pretty unusual not to pass a reflog message 
> when updating a ref, on the other hand it is very unusual to want the 
> old value so I don't think the two are comparable. At a high level the 
> two callers that want to be able to check the old value are both doing 
> essentially the same thing so can we create a specialized function that 
> encapsulates the functionality needed by --set-head and uses a ref 
> transaction?

Ok, so let's not change the signature of refs_update_symref(). On the other hand the best approach here is still not quite clear to me. As it is now, the series added two extra parameters to refs_update_symref(), the referent and the create_only option. Obviously any new function would also take all the parameters refs_update_symref takes. Should I rename the current (v8) refs_update_symref to something else (name suggestion would be welcome, refs_update_symref_extended?, refs_update_symref_check_current? checking the currently existing symref is the common theme for the two new parameters) and leave refs_update_symref as it is? Or is this really about just not having to change all the already existing callers? Because in that case I'd just wrap the new function, something like

int refs_update_symref(struct ref_store *refs, const char *ref,
		       const char *target, const char *logmsg)
	
	return refs_update_symref_extended(*refs, *ref, *target, *logmsg, NULL, 0);

Let me know what you think, I'll hold off on a v9 until I see more clearly here on what would be preferred.

Show 25 quoted lines
>
>> (This tally did make me catch an error on my side: the logmsg in fetch's
>> set_head should be "fetch" not "remote set-head", I'll fix that in a v8).
>> 
>> Imho, even if I manage to come up with a better name than
>> "refs_update_symref_extended()" wouldn't it be more confusing to have two ways
>> to update symrefs via a function call, rather than have one, where _usually_
>> you pass two NULL-s at the end?
>> 
>>>
>>> I'm also not sure about the proposed interface I would have thought it
>>> would be simpler to take a "char**" rather than an "struct strbuf*" if
>>> we do decide that it is useful for callers of refs_update_symref() to
>>> query the old value.
>> 
>> refs_read_symbolic_ref requires a strbuf, so one would need to be created
>> anyway and I also sort of got the feeling that the project likes to handle refs
>> in strbufs (which may be wrong). Are there any downsides I'm not seeing?
>
> It's true that refs_read_symbolic_ref takes and strbuf. I'd argue that's 
> a mistake for a function that is just returning a string in an "out" 
> parameter as I think it is more continent for the caller not to have to 
> initialize an strbuf just to retrieve the target of a symbolic ref. I 
> alse think that it is inconsistent with functions like 
> refs_resolve_refdup() that return a string.

Ok, I'll change this to use **char. On the other hand, if refs_read_symbolic_ref is inconsistent would it make sense to change it to also use a **char instead of strbuf? There's only four calls to it including the one I just added. Although I might rather do that _after_ this series is resolved :)

Thanks (to everyone) for your time!

Best, Bence

-- 
bence.ferdinandy.com
Bence Ferdinandy· Oct 19, 2024, 22:53 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v7 1/6] refs: atomically record overwritten ref in update_symref

On Tue Oct 15, 2024 at 19:25, Bence Ferdinandy <bence@ferdinandy.com> wrote:
>
> On Tue Oct 15, 2024 at 16:05, Phillip Wood <phillip.wood123@gmail.com> wrote:
[snip]
Show 21 quoted lines
>>>>
>>>> I'm also not sure about the proposed interface I would have thought it
>>>> would be simpler to take a "char**" rather than an "struct strbuf*" if
>>>> we do decide that it is useful for callers of refs_update_symref() to
>>>> query the old value.
>>> 
>>> refs_read_symbolic_ref requires a strbuf, so one would need to be created
>>> anyway and I also sort of got the feeling that the project likes to handle refs
>>> in strbufs (which may be wrong). Are there any downsides I'm not seeing?
>>
>> It's true that refs_read_symbolic_ref takes and strbuf. I'd argue that's 
>> a mistake for a function that is just returning a string in an "out" 
>> parameter as I think it is more continent for the caller not to have to 
>> initialize an strbuf just to retrieve the target of a symbolic ref. I 
>> alse think that it is inconsistent with functions like 
>> refs_resolve_refdup() that return a string.
>
> Ok, I'll change this to use **char. On the other hand, if
> refs_read_symbolic_ref is inconsistent would it make sense to change it to also
> use a **char instead of strbuf? There's only four calls to it including the one
> I just added. Although I might rather do that _after_ this series is resolved :)

I started doing this change, but ran into two things which I would yet again, bring up in defence of strbuf. So not only refs_read_symbolic_ref makes use of strbuf, but

a) I also use it in refs_update_symref[_extended] to check if the caller actually wants that referent or not (passing a NULL or a strbuf). b) the consumer, report_set_head_auto check for !buf->len

Both of these sound way more convenient with strbuf than with *char. Ofc I'm not exactly a C guru so ... Anyhow, v9 does not have this change.

Best, Bence

Bence Ferdinandy· Oct 14, 2024, 22:53 UTC · re: Bence Ferdinandy · lore

[PATCH v8 0/6] set-head/fetch remote/HEAD updates, small change from v7

v8 is minor non-functional fix from v7
❯ git range-diff v7~6..v7 v8~6..v8
1:  e4a2d01bca = 1:  e4a2d01bca refs: atomically record overwritten ref in update_symref
2:  e6c40ef04c = 2:  e6c40ef04c remote set-head: add new variable for readability
3:  aefa49aa0d = 3:  aefa49aa0d remote set-head: better output for --auto
4:  ad8907c6a3 = 4:  ad8907c6a3 refs: add TRANSACTION_CREATE_EXISTS error
5:  ff616d8b77 = 5:  ff616d8b77 refs: add create_only option to refs_update_symref
6:  35b93f454c ! 6:  7b844b1dbb fetch: set remote/HEAD if it does not exist
    @@ Notes
         v7: - no change
    +    v8: - changed logmsg in call to refs_update_symref from "remote
    +          set-head" to "fetch"
    +
      ## builtin/fetch.c ##
     @@ builtin/fetch.c: static int backfill_tags(struct display_state *display_state,
        return retcode;
    @@ builtin/fetch.c: static int backfill_tags(struct display_state *display_state,
     +          if (!refs_ref_exists(refs, b_remote_head.buf))
     +                  result = 1;
     +          else if (refs_update_symref(refs, b_head.buf, b_remote_head.buf,
    -+                                  "remote set-head", &b_local_head, 1))
    ++                                  "fetch", &b_local_head, 1))
     +                  result = 1;
     +          else
     +                  report_set_head(remote, head_name, &b_local_head);
Bence Ferdinandy (6):
  refs: atomically record overwritten ref in update_symref
  remote set-head: add new variable for readability
  remote set-head: better output for --auto
  refs: add TRANSACTION_CREATE_EXISTS error
  refs: add create_only option to refs_update_symref
  fetch: set remote/HEAD if it does not exist
 builtin/branch.c                 |   2 +-
 builtin/checkout.c               |   5 +-
 builtin/clone.c                  |   8 +-
 builtin/fetch.c                  |  82 +++++++++++
 builtin/notes.c                  |   3 +-
 builtin/remote.c                 |  47 +++++--
 builtin/symbolic-ref.c           |   2 +-
 builtin/worktree.c               |   2 +-
 refs.c                           |  38 +++--
 refs.h                           |   7 +-
 refs/files-backend.c             |  24 ++--
 refs/reftable-backend.c          |   6 +-
 reset.c                          |   2 +-
 sequencer.c                      |   3 +-
 setup.c                          |   3 +-
 t/helper/test-ref-store.c        |   2 +-
 t/t4207-log-decoration-colors.sh |   3 +-
 t/t5505-remote.sh                |  62 ++++++++-
 t/t5510-fetch.sh                 | 229 ++++++++++++++++---------------
 t/t5512-ls-remote.sh             |   2 +
 t/t5514-fetch-multiple.sh        |  17 ++-
 t/t5516-fetch-push.sh            |   3 +-
 t/t5527-fetch-odd-refs.sh        |   3 +-
 t/t7900-maintenance.sh           |   3 +-
 t/t9210-scalar.sh                |   5 +-
 t/t9211-scalar-clone.sh          |   6 +-
 t/t9902-completion.sh            |  65 +++++++++
 27 files changed, 469 insertions(+), 165 deletions(-)
-- 
2.47.0.7.g072c39eddb.dirty
Bence Ferdinandy· Oct 14, 2024, 22:53 UTC · re: Bence Ferdinandy · lore

[PATCH v8 1/6] refs: atomically record overwritten ref in update_symref

When updating a symref it's currently not possible to know for sure what was the previous value that was overwritten. Record the value after the ref has been locked if the caller of refs_update_symref requests it via a new variable in the function call.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    
    v5: - added before_target to reftables backend
        - added an extra safety check for transaction's existence in refs.c
    
    v6: - no change
    
    v7: - remove the whole before_target concept from the backends and
          handle checking it in refs.c instead (thanks Karthik)
        - rename the before_target to referent which is how the same concept
          is called in the backends
        - change commit prefix to be more in line with project standards
    
    v8: no change
 builtin/branch.c          |  2 +-
 builtin/checkout.c        |  4 ++--
 builtin/clone.c           |  6 +++---
 builtin/notes.c           |  2 +-
 builtin/remote.c          |  6 +++---
 builtin/symbolic-ref.c    |  2 +-
 builtin/worktree.c        |  2 +-
 refs.c                    | 16 +++++++++++-----
 refs.h                    |  3 ++-
 reset.c                   |  2 +-
 sequencer.c               |  2 +-
 setup.c                   |  2 +-
 t/helper/test-ref-store.c |  2 +-
 13 files changed, 29 insertions(+), 22 deletions(-)
Show changes to 13 files +29 −22

builtin/branch.c, builtin/checkout.c, builtin/clone.c, builtin/notes.c, builtin/remote.c, builtin/symbolic-ref.c, builtin/worktree.c, refs.c, refs.h, reset.c, sequencer.c, setup.c, t/helper/test-ref-store.c

diff --git a/builtin/branch.c b/builtin/branch.c
index fd1611ebf5..6c87690b58 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -559,7 +559,7 @@ static int replace_each_worktree_head_symref(struct worktree **worktrees,
 			continue;
 
 		refs = get_worktree_ref_store(worktrees[i]);
-		if (refs_update_symref(refs, "HEAD", newref, logmsg))
+		if (refs_update_symref(refs, "HEAD", newref, logmsg, NULL))
 			ret = error(_("HEAD of working tree %s is not updated"),
 				    worktrees[i]->path);
 	}
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 9c30000d3a..356ee9bcde 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -1015,7 +1015,7 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 			describe_detached_head(_("HEAD is now at"), new_branch_info->commit);
 		}
 	} else if (new_branch_info->path) {	/* Switch branches. */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path, msg.buf) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path, msg.buf, NULL) < 0)
 			die(_("unable to update HEAD"));
 		if (!opts->quiet) {
 			if (old_branch_info->path && !strcmp(new_branch_info->path, old_branch_info->path)) {
@@ -1479,7 +1479,7 @@ static int switch_unborn_to_new_branch(const struct checkout_opts *opts)
 		die(_("You are on a branch yet to be born"));
 	strbuf_addf(&branch_ref, "refs/heads/%s", opts->new_branch);
 	status = refs_update_symref(get_main_ref_store(the_repository),
-				    "HEAD", branch_ref.buf, "checkout -b");
+				    "HEAD", branch_ref.buf, "checkout -b", NULL);
 	strbuf_release(&branch_ref);
 	if (!opts->quiet)
 		fprintf(stderr, _("Switched to a new branch '%s'\n"),
diff --git a/builtin/clone.c b/builtin/clone.c
index e77339c847..ead2af20ea 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -661,7 +661,7 @@ static void update_remote_refs(const struct ref *refs,
 		strbuf_addstr(&head_ref, "HEAD");
 		if (refs_update_symref(get_main_ref_store(the_repository), head_ref.buf,
 				       remote_head_points_at->peer_ref->name,
-				       msg) < 0)
+				       msg, NULL) < 0)
 			die(_("unable to update %s"), head_ref.buf);
 		strbuf_release(&head_ref);
 	}
@@ -673,7 +673,7 @@ static void update_head(const struct ref *our, const struct ref *remote,
 	const char *head;
 	if (our && skip_prefix(our->name, "refs/heads/", &head)) {
 		/* Local default branch link */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name, NULL, NULL) < 0)
 			die(_("unable to update HEAD"));
 		if (!option_bare) {
 			refs_update_ref(get_main_ref_store(the_repository),
@@ -702,7 +702,7 @@ static void update_head(const struct ref *our, const struct ref *remote,
 		 * Unborn head from remote; same as "our" case above except
 		 * that we have no ref to update.
 		 */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn, NULL, NULL) < 0)
 			die(_("unable to update HEAD"));
 		if (!option_bare)
 			install_branch_config(0, head, remote_name, unborn);
diff --git a/builtin/notes.c b/builtin/notes.c
index 8c26e45526..ba646f06ff 100644
--- a/builtin/notes.c
+++ b/builtin/notes.c
@@ -980,7 +980,7 @@ static int merge(int argc, const char **argv, const char *prefix)
 			die(_("a notes merge into %s is already in-progress at %s"),
 			    notes_ref, wt->path);
 		free_worktrees(worktrees);
-		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", notes_ref, NULL))
+		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", notes_ref, NULL, NULL))
 			die(_("failed to store link to current notes ref (%s)"),
 			    notes_ref);
 		fprintf(stderr, _("Automatic notes merge failed. Fix conflicts in %s "
diff --git a/builtin/remote.c b/builtin/remote.c
index 76670ddd8b..d8ff440027 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -244,7 +244,7 @@ static int add(int argc, const char **argv, const char *prefix)
 		strbuf_reset(&buf2);
 		strbuf_addf(&buf2, "refs/remotes/%s/%s", name, master);
 
-		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote add"))
+		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote add", NULL))
 			result = error(_("Could not setup master '%s'"), master);
 	}
 
@@ -864,7 +864,7 @@ static int mv(int argc, const char **argv, const char *prefix)
 		strbuf_reset(&buf3);
 		strbuf_addf(&buf3, "remote: renamed %s to %s",
 				item->string, buf.buf);
-		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, buf3.buf))
+		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, buf3.buf, NULL))
 			die(_("creating '%s' failed"), buf.buf);
 		display_progress(progress, ++refs_renamed_nr);
 	}
@@ -1444,7 +1444,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		/* make sure it's valid */
 		if (!refs_ref_exists(get_main_ref_store(the_repository), buf2.buf))
 			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head"))
+		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head", NULL))
 			result |= error(_("Could not setup %s"), buf.buf);
 		else if (opt_a)
 			printf("%s/HEAD set to %s\n", argv[0], head_name);
diff --git a/builtin/symbolic-ref.c b/builtin/symbolic-ref.c
index 299d23d76a..7728fbc3c1 100644
--- a/builtin/symbolic-ref.c
+++ b/builtin/symbolic-ref.c
@@ -88,7 +88,7 @@ int cmd_symbolic_ref(int argc,
 		if (check_refname_format(argv[1], REFNAME_ALLOW_ONELEVEL) < 0)
 			die("Refusing to set '%s' to invalid ref '%s'", argv[0], argv[1]);
 		ret = !!refs_update_symref(get_main_ref_store(the_repository),
-					   argv[0], argv[1], msg);
+					   argv[0], argv[1], msg, NULL);
 		break;
 	default:
 		usage_with_options(git_symbolic_ref_usage, options);
diff --git a/builtin/worktree.c b/builtin/worktree.c
index fc31d072a6..a7ab4193c1 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -517,7 +517,7 @@ static int add_worktree(const char *path, const char *refname,
 		ret = refs_update_ref(wt_refs, NULL, "HEAD", &commit->object.oid,
 				      NULL, 0, UPDATE_REFS_MSG_ON_ERR);
 	else
-		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, NULL);
+		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, NULL, NULL);
 	if (ret)
 		goto done;
 
diff --git a/refs.c b/refs.c
index 5f729ed412..b964ac44d0 100644
--- a/refs.c
+++ b/refs.c
@@ -2114,7 +2114,8 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
 }
 
 int refs_update_symref(struct ref_store *refs, const char *ref,
-		       const char *target, const char *logmsg)
+		       const char *target, const char *logmsg,
+		       struct strbuf *referent)
 {
 	struct ref_transaction *transaction;
 	struct strbuf err = STRBUF_INIT;
@@ -2122,17 +2123,23 @@ int refs_update_symref(struct ref_store *refs, const char *ref,
 
 	transaction = ref_store_transaction_begin(refs, &err);
 	if (!transaction ||
-	    ref_transaction_update(transaction, ref, NULL, NULL,
+		ref_transaction_update(transaction, ref, NULL, NULL,
 				   target, NULL, REF_NO_DEREF,
 				   logmsg, &err) ||
-	    ref_transaction_commit(transaction, &err)) {
+		ref_transaction_prepare(transaction, &err)) {
 		ret = error("%s", err.buf);
+		goto cleanup;
 	}
+	if (referent)
+		refs_read_symbolic_ref(refs, ref, referent);
+
+	if (ref_transaction_commit(transaction, &err))
+		ret = error("%s", err.buf);
 
+cleanup:
 	strbuf_release(&err);
 	if (transaction)
 		ref_transaction_free(transaction);
-
 	return ret;
 }
 
@@ -2948,4 +2955,3 @@ int ref_update_expects_existing_old_ref(struct ref_update *update)
 	return (update->flags & REF_HAVE_OLD) &&
 		(!is_null_oid(&update->old_oid) || update->old_target);
 }
-
diff --git a/refs.h b/refs.h
index 108dfc93b3..b09a3a4384 100644
--- a/refs.h
+++ b/refs.h
@@ -571,7 +571,8 @@ int refs_copy_existing_ref(struct ref_store *refs, const char *oldref,
 		    const char *newref, const char *logmsg);
 
 int refs_update_symref(struct ref_store *refs, const char *refname,
-		       const char *target, const char *logmsg);
+		       const char *target, const char *logmsg,
+		       struct strbuf *referent);
 
 enum action_on_err {
 	UPDATE_REFS_MSG_ON_ERR,
diff --git a/reset.c b/reset.c
index b22b1be792..cc36a9ed56 100644
--- a/reset.c
+++ b/reset.c
@@ -76,7 +76,7 @@ static int update_refs(const struct reset_head_opts *opts,
 		if (!ret)
 			ret = refs_update_symref(get_main_ref_store(the_repository),
 						 "HEAD", switch_to_branch,
-						 reflog_head);
+						 reflog_head, NULL);
 	}
 	if (!ret && run_hook)
 		run_hooks_l(the_repository, "post-checkout",
diff --git a/sequencer.c b/sequencer.c
index 8d01cd50ac..23b162924c 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -5107,7 +5107,7 @@ static int pick_commits(struct repository *r,
 			}
 			msg = reflog_message(opts, "finish", "returning to %s",
 				head_ref.buf);
-			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf, msg)) {
+			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf, msg, NULL)) {
 				res = error(_("could not update HEAD to %s"),
 					head_ref.buf);
 				goto cleanup_head_ref;
diff --git a/setup.c b/setup.c
index 94e79b2e48..d95f051465 100644
--- a/setup.c
+++ b/setup.c
@@ -2275,7 +2275,7 @@ void create_reference_database(enum ref_storage_format ref_storage_format,
 			die(_("invalid initial branch name: '%s'"),
 			    initial_branch);
 
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref, NULL, NULL) < 0)
 			exit(1);
 		free(ref);
 	}
diff --git a/t/helper/test-ref-store.c b/t/helper/test-ref-store.c
index 65346dee55..a911302bea 100644
--- a/t/helper/test-ref-store.c
+++ b/t/helper/test-ref-store.c
@@ -120,7 +120,7 @@ static int cmd_create_symref(struct ref_store *refs, const char **argv)
 	const char *target = notnull(*argv++, "target");
 	const char *logmsg = *argv++;
 
-	return refs_update_symref(refs, refname, target, logmsg);
+	return refs_update_symref(refs, refname, target, logmsg, NULL);
 }
 
 static struct flag_definition transaction_flags[] = {
-- 
2.47.0.7.g072c39eddb.dirty
karthik nayak· Oct 15, 2024, 07:41 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v8 1/6] refs: atomically record overwritten ref in update_symref

Bence Ferdinandy <bence@ferdinandy.com> writes:
[snip]
Show 29 quoted lines
> diff --git a/refs.c b/refs.c
> index 5f729ed412..b964ac44d0 100644
> --- a/refs.c
> +++ b/refs.c
> @@ -2114,7 +2114,8 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
>  }
>
>  int refs_update_symref(struct ref_store *refs, const char *ref,
> -		       const char *target, const char *logmsg)
> +		       const char *target, const char *logmsg,
> +		       struct strbuf *referent)
>  {
>  	struct ref_transaction *transaction;
>  	struct strbuf err = STRBUF_INIT;
> @@ -2122,17 +2123,23 @@ int refs_update_symref(struct ref_store *refs, const char *ref,
>
>  	transaction = ref_store_transaction_begin(refs, &err);
>  	if (!transaction ||
> -	    ref_transaction_update(transaction, ref, NULL, NULL,
> +		ref_transaction_update(transaction, ref, NULL, NULL,
>  				   target, NULL, REF_NO_DEREF,
>  				   logmsg, &err) ||
> -	    ref_transaction_commit(transaction, &err)) {
> +		ref_transaction_prepare(transaction, &err)) {
>  		ret = error("%s", err.buf);
> +		goto cleanup;
>  	}
> +	if (referent)
> +		refs_read_symbolic_ref(refs, ref, referent);
Shouldn't we also check the return value here?
Show 10 quoted lines
> +
> +	if (ref_transaction_commit(transaction, &err))
> +		ret = error("%s", err.buf);
>
> +cleanup:
>  	strbuf_release(&err);
>  	if (transaction)
>  		ref_transaction_free(transaction);
> -
>
Why remove this whiteline?
>  	return ret;
>  }
>
[snip]
Bence Ferdinandy· Oct 15, 2024, 16:24 UTC · re: karthik nayak · lore

Re: [PATCH v8 1/6] refs: atomically record overwritten ref in update_symref

On Tue Oct 15, 2024 at 09:41, karthik nayak <karthik.188@gmail.com> wrote:
Show 35 quoted lines
> Bence Ferdinandy <bence@ferdinandy.com> writes:
>
> [snip]
>
>> diff --git a/refs.c b/refs.c
>> index 5f729ed412..b964ac44d0 100644
>> --- a/refs.c
>> +++ b/refs.c
>> @@ -2114,7 +2114,8 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
>>  }
>>
>>  int refs_update_symref(struct ref_store *refs, const char *ref,
>> -		       const char *target, const char *logmsg)
>> +		       const char *target, const char *logmsg,
>> +		       struct strbuf *referent)
>>  {
>>  	struct ref_transaction *transaction;
>>  	struct strbuf err = STRBUF_INIT;
>> @@ -2122,17 +2123,23 @@ int refs_update_symref(struct ref_store *refs, const char *ref,
>>
>>  	transaction = ref_store_transaction_begin(refs, &err);
>>  	if (!transaction ||
>> -	    ref_transaction_update(transaction, ref, NULL, NULL,
>> +		ref_transaction_update(transaction, ref, NULL, NULL,
>>  				   target, NULL, REF_NO_DEREF,
>>  				   logmsg, &err) ||
>> -	    ref_transaction_commit(transaction, &err)) {
>> +		ref_transaction_prepare(transaction, &err)) {
>>  		ret = error("%s", err.buf);
>> +		goto cleanup;
>>  	}
>> +	if (referent)
>> +		refs_read_symbolic_ref(refs, ref, referent);
>
> Shouldn't we also check the return value here?

My reasoning was that if this fails referent will just look like as if it did not exist. Since this is an addition to set-head and fetch failing to set the HEAD in a case which would have previously worked I did not think it prudent to now fail on this for any reason.

Show 13 quoted lines
>
>> +
>> +	if (ref_transaction_commit(transaction, &err))
>> +		ret = error("%s", err.buf);
>>
>> +cleanup:
>>  	strbuf_release(&err);
>>  	if (transaction)
>>  		ref_transaction_free(transaction);
>> -
>>
>
> Why remove this whiteline?
Looks like a mistake made during rebase.
-- 
bence.ferdinandy.com
Bence Ferdinandy· Oct 14, 2024, 22:53 UTC · re: Bence Ferdinandy · lore

[PATCH v8 2/6] remote set-head: add new variable for readability

Instead of calling get_main_ref_store(the_repository) multiple times, call it once and store in a new refs variable. Although this change probably offers some performance benefits, the main purpose is to shorten the line lengths of function calls using this variable for better readability.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v5: new patch (split from the next patch as a preparatory step)
    
    v6: no change
    
    v7: - change commit prefix to be more in line with project standards
    
    v8: no change
 builtin/remote.c | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)
Show changes to builtin/remote.c +4 −3
diff --git a/builtin/remote.c b/builtin/remote.c
index d8ff440027..353ffd2c43 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1404,6 +1404,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 	int i, opt_a = 0, opt_d = 0, result = 0;
 	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;
 	char *head_name = NULL;
+	struct ref_store *refs = get_main_ref_store(the_repository);
 
 	struct option options[] = {
 		OPT_BOOL('a', "auto", &opt_a,
@@ -1434,7 +1435,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 			head_name = xstrdup(states.heads.items[0].string);
 		free_remote_ref_states(&states);
 	} else if (opt_d && !opt_a && argc == 1) {
-		if (refs_delete_ref(get_main_ref_store(the_repository), NULL, buf.buf, NULL, REF_NO_DEREF))
+		if (refs_delete_ref(refs, NULL, buf.buf, NULL, REF_NO_DEREF))
 			result |= error(_("Could not delete %s"), buf.buf);
 	} else
 		usage_with_options(builtin_remote_sethead_usage, options);
@@ -1442,9 +1443,9 @@ static int set_head(int argc, const char **argv, const char *prefix)
 	if (head_name) {
 		strbuf_addf(&buf2, "refs/remotes/%s/%s", argv[0], head_name);
 		/* make sure it's valid */
-		if (!refs_ref_exists(get_main_ref_store(the_repository), buf2.buf))
+		if (!refs_ref_exists(refs, buf2.buf))
 			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head", NULL))
+		else if (refs_update_symref(refs, buf.buf, buf2.buf, "remote set-head", NULL))
 			result |= error(_("Could not setup %s"), buf.buf);
 		else if (opt_a)
 			printf("%s/HEAD set to %s\n", argv[0], head_name);
-- 
2.47.0.7.g072c39eddb.dirty
Bence Ferdinandy· Oct 14, 2024, 22:53 UTC · re: Bence Ferdinandy · lore

[PATCH v8 4/6] refs: add TRANSACTION_CREATE_EXISTS error

Currently there is only one special error for transaction, for when there is a naming conflict, all other errors are dumped under a generic error. Add a new special error case for when the caller requests the reference to be updated only when it does not yet exist and the reference actually does exist.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    v5: no change
    v6: no change
    
    v7: - change commit prefix to be more in line with project standards
        - changed error checking to Karthik's suggestion
    
    v8: no change
 refs.h                  |  4 +++-
 refs/files-backend.c    | 24 ++++++++++++++++--------
 refs/reftable-backend.c |  6 ++++--
 3 files changed, 23 insertions(+), 11 deletions(-)
Show changes to 3 files +23 −11

refs.h, refs/files-backend.c, refs/reftable-backend.c

diff --git a/refs.h b/refs.h
index b09a3a4384..c83b1ec76e 100644
--- a/refs.h
+++ b/refs.h
@@ -759,8 +759,10 @@ int ref_transaction_verify(struct ref_transaction *transaction,
 
 /* Naming conflict (for example, the ref names A and A/B conflict). */
 #define TRANSACTION_NAME_CONFLICT -1
+/* When only creation was requested, but the ref already exists. */
+#define TRANSACTION_CREATE_EXISTS -2
 /* All other errors. */
-#define TRANSACTION_GENERIC_ERROR -2
+#define TRANSACTION_GENERIC_ERROR -3
 
 /*
  * Perform the preparatory stages of committing `transaction`. Acquire
diff --git a/refs/files-backend.c b/refs/files-backend.c
index 0824c0b8a9..e743ec44b5 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -2502,14 +2502,18 @@ static int split_symref_update(struct ref_update *update,
 static int check_old_oid(struct ref_update *update, struct object_id *oid,
 			 struct strbuf *err)
 {
+	int ret = TRANSACTION_GENERIC_ERROR;
+
 	if (!(update->flags & REF_HAVE_OLD) ||
 		   oideq(oid, &update->old_oid))
 		return 0;
 
-	if (is_null_oid(&update->old_oid))
+	if (is_null_oid(&update->old_oid)) {
 		strbuf_addf(err, "cannot lock ref '%s': "
 			    "reference already exists",
 			    ref_update_original_update_refname(update));
+		ret = TRANSACTION_CREATE_EXISTS;
+	}
 	else if (is_null_oid(oid))
 		strbuf_addf(err, "cannot lock ref '%s': "
 			    "reference is missing but expected %s",
@@ -2522,7 +2526,7 @@ static int check_old_oid(struct ref_update *update, struct object_id *oid,
 			    oid_to_hex(oid),
 			    oid_to_hex(&update->old_oid));
 
-	return -1;
+	return ret;
 }
 
 /*
@@ -2602,9 +2606,11 @@ static int lock_ref_for_update(struct files_ref_store *refs,
 					ret = TRANSACTION_GENERIC_ERROR;
 					goto out;
 				}
-			} else if  (check_old_oid(update, &lock->old_oid, err)) {
-				ret = TRANSACTION_GENERIC_ERROR;
-				goto out;
+			} else {
+				ret = check_old_oid(update, &lock->old_oid, err);
+				if  (ret) {
+					goto out;
+				}
 			}
 		} else {
 			/*
@@ -2635,9 +2641,11 @@ static int lock_ref_for_update(struct files_ref_store *refs,
 				    update->old_target);
 			ret = TRANSACTION_GENERIC_ERROR;
 			goto out;
-		} else if  (check_old_oid(update, &lock->old_oid, err)) {
-			ret = TRANSACTION_GENERIC_ERROR;
-			goto out;
+		} else {
+			ret = check_old_oid(update, &lock->old_oid, err);
+			if  (ret) {
+				goto out;
+			}
 		}
 
 		/*
diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
index 3c96fbf66f..ebf8e57fbc 100644
--- a/refs/reftable-backend.c
+++ b/refs/reftable-backend.c
@@ -1206,10 +1206,13 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 				goto done;
 			}
 		} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {
-			if (is_null_oid(&u->old_oid))
+			ret = TRANSACTION_NAME_CONFLICT;
+			if (is_null_oid(&u->old_oid)) {
 				strbuf_addf(err, _("cannot lock ref '%s': "
 						   "reference already exists"),
 					    ref_update_original_update_refname(u));
+				ret = TRANSACTION_CREATE_EXISTS;
+			}
 			else if (is_null_oid(&current_oid))
 				strbuf_addf(err, _("cannot lock ref '%s': "
 						   "reference is missing but expected %s"),
@@ -1221,7 +1224,6 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 					    ref_update_original_update_refname(u),
 					    oid_to_hex(&current_oid),
 					    oid_to_hex(&u->old_oid));
-			ret = -1;
 			goto done;
 		}
 
-- 
2.47.0.7.g072c39eddb.dirty
Bence Ferdinandy· Oct 14, 2024, 22:53 UTC · re: Bence Ferdinandy · lore

[PATCH v8 5/6] refs: add create_only option to refs_update_symref

Allow the caller to specify that it only wants to update the symref if it does not already exist. Silently ignore the error from the transaction API if the symref already exists.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    v5: no change
    
    v6: - switched from bool to int for create_only
        - refactored logic in refs_update_symref with goto as layed out by
          Junio
    
    v7: - change commit prefix to be more in line with project standards
        - refactored code to accommodate changes in the first patch, but
          otherwise no change
    
    v8: no change
 builtin/branch.c          |  2 +-
 builtin/checkout.c        |  5 +++--
 builtin/clone.c           |  8 +++++---
 builtin/notes.c           |  3 ++-
 builtin/remote.c          |  9 ++++++---
 builtin/symbolic-ref.c    |  2 +-
 builtin/worktree.c        |  2 +-
 refs.c                    | 30 ++++++++++++++++++++++--------
 refs.h                    |  2 +-
 reset.c                   |  2 +-
 sequencer.c               |  3 ++-
 setup.c                   |  3 ++-
 t/helper/test-ref-store.c |  2 +-
 13 files changed, 48 insertions(+), 25 deletions(-)
Show changes to 13 files +48 −25

builtin/branch.c, builtin/checkout.c, builtin/clone.c, builtin/notes.c, builtin/remote.c, builtin/symbolic-ref.c, builtin/worktree.c, refs.c, refs.h, reset.c, sequencer.c, setup.c, t/helper/test-ref-store.c

diff --git a/builtin/branch.c b/builtin/branch.c
index 6c87690b58..2fcbcbfd59 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -559,7 +559,7 @@ static int replace_each_worktree_head_symref(struct worktree **worktrees,
 			continue;
 
 		refs = get_worktree_ref_store(worktrees[i]);
-		if (refs_update_symref(refs, "HEAD", newref, logmsg, NULL))
+		if (refs_update_symref(refs, "HEAD", newref, logmsg, NULL, 0))
 			ret = error(_("HEAD of working tree %s is not updated"),
 				    worktrees[i]->path);
 	}
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 356ee9bcde..9abb71dc07 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -1015,7 +1015,8 @@ static void update_refs_for_switch(const struct checkout_opts *opts,
 			describe_detached_head(_("HEAD is now at"), new_branch_info->commit);
 		}
 	} else if (new_branch_info->path) {	/* Switch branches. */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path, msg.buf, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", new_branch_info->path,
+					msg.buf, NULL, 0) < 0)
 			die(_("unable to update HEAD"));
 		if (!opts->quiet) {
 			if (old_branch_info->path && !strcmp(new_branch_info->path, old_branch_info->path)) {
@@ -1479,7 +1480,7 @@ static int switch_unborn_to_new_branch(const struct checkout_opts *opts)
 		die(_("You are on a branch yet to be born"));
 	strbuf_addf(&branch_ref, "refs/heads/%s", opts->new_branch);
 	status = refs_update_symref(get_main_ref_store(the_repository),
-				    "HEAD", branch_ref.buf, "checkout -b", NULL);
+				    "HEAD", branch_ref.buf, "checkout -b", NULL, 0);
 	strbuf_release(&branch_ref);
 	if (!opts->quiet)
 		fprintf(stderr, _("Switched to a new branch '%s'\n"),
diff --git a/builtin/clone.c b/builtin/clone.c
index ead2af20ea..87e8f1421a 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -661,7 +661,7 @@ static void update_remote_refs(const struct ref *refs,
 		strbuf_addstr(&head_ref, "HEAD");
 		if (refs_update_symref(get_main_ref_store(the_repository), head_ref.buf,
 				       remote_head_points_at->peer_ref->name,
-				       msg, NULL) < 0)
+				       msg, NULL, 0) < 0)
 			die(_("unable to update %s"), head_ref.buf);
 		strbuf_release(&head_ref);
 	}
@@ -673,7 +673,8 @@ static void update_head(const struct ref *our, const struct ref *remote,
 	const char *head;
 	if (our && skip_prefix(our->name, "refs/heads/", &head)) {
 		/* Local default branch link */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name, NULL, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", our->name,
+					NULL, NULL, 0) < 0)
 			die(_("unable to update HEAD"));
 		if (!option_bare) {
 			refs_update_ref(get_main_ref_store(the_repository),
@@ -702,7 +703,8 @@ static void update_head(const struct ref *our, const struct ref *remote,
 		 * Unborn head from remote; same as "our" case above except
 		 * that we have no ref to update.
 		 */
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn, NULL, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", unborn,
+					NULL, NULL, 0) < 0)
 			die(_("unable to update HEAD"));
 		if (!option_bare)
 			install_branch_config(0, head, remote_name, unborn);
diff --git a/builtin/notes.c b/builtin/notes.c
index ba646f06ff..7b3138e3c3 100644
--- a/builtin/notes.c
+++ b/builtin/notes.c
@@ -980,7 +980,8 @@ static int merge(int argc, const char **argv, const char *prefix)
 			die(_("a notes merge into %s is already in-progress at %s"),
 			    notes_ref, wt->path);
 		free_worktrees(worktrees);
-		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", notes_ref, NULL, NULL))
+		if (refs_update_symref(get_main_ref_store(the_repository), "NOTES_MERGE_REF", notes_ref,
+					NULL, NULL, 0))
 			die(_("failed to store link to current notes ref (%s)"),
 			    notes_ref);
 		fprintf(stderr, _("Automatic notes merge failed. Fix conflicts in %s "
diff --git a/builtin/remote.c b/builtin/remote.c
index 2b6948439f..d60ec6e907 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -244,7 +244,8 @@ static int add(int argc, const char **argv, const char *prefix)
 		strbuf_reset(&buf2);
 		strbuf_addf(&buf2, "refs/remotes/%s/%s", name, master);
 
-		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote add", NULL))
+		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf,
+					"remote add", NULL, 0))
 			result = error(_("Could not setup master '%s'"), master);
 	}
 
@@ -864,7 +865,8 @@ static int mv(int argc, const char **argv, const char *prefix)
 		strbuf_reset(&buf3);
 		strbuf_addf(&buf3, "remote: renamed %s to %s",
 				item->string, buf.buf);
-		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, buf3.buf, NULL))
+		if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf,
+					buf3.buf, NULL, 0))
 			die(_("creating '%s' failed"), buf.buf);
 		display_progress(progress, ++refs_renamed_nr);
 	}
@@ -1470,7 +1472,8 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		/* make sure it's valid */
 		if (!refs_ref_exists(refs, buf2.buf))
 			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(refs, buf.buf, buf2.buf, "remote set-head", &buf_prev))
+		else if (refs_update_symref(refs, buf.buf, buf2.buf,
+					"remote set-head", &buf_prev, 0))
 			result |= error(_("Could not setup %s"), buf.buf);
 		else if (opt_a) {
 			report_set_head_auto(argv[0], head_name, &buf_prev);
diff --git a/builtin/symbolic-ref.c b/builtin/symbolic-ref.c
index 7728fbc3c1..cfbb53b30c 100644
--- a/builtin/symbolic-ref.c
+++ b/builtin/symbolic-ref.c
@@ -88,7 +88,7 @@ int cmd_symbolic_ref(int argc,
 		if (check_refname_format(argv[1], REFNAME_ALLOW_ONELEVEL) < 0)
 			die("Refusing to set '%s' to invalid ref '%s'", argv[0], argv[1]);
 		ret = !!refs_update_symref(get_main_ref_store(the_repository),
-					   argv[0], argv[1], msg, NULL);
+					   argv[0], argv[1], msg, NULL, 0);
 		break;
 	default:
 		usage_with_options(git_symbolic_ref_usage, options);
diff --git a/builtin/worktree.c b/builtin/worktree.c
index a7ab4193c1..2e95367235 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -517,7 +517,7 @@ static int add_worktree(const char *path, const char *refname,
 		ret = refs_update_ref(wt_refs, NULL, "HEAD", &commit->object.oid,
 				      NULL, 0, UPDATE_REFS_MSG_ON_ERR);
 	else
-		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, NULL, NULL);
+		ret = refs_update_symref(wt_refs, "HEAD", symref.buf, NULL, NULL, 0);
 	if (ret)
 		goto done;
 
diff --git a/refs.c b/refs.c
index b964ac44d0..29c020e78e 100644
--- a/refs.c
+++ b/refs.c
@@ -2115,26 +2115,40 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
 
 int refs_update_symref(struct ref_store *refs, const char *ref,
 		       const char *target, const char *logmsg,
-		       struct strbuf *referent)
+		       struct strbuf *referent, int create_only)
 {
 	struct ref_transaction *transaction;
 	struct strbuf err = STRBUF_INIT;
-	int ret = 0;
+	int ret = 0, prepret = 0;
 
 	transaction = ref_store_transaction_begin(refs, &err);
-	if (!transaction ||
-		ref_transaction_update(transaction, ref, NULL, NULL,
-				   target, NULL, REF_NO_DEREF,
-				   logmsg, &err) ||
-		ref_transaction_prepare(transaction, &err)) {
+	if (!transaction) {
+	error_return:
 		ret = error("%s", err.buf);
 		goto cleanup;
 	}
+	if (create_only) {
+		if (ref_transaction_create(transaction, ref, NULL, target,
+					   REF_NO_DEREF, logmsg, &err))
+			goto error_return;
+		prepret = ref_transaction_prepare(transaction, &err);
+		if (prepret && prepret != TRANSACTION_CREATE_EXISTS)
+			goto error_return;
+	} else {
+		if (ref_transaction_update(transaction, ref, NULL, NULL,
+					   target, NULL, REF_NO_DEREF,
+					   logmsg, &err) ||
+			ref_transaction_prepare(transaction, &err))
+			goto error_return;
+	}
+
 	if (referent)
 		refs_read_symbolic_ref(refs, ref, referent);
 
+	if (prepret == TRANSACTION_CREATE_EXISTS)
+		goto cleanup;
 	if (ref_transaction_commit(transaction, &err))
-		ret = error("%s", err.buf);
+		goto error_return;
 
 cleanup:
 	strbuf_release(&err);
diff --git a/refs.h b/refs.h
index c83b1ec76e..41ae384f11 100644
--- a/refs.h
+++ b/refs.h
@@ -572,7 +572,7 @@ int refs_copy_existing_ref(struct ref_store *refs, const char *oldref,
 
 int refs_update_symref(struct ref_store *refs, const char *refname,
 		       const char *target, const char *logmsg,
-		       struct strbuf *referent);
+		       struct strbuf *referent, int create_only);
 
 enum action_on_err {
 	UPDATE_REFS_MSG_ON_ERR,
diff --git a/reset.c b/reset.c
index cc36a9ed56..919fb84f5f 100644
--- a/reset.c
+++ b/reset.c
@@ -76,7 +76,7 @@ static int update_refs(const struct reset_head_opts *opts,
 		if (!ret)
 			ret = refs_update_symref(get_main_ref_store(the_repository),
 						 "HEAD", switch_to_branch,
-						 reflog_head, NULL);
+						 reflog_head, NULL, 0);
 	}
 	if (!ret && run_hook)
 		run_hooks_l(the_repository, "post-checkout",
diff --git a/sequencer.c b/sequencer.c
index 23b162924c..103d0ddfb2 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -5107,7 +5107,8 @@ static int pick_commits(struct repository *r,
 			}
 			msg = reflog_message(opts, "finish", "returning to %s",
 				head_ref.buf);
-			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf, msg, NULL)) {
+			if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", head_ref.buf,
+						msg, NULL, 0)) {
 				res = error(_("could not update HEAD to %s"),
 					head_ref.buf);
 				goto cleanup_head_ref;
diff --git a/setup.c b/setup.c
index d95f051465..9bbc655ee4 100644
--- a/setup.c
+++ b/setup.c
@@ -2275,7 +2275,8 @@ void create_reference_database(enum ref_storage_format ref_storage_format,
 			die(_("invalid initial branch name: '%s'"),
 			    initial_branch);
 
-		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref, NULL, NULL) < 0)
+		if (refs_update_symref(get_main_ref_store(the_repository), "HEAD", ref,
+					NULL, NULL, 0) < 0)
 			exit(1);
 		free(ref);
 	}
diff --git a/t/helper/test-ref-store.c b/t/helper/test-ref-store.c
index a911302bea..f72b029e51 100644
--- a/t/helper/test-ref-store.c
+++ b/t/helper/test-ref-store.c
@@ -120,7 +120,7 @@ static int cmd_create_symref(struct ref_store *refs, const char **argv)
 	const char *target = notnull(*argv++, "target");
 	const char *logmsg = *argv++;
 
-	return refs_update_symref(refs, refname, target, logmsg, NULL);
+	return refs_update_symref(refs, refname, target, logmsg, NULL, 0);
 }
 
 static struct flag_definition transaction_flags[] = {
-- 
2.47.0.7.g072c39eddb.dirty
Bence Ferdinandy· Oct 14, 2024, 22:53 UTC · re: Bence Ferdinandy · lore

[PATCH v8 6/6] fetch: set remote/HEAD if it does not exist

If the user has remote/HEAD set already and it looks like it has changed on the server, then print a message, otherwise set it if we can. Silently pass if the user already has the same remote/HEAD set as reported by the server or if we encounter any errors along the way.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v3: - does not rely on remote set-head anymore so it only authenticates
        once
        - uses the new REF_CREATE_ONLY to atomically check if the ref exists
          and only write it if it doesn't
        - in all other cases the maximum it does is print a warning
    
    v4: - instead of the discarded REF_CREATE_ONLY, it uses the existing,
          but updated transaction api to request a silent create only
        - it now uses the atomic before_target to determine reporting
        - refactored for legibility
    
    v5: - instead of printing a not too useful message, it now fails
          silently, this in line with the objective to only set up
          remote/HEAD automatically if the right thing is trivial, for
          everything else there is remote set-head
        - fixed all failing tests
        - added two new tests, one for checking if remote/HEAD is set to the
          correct one, and one to test that we do not override remote/HEAD
          if it has changed on the server from what we have locally
    
    v6: - fixed style issues and unintended extra empty line
        - updated function call with bool to int from previous patch's
          change
        - removed calls to error(...) inherited from builtin/remote.c so we
          actually fail silently
        - set the test for remote set-head --auto to the correct value here,
          which was previously erronously set in the remote set-head patch
    
    v7: - no change
    
    v8: - changed logmsg in call to refs_update_symref from "remote
          set-head" to "fetch"
 builtin/fetch.c                  |  82 +++++++++++
 t/t4207-log-decoration-colors.sh |   3 +-
 t/t5505-remote.sh                |  13 +-
 t/t5510-fetch.sh                 | 229 ++++++++++++++++---------------
 t/t5512-ls-remote.sh             |   2 +
 t/t5514-fetch-multiple.sh        |  17 ++-
 t/t5516-fetch-push.sh            |   3 +-
 t/t5527-fetch-odd-refs.sh        |   3 +-
 t/t7900-maintenance.sh           |   3 +-
 t/t9210-scalar.sh                |   5 +-
 t/t9211-scalar-clone.sh          |   6 +-
 t/t9902-completion.sh            |  65 +++++++++
 12 files changed, 308 insertions(+), 123 deletions(-)
Show changes to 12 files +308 −123

builtin/fetch.c, t/t4207-log-decoration-colors.sh, t/t5505-remote.sh, t/t5510-fetch.sh, t/t5512-ls-remote.sh, t/t5514-fetch-multiple.sh, t/t5516-fetch-push.sh, t/t5527-fetch-odd-refs.sh, t/t7900-maintenance.sh, t/t9210-scalar.sh, t/t9211-scalar-clone.sh, t/t9902-completion.sh

diff --git a/builtin/fetch.c b/builtin/fetch.c
index 80a64d0d26..88ebca8294 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1578,6 +1578,80 @@ static int backfill_tags(struct display_state *display_state,
 	return retcode;
 }
 
+static void report_set_head(const char *remote, const char *head_name,
+			struct strbuf *buf_prev) {
+	struct strbuf buf_prefix = STRBUF_INIT;
+	const char *prev_head = NULL;
+
+	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
+	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
+
+	if (prev_head && strcmp(prev_head, head_name)) {
+		printf("'HEAD' at '%s' has changed from '%s' to '%s'\n",
+			remote, prev_head, head_name);
+		printf("Run 'git remote set-head %s %s' to follow the change.\n",
+			remote, head_name);
+	}
+}
+
+static const char *strip_refshead(const char *name){
+	skip_prefix(name, "refs/heads/", &name);
+	return name;
+}
+
+static int set_head(const struct ref *remote_refs)
+{
+	int result = 0;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
+		b_local_head = STRBUF_INIT;
+	const char *remote = gtransport->remote->name;
+	char *head_name = NULL;
+	struct ref *ref, *matches;
+	struct ref *fetch_map = NULL, **fetch_map_tail = &fetch_map;
+	struct refspec_item refspec = {
+		.force = 0,
+		.pattern = 1,
+		.src = (char *) "refs/heads/*",
+		.dst = (char *) "refs/heads/*",
+	};
+	struct string_list heads = STRING_LIST_INIT_DUP;
+	struct ref_store *refs = get_main_ref_store(the_repository);
+
+	get_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);
+	matches = guess_remote_head(find_ref_by_name(remote_refs, "HEAD"),
+				    fetch_map, 1);
+	for (ref = matches; ref; ref = ref->next) {
+		string_list_append(&heads, strip_refshead(ref->name));
+	}
+
+
+	if (!heads.nr)
+		result = 1;
+	else if (heads.nr > 1)
+		result = 1;
+	else
+		head_name = xstrdup(heads.items[0].string);
+	if (head_name) {
+		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
+		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote, head_name);
+		/* make sure it's valid */
+		if (!refs_ref_exists(refs, b_remote_head.buf))
+			result = 1;
+		else if (refs_update_symref(refs, b_head.buf, b_remote_head.buf,
+					"fetch", &b_local_head, 1))
+			result = 1;
+		else
+			report_set_head(remote, head_name, &b_local_head);
+
+		free(head_name);
+	}
+
+	strbuf_release(&b_head);
+	strbuf_release(&b_local_head);
+	strbuf_release(&b_remote_head);
+	return result;
+}
+
 static int do_fetch(struct transport *transport,
 		    struct refspec *rs,
 		    const struct fetch_config *config)
@@ -1647,6 +1721,8 @@ static int do_fetch(struct transport *transport,
 				    "refs/tags/");
 	}
 
+	strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
+
 	if (must_list_refs) {
 		trace2_region_enter("fetch", "remote_refs", the_repository);
 		remote_refs = transport_get_remote_refs(transport,
@@ -1791,6 +1867,12 @@ static int do_fetch(struct transport *transport,
 				  "you need to specify exactly one branch with the --set-upstream option"));
 		}
 	}
+	if (set_head(remote_refs))
+		;
+		/*
+		 * Way too many cases where this can go wrong
+		 * so let's just fail silently for now.
+		 */
 
 cleanup:
 	if (retcode) {
diff --git a/t/t4207-log-decoration-colors.sh b/t/t4207-log-decoration-colors.sh
index 73ea9e5155..d55d22cb2f 100755
--- a/t/t4207-log-decoration-colors.sh
+++ b/t/t4207-log-decoration-colors.sh
@@ -59,7 +59,8 @@ ${c_reset}${c_tag}tag: ${c_reset}${c_tag}v1.0${c_reset}${c_commit}, \
 ${c_reset}${c_tag}tag: ${c_reset}${c_tag}B${c_reset}${c_commit})${c_reset} B
 ${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
 ${c_tag}tag: ${c_reset}${c_tag}A1${c_reset}${c_commit}, \
-${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit})${c_reset} A1
+${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit}, \
+${c_reset}${c_remoteBranch}other/HEAD${c_reset}${c_commit})${c_reset} A1
 	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
 ${c_stash}refs/stash${c_reset}${c_commit})${c_reset} On main: Changes to A.t
 	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index d99cd1d0aa..cb45f22232 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -71,7 +71,7 @@ test_expect_success 'add another remote' '
 		cd test &&
 		git remote add -f second ../two &&
 		tokens_match "origin second" "$(git remote)" &&
-		check_tracking_branch second main side another &&
+		check_tracking_branch second main side another HEAD &&
 		git for-each-ref "--format=%(refname)" refs/remotes |
 		sed -e "/^refs\/remotes\/origin\//d" \
 		    -e "/^refs\/remotes\/second\//d" >actual &&
@@ -473,7 +473,7 @@ test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
 		cd test &&
 		git fetch two "refs/heads/*:refs/remotes/two/*" &&
 		git remote set-head --auto two >output 2>&1 &&
-		echo "'\''two/HEAD'\'' is now created and points to '\''main'\''" >expect &&
+		echo "'\''two/HEAD'\'' is unchanged and points to '\''main'\''" >expect &&
 		test_cmp expect output
 	)
 '
@@ -763,6 +763,7 @@ test_expect_success 'reject --no-no-tags' '
 cat >one/expect <<\EOF
   apis/main
   apis/side
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
@@ -780,6 +781,7 @@ test_expect_success 'update' '
 '
 
 cat >one/expect <<\EOF
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
@@ -792,7 +794,7 @@ EOF
 test_expect_success 'update with arguments' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -835,7 +837,7 @@ EOF
 test_expect_success 'update default' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -847,6 +849,7 @@ test_expect_success 'update default' '
 '
 
 cat >one/expect <<\EOF
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
@@ -869,7 +872,7 @@ test_expect_success 'update default (overridden, with funny whitespace)' '
 test_expect_success 'update (with remotes.default defined)' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index 0890b9f61c..dfc8d748ba 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -75,6 +75,30 @@ test_expect_success "fetch test for-merge" '
 	cut -f -2 .git/FETCH_HEAD >actual &&
 	test_cmp expected actual'
 
+test_expect_success "fetch test remote HEAD" '
+	cd "$D" &&
+	cd two &&
+	git fetch &&
+	git rev-parse --verify refs/remotes/origin/HEAD &&
+	git rev-parse --verify refs/remotes/origin/main &&
+	head=$(git rev-parse refs/remotes/origin/HEAD) &&
+	branch=$(git rev-parse refs/remotes/origin/main) &&
+	test "z$head" = "z$branch"'
+
+test_expect_success "fetch test remote HEAD change" '
+	cd "$D" &&
+	cd two &&
+	git switch -c other &&
+	git push -u origin other &&
+	git rev-parse --verify refs/remotes/origin/HEAD &&
+	git rev-parse --verify refs/remotes/origin/main &&
+	git rev-parse --verify refs/remotes/origin/other &&
+	git remote set-head origin other &&
+	git fetch &&
+	head=$(git rev-parse refs/remotes/origin/HEAD) &&
+	branch=$(git rev-parse refs/remotes/origin/other) &&
+	test "z$head" = "z$branch"'
+
 test_expect_success 'fetch --prune on its own works as expected' '
 	cd "$D" &&
 	git clone . prune &&
@@ -478,7 +502,6 @@ test_expect_success 'unbundle 1' '
 	test_must_fail git fetch "$D/bundle1" main:main
 '
 
-
 test_expect_success 'bundle 1 has only 3 files ' '
 	cd "$D" &&
 	test_bundle_object_count bundle1 3
@@ -819,7 +842,7 @@ test_expect_success 'fetch from multiple configured URLs in single remote' '
 
 # configured prune tests
 
-set_config_tristate () {
+set_config_tristate() {
 	# var=$1 val=$2
 	case "$2" in
 	unset)
@@ -833,12 +856,12 @@ set_config_tristate () {
 	esac
 }
 
-test_configured_prune () {
+test_configured_prune() {
 	test_configured_prune_type "$@" "name"
 	test_configured_prune_type "$@" "link"
 }
 
-test_configured_prune_type () {
+test_configured_prune_type() {
 	fetch_prune=$1
 	remote_origin_prune=$2
 	fetch_prune_tags=$3
@@ -848,8 +871,7 @@ test_configured_prune_type () {
 	cmdline=$7
 	mode=$8
 
-	if test -z "$cmdline_setup"
-	then
+	if test -z "$cmdline_setup"; then
 		test_expect_success 'setup cmdline_setup variable for subsequent test' '
 			remote_url="file://$(git -C one config remote.origin.url)" &&
 			remote_fetch="$(git -C one config remote.origin.fetch)" &&
@@ -857,12 +879,10 @@ test_configured_prune_type () {
 		'
 	fi
 
-	if test "$mode" = 'link'
-	then
+	if test "$mode" = 'link'; then
 		new_cmdline=""
 
-		if test "$cmdline" = ""
-		then
+		if test "$cmdline" = ""; then
 			new_cmdline=$cmdline_setup
 		else
 			new_cmdline=$(perl -e '
@@ -873,10 +893,8 @@ test_configured_prune_type () {
 		fi
 
 		if test "$fetch_prune_tags" = 'true' ||
-		   test "$remote_origin_prune_tags" = 'true'
-		then
-			if ! printf '%s' "$cmdline\n" | grep -q refs/remotes/origin/
-			then
+			test "$remote_origin_prune_tags" = 'true'; then
+			if ! printf '%s' "$cmdline\n" | grep -q refs/remotes/origin/; then
 				new_cmdline="$new_cmdline refs/tags/*:refs/tags/*"
 			fi
 		fi
@@ -946,100 +964,100 @@ test_configured_prune_type () {
 # $7 git-fetch $cmdline:
 #
 #                     $1    $2    $3    $4    $5     $6     $7
-test_configured_prune unset unset unset unset kept   kept   ""
-test_configured_prune unset unset unset unset kept   kept   "--no-prune"
-test_configured_prune unset unset unset unset pruned kept   "--prune"
-test_configured_prune unset unset unset unset kept   pruned \
+test_configured_prune unset unset unset unset kept kept ""
+test_configured_prune unset unset unset unset kept kept "--no-prune"
+test_configured_prune unset unset unset unset pruned kept "--prune"
+test_configured_prune unset unset unset unset kept pruned \
 	"--prune origin refs/tags/*:refs/tags/*"
 test_configured_prune unset unset unset unset pruned pruned \
 	"--prune origin refs/tags/*:refs/tags/* +refs/heads/*:refs/remotes/origin/*"
 
-test_configured_prune false unset unset unset kept   kept   ""
-test_configured_prune false unset unset unset kept   kept   "--no-prune"
-test_configured_prune false unset unset unset pruned kept   "--prune"
+test_configured_prune false unset unset unset kept kept ""
+test_configured_prune false unset unset unset kept kept "--no-prune"
+test_configured_prune false unset unset unset pruned kept "--prune"
 
-test_configured_prune true  unset unset unset pruned kept   ""
-test_configured_prune true  unset unset unset pruned kept   "--prune"
-test_configured_prune true  unset unset unset kept   kept   "--no-prune"
+test_configured_prune true unset unset unset pruned kept ""
+test_configured_prune true unset unset unset pruned kept "--prune"
+test_configured_prune true unset unset unset kept kept "--no-prune"
 
-test_configured_prune unset false unset unset kept   kept   ""
-test_configured_prune unset false unset unset kept   kept   "--no-prune"
-test_configured_prune unset false unset unset pruned kept   "--prune"
+test_configured_prune unset false unset unset kept kept ""
+test_configured_prune unset false unset unset kept kept "--no-prune"
+test_configured_prune unset false unset unset pruned kept "--prune"
 
-test_configured_prune false false unset unset kept   kept   ""
-test_configured_prune false false unset unset kept   kept   "--no-prune"
-test_configured_prune false false unset unset pruned kept   "--prune"
-test_configured_prune false false unset unset kept   pruned \
+test_configured_prune false false unset unset kept kept ""
+test_configured_prune false false unset unset kept kept "--no-prune"
+test_configured_prune false false unset unset pruned kept "--prune"
+test_configured_prune false false unset unset kept pruned \
 	"--prune origin refs/tags/*:refs/tags/*"
 test_configured_prune false false unset unset pruned pruned \
 	"--prune origin refs/tags/*:refs/tags/* +refs/heads/*:refs/remotes/origin/*"
 
-test_configured_prune true  false unset unset kept   kept   ""
-test_configured_prune true  false unset unset pruned kept   "--prune"
-test_configured_prune true  false unset unset kept   kept   "--no-prune"
+test_configured_prune true false unset unset kept kept ""
+test_configured_prune true false unset unset pruned kept "--prune"
+test_configured_prune true false unset unset kept kept "--no-prune"
 
-test_configured_prune unset true  unset unset pruned kept   ""
-test_configured_prune unset true  unset unset kept   kept   "--no-prune"
-test_configured_prune unset true  unset unset pruned kept   "--prune"
+test_configured_prune unset true unset unset pruned kept ""
+test_configured_prune unset true unset unset kept kept "--no-prune"
+test_configured_prune unset true unset unset pruned kept "--prune"
 
-test_configured_prune false true  unset unset pruned kept   ""
-test_configured_prune false true  unset unset kept   kept   "--no-prune"
-test_configured_prune false true  unset unset pruned kept   "--prune"
+test_configured_prune false true unset unset pruned kept ""
+test_configured_prune false true unset unset kept kept "--no-prune"
+test_configured_prune false true unset unset pruned kept "--prune"
 
-test_configured_prune true  true  unset unset pruned kept   ""
-test_configured_prune true  true  unset unset pruned kept   "--prune"
-test_configured_prune true  true  unset unset kept   kept   "--no-prune"
-test_configured_prune true  true  unset unset kept   pruned \
+test_configured_prune true true unset unset pruned kept ""
+test_configured_prune true true unset unset pruned kept "--prune"
+test_configured_prune true true unset unset kept kept "--no-prune"
+test_configured_prune true true unset unset kept pruned \
 	"--prune origin refs/tags/*:refs/tags/*"
-test_configured_prune true  true  unset unset pruned pruned \
+test_configured_prune true true unset unset pruned pruned \
 	"--prune origin refs/tags/*:refs/tags/* +refs/heads/*:refs/remotes/origin/*"
 
 # --prune-tags on its own does nothing, needs --prune as well, same
 # for fetch.pruneTags without fetch.prune
-test_configured_prune unset unset unset unset kept kept     "--prune-tags"
-test_configured_prune unset unset true unset  kept kept     ""
-test_configured_prune unset unset unset true  kept kept     ""
+test_configured_prune unset unset unset unset kept kept "--prune-tags"
+test_configured_prune unset unset true unset kept kept ""
+test_configured_prune unset unset unset true kept kept ""
 
 # These will prune the tags
 test_configured_prune unset unset unset unset pruned pruned "--prune --prune-tags"
-test_configured_prune true  unset true  unset pruned pruned ""
-test_configured_prune unset true  unset true  pruned pruned ""
+test_configured_prune true unset true unset pruned pruned ""
+test_configured_prune unset true unset true pruned pruned ""
 
 # remote.<name>.pruneTags overrides fetch.pruneTags, just like
 # remote.<name>.prune overrides fetch.prune if set.
-test_configured_prune true  unset true unset pruned pruned  ""
-test_configured_prune false true  false true  pruned pruned ""
-test_configured_prune true  false true  false kept   kept   ""
+test_configured_prune true unset true unset pruned pruned ""
+test_configured_prune false true false true pruned pruned ""
+test_configured_prune true false true false kept kept ""
 
 # When --prune-tags is supplied it's ignored if an explicit refspec is
 # given, same for the configuration options.
 test_configured_prune unset unset unset unset pruned kept \
 	"--prune --prune-tags origin +refs/heads/*:refs/remotes/origin/*"
-test_configured_prune unset unset true  unset pruned kept \
+test_configured_prune unset unset true unset pruned kept \
 	"--prune origin +refs/heads/*:refs/remotes/origin/*"
-test_configured_prune unset unset unset true pruned  kept \
+test_configured_prune unset unset unset true pruned kept \
 	"--prune origin +refs/heads/*:refs/remotes/origin/*"
 
 # Pruning that also takes place if a file:// url replaces a named
 # remote. However, because there's no implicit
 # +refs/heads/*:refs/remotes/origin/* refspec and supplying it on the
 # command-line negates --prune-tags, the branches will not be pruned.
-test_configured_prune_type unset unset unset unset kept   kept   "origin --prune-tags" "name"
-test_configured_prune_type unset unset unset unset kept   kept   "origin --prune-tags" "link"
+test_configured_prune_type unset unset unset unset kept kept "origin --prune-tags" "name"
+test_configured_prune_type unset unset unset unset kept kept "origin --prune-tags" "link"
 test_configured_prune_type unset unset unset unset pruned pruned "origin --prune --prune-tags" "name"
-test_configured_prune_type unset unset unset unset kept   pruned "origin --prune --prune-tags" "link"
+test_configured_prune_type unset unset unset unset kept pruned "origin --prune --prune-tags" "link"
 test_configured_prune_type unset unset unset unset pruned pruned "--prune --prune-tags origin" "name"
-test_configured_prune_type unset unset unset unset kept   pruned "--prune --prune-tags origin" "link"
-test_configured_prune_type unset unset true  unset pruned pruned "--prune origin" "name"
-test_configured_prune_type unset unset true  unset kept   pruned "--prune origin" "link"
-test_configured_prune_type unset unset unset true  pruned pruned "--prune origin" "name"
-test_configured_prune_type unset unset unset true  kept   pruned "--prune origin" "link"
-test_configured_prune_type true  unset true  unset pruned pruned "origin" "name"
-test_configured_prune_type true  unset true  unset kept   pruned "origin" "link"
-test_configured_prune_type unset  true true  unset pruned pruned "origin" "name"
-test_configured_prune_type unset  true true  unset kept   pruned "origin" "link"
-test_configured_prune_type unset  true unset true  pruned pruned "origin" "name"
-test_configured_prune_type unset  true unset true  kept   pruned "origin" "link"
+test_configured_prune_type unset unset unset unset kept pruned "--prune --prune-tags origin" "link"
+test_configured_prune_type unset unset true unset pruned pruned "--prune origin" "name"
+test_configured_prune_type unset unset true unset kept pruned "--prune origin" "link"
+test_configured_prune_type unset unset unset true pruned pruned "--prune origin" "name"
+test_configured_prune_type unset unset unset true kept pruned "--prune origin" "link"
+test_configured_prune_type true unset true unset pruned pruned "origin" "name"
+test_configured_prune_type true unset true unset kept pruned "origin" "link"
+test_configured_prune_type unset true true unset pruned pruned "origin" "name"
+test_configured_prune_type unset true true unset kept pruned "origin" "link"
+test_configured_prune_type unset true unset true pruned pruned "origin" "name"
+test_configured_prune_type unset true unset true kept pruned "origin" "link"
 
 # When all remote.origin.fetch settings are deleted a --prune
 # --prune-tags still implicitly supplies refs/tags/*:refs/tags/* so
@@ -1137,8 +1155,7 @@ test_expect_success 'fetching with auto-gc does not lock up' '
 	)
 '
 
-for section in fetch transfer
-do
+for section in fetch transfer; do
 	test_expect_success "$section.hideRefs affects connectivity check" '
 		GIT_TRACE="$PWD"/trace git -c $section.hideRefs=refs -c \
 			$section.hideRefs="!refs/tags/" fetch &&
@@ -1157,21 +1174,23 @@ test_expect_success 'prepare source branch' '
 	test 3 -le $(wc -l <actual)
 '
 
-validate_store_type () {
+validate_store_type() {
 	git -C dest count-objects -v >actual &&
-	case "$store_type" in
-	packed)
-		grep "^count: 0$" actual ;;
-	loose)
-		grep "^packs: 0$" actual ;;
-	esac || {
+		case "$store_type" in
+		packed)
+			grep "^count: 0$" actual
+			;;
+		loose)
+			grep "^packs: 0$" actual
+			;;
+		esac || {
 		echo "store_type is $store_type"
 		cat actual
 		false
 	}
 }
 
-test_unpack_limit () {
+test_unpack_limit() {
 	store_type=$1
 
 	case "$store_type" in
@@ -1192,43 +1211,39 @@ test_unpack_limit () {
 test_unpack_limit packed
 test_unpack_limit loose
 
-setup_negotiation_tip () {
+setup_negotiation_tip() {
 	SERVER="$1"
 	URL="$2"
 	USE_PROTOCOL_V2="$3"
 
 	rm -rf "$SERVER" client trace &&
-	git init -b main "$SERVER" &&
-	test_commit -C "$SERVER" alpha_1 &&
-	test_commit -C "$SERVER" alpha_2 &&
-	git -C "$SERVER" checkout --orphan beta &&
-	test_commit -C "$SERVER" beta_1 &&
-	test_commit -C "$SERVER" beta_2 &&
-
-	git clone "$URL" client &&
-
-	if test "$USE_PROTOCOL_V2" -eq 1
-	then
-		git -C "$SERVER" config protocol.version 2 &&
-		git -C client config protocol.version 2
-	fi &&
-
-	test_commit -C "$SERVER" beta_s &&
-	git -C "$SERVER" checkout main &&
-	test_commit -C "$SERVER" alpha_s &&
-	git -C "$SERVER" tag -d alpha_1 alpha_2 beta_1 beta_2
+		git init -b main "$SERVER" &&
+		test_commit -C "$SERVER" alpha_1 &&
+		test_commit -C "$SERVER" alpha_2 &&
+		git -C "$SERVER" checkout --orphan beta &&
+		test_commit -C "$SERVER" beta_1 &&
+		test_commit -C "$SERVER" beta_2 &&
+		git clone "$URL" client &&
+		if test "$USE_PROTOCOL_V2" -eq 1; then
+			git -C "$SERVER" config protocol.version 2 &&
+				git -C client config protocol.version 2
+		fi &&
+		test_commit -C "$SERVER" beta_s &&
+		git -C "$SERVER" checkout main &&
+		test_commit -C "$SERVER" alpha_s &&
+		git -C "$SERVER" tag -d alpha_1 alpha_2 beta_1 beta_2
 }
 
-check_negotiation_tip () {
+check_negotiation_tip() {
 	# Ensure that {alpha,beta}_1 are sent as "have", but not {alpha_beta}_2
 	ALPHA_1=$(git -C client rev-parse alpha_1) &&
-	grep "fetch> have $ALPHA_1" trace &&
-	BETA_1=$(git -C client rev-parse beta_1) &&
-	grep "fetch> have $BETA_1" trace &&
-	ALPHA_2=$(git -C client rev-parse alpha_2) &&
-	! grep "fetch> have $ALPHA_2" trace &&
-	BETA_2=$(git -C client rev-parse beta_2) &&
-	! grep "fetch> have $BETA_2" trace
+		grep "fetch> have $ALPHA_1" trace &&
+		BETA_1=$(git -C client rev-parse beta_1) &&
+		grep "fetch> have $BETA_1" trace &&
+		ALPHA_2=$(git -C client rev-parse alpha_2) &&
+		! grep "fetch> have $ALPHA_2" trace &&
+		BETA_2=$(git -C client rev-parse beta_2) &&
+		! grep "fetch> have $BETA_2" trace
 }
 
 test_expect_success '--negotiation-tip limits "have" lines sent' '
diff --git a/t/t5512-ls-remote.sh b/t/t5512-ls-remote.sh
index 64b3491e4e..1b3865e154 100755
--- a/t/t5512-ls-remote.sh
+++ b/t/t5512-ls-remote.sh
@@ -293,6 +293,8 @@ test_expect_success 'ls-remote with filtered symref (refname)' '
 	cat >expect <<-EOF &&
 	ref: refs/heads/main	HEAD
 	$rev	HEAD
+	ref: refs/remotes/origin/main	refs/remotes/origin/HEAD
+	$rev	refs/remotes/origin/HEAD
 	EOF
 	git ls-remote --symref . HEAD >actual &&
 	test_cmp expect actual
diff --git a/t/t5514-fetch-multiple.sh b/t/t5514-fetch-multiple.sh
index 579872c258..e3482b27b2 100755
--- a/t/t5514-fetch-multiple.sh
+++ b/t/t5514-fetch-multiple.sh
@@ -45,14 +45,17 @@ test_expect_success setup '
 '
 
 cat > test/expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
   origin/HEAD -> origin/main
   origin/main
   origin/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -97,6 +100,7 @@ cat > expect << EOF
   origin/HEAD -> origin/main
   origin/main
   origin/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
@@ -112,8 +116,10 @@ test_expect_success 'git fetch --multiple (but only one remote)' '
 '
 
 cat > expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -141,7 +147,7 @@ test_expect_success 'git fetch --multiple (bad remote names)' '
 
 test_expect_success 'git fetch --all (skipFetchAll)' '
 	(cd test4 &&
-	 for b in $(git branch -r)
+	 for b in $(git branch -r | grep -v HEAD)
 	 do
 		git branch -r -d $b || exit 1
 	 done &&
@@ -153,11 +159,14 @@ test_expect_success 'git fetch --all (skipFetchAll)' '
 '
 
 cat > expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -165,7 +174,7 @@ EOF
 
 test_expect_success 'git fetch --multiple (ignoring skipFetchAll)' '
 	(cd test4 &&
-	 for b in $(git branch -r)
+	 for b in $(git branch -r | grep -v HEAD)
 	 do
 		git branch -r -d $b || exit 1
 	 done &&
@@ -221,14 +230,17 @@ test_expect_success 'git fetch --multiple --jobs=0 picks a default' '
 
 create_fetch_all_expect () {
 	cat >expect <<-\EOF
+	  one/HEAD -> one/main
 	  one/main
 	  one/side
 	  origin/HEAD -> origin/main
 	  origin/main
 	  origin/side
+	  three/HEAD -> three/main
 	  three/another
 	  three/main
 	  three/side
+	  two/HEAD -> two/main
 	  two/another
 	  two/main
 	  two/side
@@ -265,6 +277,7 @@ test_expect_success 'git fetch (fetch all remotes with fetch.all = true)' '
 
 create_fetch_one_expect () {
 	cat >expect <<-\EOF
+	  one/HEAD -> one/main
 	  one/main
 	  one/side
 	  origin/HEAD -> origin/main
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 331778bd42..5a051aa0c7 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1395,7 +1395,8 @@ test_expect_success 'fetch follows tags by default' '
 		git tag -m "annotated" tag &&
 		git for-each-ref >tmp1 &&
 		sed -n "p; s|refs/heads/main$|refs/remotes/origin/main|p" tmp1 |
-		sort -k 3 >../expect
+		sed -n "p; s|refs/heads/main$|refs/remotes/origin/HEAD|p"  |
+		sort -k 4 >../expect
 	) &&
 	test_when_finished "rm -rf dst" &&
 	git init dst &&
diff --git a/t/t5527-fetch-odd-refs.sh b/t/t5527-fetch-odd-refs.sh
index 98ece27c6a..d3996af6ee 100755
--- a/t/t5527-fetch-odd-refs.sh
+++ b/t/t5527-fetch-odd-refs.sh
@@ -52,7 +52,8 @@ test_expect_success LONG_REF 'fetch handles extremely long refname' '
 	long
 	main
 	EOF
-	git for-each-ref --format="%(subject)" refs/remotes/long >actual &&
+	git for-each-ref --format="%(subject)" refs/remotes/long \
+		--exclude=refs/remotes/long/HEAD >actual &&
 	test_cmp expect actual
 '
 
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index a66d0e089d..88331d1ceb 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -329,7 +329,8 @@ test_expect_success 'incremental-repack task' '
 
 	# Delete refs that have not been repacked in these packs.
 	git for-each-ref --format="delete %(refname)" \
-		refs/prefetch refs/tags refs/remotes >refs &&
+		refs/prefetch refs/tags refs/remotes \
+		--exclude=refs/remotes/*/HEAD >refs &&
 	git update-ref --stdin <refs &&
 
 	# Replace the object directory with this pack layout.
diff --git a/t/t9210-scalar.sh b/t/t9210-scalar.sh
index 027235d61a..a81662713e 100755
--- a/t/t9210-scalar.sh
+++ b/t/t9210-scalar.sh
@@ -150,7 +150,8 @@ test_expect_success 'scalar clone' '
 			"$(pwd)" &&
 
 		git for-each-ref --format="%(refname)" refs/remotes/origin/ >actual &&
-		echo "refs/remotes/origin/parallel" >expect &&
+		echo "refs/remotes/origin/HEAD" >>expect &&
+		echo "refs/remotes/origin/parallel" >>expect &&
 		test_cmp expect actual &&
 
 		test_path_is_missing 1/2 &&
@@ -219,7 +220,7 @@ test_expect_success 'scalar reconfigure --all with includeIf.onbranch' '
 	done
 '
 
- test_expect_success 'scalar reconfigure --all with detached HEADs' '
+test_expect_success 'scalar reconfigure --all with detached HEADs' '
 	repos="two three four" &&
 	for num in $repos
 	do
diff --git a/t/t9211-scalar-clone.sh b/t/t9211-scalar-clone.sh
index 7869f45ee6..01f71910f5 100755
--- a/t/t9211-scalar-clone.sh
+++ b/t/t9211-scalar-clone.sh
@@ -31,7 +31,7 @@ test_expect_success 'set up repository to clone' '
 	)
 '
 
-cleanup_clone () {
+cleanup_clone() {
 	rm -rf "$1"
 }
 
@@ -127,7 +127,7 @@ test_expect_success '--single-branch clones HEAD only' '
 	(
 		cd $enlistment/src &&
 		git for-each-ref refs/remotes/origin >out &&
-		test_line_count = 1 out &&
+		test_line_count = 2 out &&
 		grep "refs/remotes/origin/base" out
 	) &&
 
@@ -141,7 +141,7 @@ test_expect_success '--no-single-branch clones all branches' '
 	(
 		cd $enlistment/src &&
 		git for-each-ref refs/remotes/origin >out &&
-		test_line_count = 2 out &&
+		test_line_count = 3 out &&
 		grep "refs/remotes/origin/base" out &&
 		grep "refs/remotes/origin/parallel" out
 	) &&
diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh
index cc6aa9f0cd..b663c4609e 100755
--- a/t/t9902-completion.sh
+++ b/t/t9902-completion.sh
@@ -658,6 +658,7 @@ test_expect_success '__git_refs - simple' '
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	matching-tag
@@ -673,6 +674,7 @@ test_expect_success '__git_refs - full refs' '
 	cat >expected <<-EOF &&
 	refs/heads/main
 	refs/heads/matching-branch
+	refs/remotes/other/HEAD
 	refs/remotes/other/branch-in-other
 	refs/remotes/other/main-in-other
 	refs/tags/matching-tag
@@ -729,6 +731,7 @@ test_expect_success '__git_refs - remote on local file system - full refs' '
 test_expect_success '__git_refs - configured remote' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -756,6 +759,7 @@ test_expect_success '__git_refs - configured remote - full refs' '
 test_expect_success '__git_refs - configured remote - repo given on the command line' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -787,6 +791,7 @@ test_expect_success '__git_refs - configured remote - full refs - repo given on
 test_expect_success '__git_refs - configured remote - remote name matches a directory' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -875,12 +880,14 @@ test_expect_success '__git_refs - unique remote branches for git checkout DWIMer
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/ambiguous
 	other/branch-in-other
 	other/main-in-other
 	remote/ambiguous
 	remote/branch-in-remote
 	matching-tag
+	HEAD
 	branch-in-other
 	branch-in-remote
 	main-in-other
@@ -904,6 +911,7 @@ test_expect_success '__git_refs - after --opt=' '
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	matching-tag
@@ -919,6 +927,7 @@ test_expect_success '__git_refs - after --opt= - full refs' '
 	cat >expected <<-EOF &&
 	refs/heads/main
 	refs/heads/matching-branch
+	refs/remotes/other/HEAD
 	refs/remotes/other/branch-in-other
 	refs/remotes/other/main-in-other
 	refs/tags/matching-tag
@@ -935,6 +944,7 @@ test_expect_success '__git refs - excluding refs' '
 	^HEAD
 	^main
 	^matching-branch
+	^other/HEAD
 	^other/branch-in-other
 	^other/main-in-other
 	^matching-tag
@@ -950,6 +960,7 @@ test_expect_success '__git refs - excluding full refs' '
 	cat >expected <<-EOF &&
 	^refs/heads/main
 	^refs/heads/matching-branch
+	^refs/remotes/other/HEAD
 	^refs/remotes/other/branch-in-other
 	^refs/remotes/other/main-in-other
 	^refs/tags/matching-tag
@@ -975,6 +986,7 @@ test_expect_success '__git_refs - do not filter refs unless told so' '
 	main
 	matching-branch
 	matching/branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	other/matching/branch-in-other
@@ -1095,6 +1107,7 @@ test_expect_success '__git_complete_refs - simple' '
 	HEAD Z
 	main Z
 	matching-branch Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	matching-tag Z
@@ -1123,6 +1136,7 @@ test_expect_success '__git_complete_refs - matching' '
 test_expect_success '__git_complete_refs - remote' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main-in-other Z
 	EOF
@@ -1139,9 +1153,11 @@ test_expect_success '__git_complete_refs - track' '
 	HEAD Z
 	main Z
 	matching-branch Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	matching-tag Z
+	HEAD Z
 	branch-in-other Z
 	main-in-other Z
 	EOF
@@ -1184,6 +1200,7 @@ test_expect_success '__git_complete_refs - suffix' '
 	HEAD.
 	main.
 	matching-branch.
+	other/HEAD.
 	other/branch-in-other.
 	other/main-in-other.
 	matching-tag.
@@ -1199,6 +1216,7 @@ test_expect_success '__git_complete_refs - suffix' '
 test_expect_success '__git_complete_fetch_refspecs - simple' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	HEAD:HEAD Z
+	HEAD:HEAD Z
 	branch-in-other:branch-in-other Z
 	main-in-other:main-in-other Z
 	EOF
@@ -1225,6 +1243,7 @@ test_expect_success '__git_complete_fetch_refspecs - matching' '
 test_expect_success '__git_complete_fetch_refspecs - prefix' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	+HEAD:HEAD Z
+	+HEAD:HEAD Z
 	+branch-in-other:branch-in-other Z
 	+main-in-other:main-in-other Z
 	EOF
@@ -1289,6 +1308,7 @@ test_expect_success '__git_complete_worktree_paths with -C' '
 
 test_expect_success 'git switch - with no options, complete local branches and unique remote branch names for DWIM logic' '
 	test_completion "git switch " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1435,11 +1455,13 @@ test_expect_success 'git-bisect - existing view subcommand is recognized and ena
 test_expect_success 'git checkout - completes refs and unique remote branches for DWIM' '
 	test_completion "git checkout " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1461,6 +1483,7 @@ test_expect_success 'git switch - with GIT_COMPLETION_CHECKOUT_NO_GUESS=1, compl
 
 test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_GUESS=1, complete local branches and unique remote names for DWIM logic' '
 	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git switch --guess " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1470,6 +1493,7 @@ test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_G
 
 test_expect_success 'git switch - a later --guess overrides previous --no-guess, complete local and remote unique branches for DWIM' '
 	test_completion "git switch --no-guess --guess " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1490,6 +1514,7 @@ test_expect_success 'git checkout - with GIT_COMPLETION_NO_GUESS=1 only complete
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1498,11 +1523,13 @@ test_expect_success 'git checkout - with GIT_COMPLETION_NO_GUESS=1 only complete
 test_expect_success 'git checkout - --guess overrides GIT_COMPLETION_NO_GUESS=1, complete refs and unique remote branches for DWIM' '
 	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git checkout --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1514,6 +1541,7 @@ test_expect_success 'git checkout - with --no-guess, only completes refs' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1522,11 +1550,13 @@ test_expect_success 'git checkout - with --no-guess, only completes refs' '
 test_expect_success 'git checkout - a later --guess overrides previous --no-guess, complete refs and unique remote branches for DWIM' '
 	test_completion "git checkout --no-guess --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1538,6 +1568,7 @@ test_expect_success 'git checkout - a later --no-guess overrides previous --gues
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1550,6 +1581,7 @@ test_expect_success 'git checkout - with checkout.guess = false, only completes
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1559,11 +1591,13 @@ test_expect_success 'git checkout - with checkout.guess = true, completes refs a
 	test_config checkout.guess true &&
 	test_completion "git checkout " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1573,11 +1607,13 @@ test_expect_success 'git checkout - a later --guess overrides previous checkout.
 	test_config checkout.guess false &&
 	test_completion "git checkout --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1590,6 +1626,7 @@ test_expect_success 'git checkout - a later --no-guess overrides previous checko
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1601,6 +1638,7 @@ test_expect_success 'git switch - with --detach, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1612,6 +1650,7 @@ test_expect_success 'git checkout - with --detach, complete only references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1783,6 +1822,7 @@ test_expect_success 'git switch - with -d, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1794,6 +1834,7 @@ test_expect_success 'git checkout - with -d, complete only references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1801,10 +1842,12 @@ test_expect_success 'git checkout - with -d, complete only references' '
 
 test_expect_success 'git switch - with --track, complete only remote branches' '
 	test_completion "git switch --track " <<-\EOF &&
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
 	test_completion "git switch -t " <<-\EOF
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1812,10 +1855,12 @@ test_expect_success 'git switch - with --track, complete only remote branches' '
 
 test_expect_success 'git checkout - with --track, complete only remote branches' '
 	test_completion "git checkout --track " <<-\EOF &&
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
 	test_completion "git checkout -t " <<-\EOF
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1834,6 +1879,7 @@ test_expect_success 'git checkout - with --no-track, complete only local referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1845,6 +1891,7 @@ test_expect_success 'git switch - with -c, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1856,6 +1903,7 @@ test_expect_success 'git switch - with -C, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1867,6 +1915,7 @@ test_expect_success 'git switch - with -c and --track, complete all references'
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1878,6 +1927,7 @@ test_expect_success 'git switch - with -C and --track, complete all references'
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1889,6 +1939,7 @@ test_expect_success 'git switch - with -c and --no-track, complete all reference
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1900,6 +1951,7 @@ test_expect_success 'git switch - with -C and --no-track, complete all reference
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1911,6 +1963,7 @@ test_expect_success 'git checkout - with -b, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1922,6 +1975,7 @@ test_expect_success 'git checkout - with -B, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1933,6 +1987,7 @@ test_expect_success 'git checkout - with -b and --track, complete all references
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1944,6 +1999,7 @@ test_expect_success 'git checkout - with -B and --track, complete all references
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1955,6 +2011,7 @@ test_expect_success 'git checkout - with -b and --no-track, complete all referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1966,6 +2023,7 @@ test_expect_success 'git checkout - with -B and --no-track, complete all referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1973,6 +2031,7 @@ test_expect_success 'git checkout - with -B and --no-track, complete all referen
 
 test_expect_success 'git switch - for -c, complete local branches and unique remote branches' '
 	test_completion "git switch -c " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1982,6 +2041,7 @@ test_expect_success 'git switch - for -c, complete local branches and unique rem
 
 test_expect_success 'git switch - for -C, complete local branches and unique remote branches' '
 	test_completion "git switch -C " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2019,6 +2079,7 @@ test_expect_success 'git switch - for -C with --no-track, complete local branche
 
 test_expect_success 'git checkout - for -b, complete local branches and unique remote branches' '
 	test_completion "git checkout -b " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2028,6 +2089,7 @@ test_expect_success 'git checkout - for -b, complete local branches and unique r
 
 test_expect_success 'git checkout - for -B, complete local branches and unique remote branches' '
 	test_completion "git checkout -B " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2065,6 +2127,7 @@ test_expect_success 'git checkout - for -B with --no-track, complete local branc
 
 test_expect_success 'git switch - with --orphan completes local branch names and unique remote branch names' '
 	test_completion "git switch --orphan " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2080,6 +2143,7 @@ test_expect_success 'git switch - --orphan with branch already provided complete
 
 test_expect_success 'git checkout - with --orphan completes local branch names and unique remote branch names' '
 	test_completion "git checkout --orphan " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2093,6 +2157,7 @@ test_expect_success 'git checkout - --orphan with branch already provided comple
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
-- 
2.47.0.7.g072c39eddb.dirty
Bence Ferdinandy· Oct 14, 2024, 22:53 UTC · re: Bence Ferdinandy · lore

[PATCH v8 3/6] remote set-head: better output for --auto

Currently, set-head --auto will print a message saying "remote/HEAD set to branch", which implies something was changed.

Change the output of --auto, so the output actually reflects what was done: a) set a previously unset HEAD, b) change HEAD because remote changed or c) no updates. As a fourth output, if HEAD is changed from a previous value that was not a remote branch, explicitly call attention to this fact.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v1-v2:
    
        was RFC in https://lore.kernel.org/git/20240910203835.2288291-1-bence@ferdinandy.com/
    
    v3:
    
        This patch was originally sent along when I thought set-head was
        going to be invoked by fetch, but the discussion on the RFC
        concluded that it should be not. This opened the possibility to make
        it more explicit.
    
        Note: although I feel both things the patch does are really just
        cosmetic, an argument could be made for breaking it into two, one
        for the no-op part and one for the --auto print update.
    
        Was sent in:
        https://lore.kernel.org/git/20240915221055.904107-1-bence@ferdinandy.com/
    
    v4:
        - changes are now handled atomically via the ref update transaction
        - outputs have changed along the lines of Junio's suggestion
        - minor refactor to set_head for improved legibility
    
    v5: - the minor refactor has been split out into its own patch
    
    v6: - fixed uninitialized prev_head
        - fixed case of unusual previous target
        - fixed a test that would have been actually broken at this patch
          (the output was only correct with the later update to fetch)
        - added 4 tests for the 4 output cases
    
    v7: - change commit prefix to be more in line with project standards
        - fixed tests to also work with the reftable backend
        - renamed report function, fixed style issue with checking buf len
        - fixed not releasing an strbuf
    
    v8: no change
 builtin/remote.c  | 35 ++++++++++++++++++++++++++++----
 t/t5505-remote.sh | 51 ++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 81 insertions(+), 5 deletions(-)
Show changes to 2 files +81 −5

builtin/remote.c, t/t5505-remote.sh

diff --git a/builtin/remote.c b/builtin/remote.c
index 353ffd2c43..2b6948439f 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1399,10 +1399,35 @@ static int show(int argc, const char **argv, const char *prefix)
 	return result;
 }
 
+static void report_set_head_auto(const char *remote, const char *head_name,
+			struct strbuf *buf_prev) {
+	struct strbuf buf_prefix = STRBUF_INIT;
+	const char *prev_head = NULL;
+
+	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
+	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
+
+	if (prev_head && !strcmp(prev_head, head_name))
+		printf("'%s/HEAD' is unchanged and points to '%s'\n",
+			remote, head_name);
+	else if (prev_head)
+		printf("'%s/HEAD' has changed from '%s' and now points to '%s'\n",
+			remote, prev_head, head_name);
+	else if (!buf_prev->len)
+		printf("'%s/HEAD' is now created and points to '%s'\n",
+			remote, head_name);
+	else
+		printf("'%s/HEAD' used to point to '%s' "
+			"(which is not a remote branch), but now points to '%s'\n",
+			remote, buf_prev->buf, head_name);
+	strbuf_release(&buf_prefix);
+}
+
 static int set_head(int argc, const char **argv, const char *prefix)
 {
 	int i, opt_a = 0, opt_d = 0, result = 0;
-	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;
+	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT,
+		buf_prev = STRBUF_INIT;
 	char *head_name = NULL;
 	struct ref_store *refs = get_main_ref_store(the_repository);
 
@@ -1445,15 +1470,17 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		/* make sure it's valid */
 		if (!refs_ref_exists(refs, buf2.buf))
 			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(refs, buf.buf, buf2.buf, "remote set-head", NULL))
+		else if (refs_update_symref(refs, buf.buf, buf2.buf, "remote set-head", &buf_prev))
 			result |= error(_("Could not setup %s"), buf.buf);
-		else if (opt_a)
-			printf("%s/HEAD set to %s\n", argv[0], head_name);
+		else if (opt_a) {
+			report_set_head_auto(argv[0], head_name, &buf_prev);
+		}
 		free(head_name);
 	}
 
 	strbuf_release(&buf);
 	strbuf_release(&buf2);
+	strbuf_release(&buf_prev);
 	return result;
 }
 
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 532035933f..d99cd1d0aa 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -429,12 +429,51 @@ test_expect_success 'set-head --auto' '
 	)
 '
 
+test_expect_success 'set-head --auto detects creation' '
+	(
+		cd test &&
+		git symbolic-ref -d refs/remotes/origin/HEAD &&
+		git remote set-head --auto origin >output &&
+		echo "'\''origin/HEAD'\'' is now created and points to '\''main'\''" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects no change' '
+	(
+		cd test &&
+		git remote set-head --auto origin >output &&
+		echo "'\''origin/HEAD'\'' is unchanged and points to '\''main'\''" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects change' '
+	(
+		cd test &&
+		git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/ahead &&
+		git remote set-head --auto origin >output &&
+		echo "'\''origin/HEAD'\'' has changed from '\''ahead'\'' and now points to '\''main'\''" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects strange ref' '
+	(
+		cd test &&
+		git symbolic-ref refs/remotes/origin/HEAD refs/heads/main &&
+		git remote set-head --auto origin >output &&
+		echo "'\''origin/HEAD'\'' used to point to '\''refs/heads/main'\'' (which is not a remote branch), but now points to '\''main'\''" >expect &&
+		test_cmp expect output
+	)
+'
+
 test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
 	(
 		cd test &&
 		git fetch two "refs/heads/*:refs/remotes/two/*" &&
 		git remote set-head --auto two >output 2>&1 &&
-		echo "two/HEAD set to main" >expect &&
+		echo "'\''two/HEAD'\'' is now created and points to '\''main'\''" >expect &&
 		test_cmp expect output
 	)
 '
@@ -453,6 +492,16 @@ test_expect_success 'set-head explicit' '
 	)
 '
 
+test_expect_success 'set-head --auto reports change' '
+	(
+		cd test &&
+		git remote set-head origin side2 &&
+		git remote set-head --auto origin >output 2>&1 &&
+		echo "'\''origin/HEAD'\'' has changed from '\''side2'\'' and now points to '\''main'\''" >expect &&
+		test_cmp expect output
+	)
+'
+
 cat >test/expect <<EOF
 Pruning origin
 URL: $(pwd)/one
-- 
2.47.0.7.g072c39eddb.dirty
karthik nayak· Oct 15, 2024, 07:47 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v8 3/6] remote set-head: better output for --auto

Bence Ferdinandy <bence@ferdinandy.com> writes:
[snip]
Show 24 quoted lines
> +static void report_set_head_auto(const char *remote, const char *head_name,
> +			struct strbuf *buf_prev) {
> +	struct strbuf buf_prefix = STRBUF_INIT;
> +	const char *prev_head = NULL;
> +
> +	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
> +	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
> +
> +	if (prev_head && !strcmp(prev_head, head_name))
> +		printf("'%s/HEAD' is unchanged and points to '%s'\n",
> +			remote, head_name);
> +	else if (prev_head)
> +		printf("'%s/HEAD' has changed from '%s' and now points to '%s'\n",
> +			remote, prev_head, head_name);
> +	else if (!buf_prev->len)
> +		printf("'%s/HEAD' is now created and points to '%s'\n",
> +			remote, head_name);
> +	else
> +		printf("'%s/HEAD' used to point to '%s' "
> +			"(which is not a remote branch), but now points to '%s'\n",
> +			remote, buf_prev->buf, head_name);
> +	strbuf_release(&buf_prefix);
> +}
> +
Shouldn't the messages above be marked for translation too? using `_()`?
Show 6 quoted lines
>  static int set_head(int argc, const char **argv, const char *prefix)
>  {
>  	int i, opt_a = 0, opt_d = 0, result = 0;
> -	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;
> +	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT,
> +		buf_prev = STRBUF_INIT;
Nit: these names could definitely be better.
[snip]
Bence Ferdinandy· Oct 15, 2024, 16:31 UTC · re: karthik nayak · lore

Re: [PATCH v8 3/6] remote set-head: better output for --auto

On Tue Oct 15, 2024 at 09:47, karthik nayak <karthik.188@gmail.com> wrote:
Show 30 quoted lines
> Bence Ferdinandy <bence@ferdinandy.com> writes:
>
> [snip]
>
>> +static void report_set_head_auto(const char *remote, const char *head_name,
>> +			struct strbuf *buf_prev) {
>> +	struct strbuf buf_prefix = STRBUF_INIT;
>> +	const char *prev_head = NULL;
>> +
>> +	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
>> +	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
>> +
>> +	if (prev_head && !strcmp(prev_head, head_name))
>> +		printf("'%s/HEAD' is unchanged and points to '%s'\n",
>> +			remote, head_name);
>> +	else if (prev_head)
>> +		printf("'%s/HEAD' has changed from '%s' and now points to '%s'\n",
>> +			remote, prev_head, head_name);
>> +	else if (!buf_prev->len)
>> +		printf("'%s/HEAD' is now created and points to '%s'\n",
>> +			remote, head_name);
>> +	else
>> +		printf("'%s/HEAD' used to point to '%s' "
>> +			"(which is not a remote branch), but now points to '%s'\n",
>> +			remote, buf_prev->buf, head_name);
>> +	strbuf_release(&buf_prefix);
>> +}
>> +
>
> Shouldn't the messages above be marked for translation too? using `_()`?

I guess it is unlikely to fall under the "should be machine readable" category. I'll fix that up as well.

Show 9 quoted lines
>
>>  static int set_head(int argc, const char **argv, const char *prefix)
>>  {
>>  	int i, opt_a = 0, opt_d = 0, result = 0;
>> -	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;
>> +	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT,
>> +		buf_prev = STRBUF_INIT;
>
> Nit: these names could definitely be better.

Indeed ... The second patch is a refactor of remote set-head, I could include a variable rename refactor there as well, and use the same names I used in the last patch when adding the similar calls to fetch's set_head.

Taylor Blau· Oct 16, 2024, 00:26 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v8 0/6] set-head/fetch remote/HEAD updates, small change from v7

On Tue, Oct 15, 2024 at 12:53:09AM +0200, Bence Ferdinandy wrote:
Show 7 quoted lines
> Bence Ferdinandy (6):
>   refs: atomically record overwritten ref in update_symref
>   remote set-head: add new variable for readability
>   remote set-head: better output for --auto
>   refs: add TRANSACTION_CREATE_EXISTS error
>   refs: add create_only option to refs_update_symref
>   fetch: set remote/HEAD if it does not exist

I integrated this new round into my copy of 'seen' today and noticed some test breakage in t5505 here:

    https://github.com/ttaylorr/git/actions/runs/11356267070

I'm going to temporarily eject this topic out of 'seen' until we can get an analysis of what's going on here.

Thanks, Taylor

Bence Ferdinandy· Oct 16, 2024, 08:18 UTC · re: Taylor Blau · lore

Re: [PATCH v8 0/6] set-head/fetch remote/HEAD updates, small change from v7

On Wed Oct 16, 2024 at 02:26, Taylor Blau <me@ttaylorr.com> wrote:
Show 16 quoted lines
> On Tue, Oct 15, 2024 at 12:53:09AM +0200, Bence Ferdinandy wrote:
>> Bence Ferdinandy (6):
>>   refs: atomically record overwritten ref in update_symref
>>   remote set-head: add new variable for readability
>>   remote set-head: better output for --auto
>>   refs: add TRANSACTION_CREATE_EXISTS error
>>   refs: add create_only option to refs_update_symref
>>   fetch: set remote/HEAD if it does not exist
>
> I integrated this new round into my copy of 'seen' today and noticed
> some test breakage in t5505 here:
>
>     https://github.com/ttaylorr/git/actions/runs/11356267070
>
> I'm going to temporarily eject this topic out of 'seen' until we can get
> an analysis of what's going on here.

Thanks for the heads up! I see that indeed the failing output is different than what I have locally (e.g. no "apis/HEAD -> apis/main" in any of the test files I have). On the other hand I can't reproduce it so I will need some help with this I think.

I tested with both a rebase on master and a rebase on your seen branch. I'm using these to commands in t/ to run tests:

prove --timer --jobs 15 ./t[0-9]*.sh GIT_TEST_DEFAULT_REF_FORMAT=reftable prove --timer --jobs 15 ./t[0-9]*.sh

Everything passes. Should I be running this differently? I'm assuming the test do not use any global or local git config I may have?

I'm testing on Ubuntu 22.04 natively and in WSL, but the nature of the error doesn't seem like it could or should be related to the OS.

Thanks, Bence

Taylor Blau· Oct 16, 2024, 21:05 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v8 0/6] set-head/fetch remote/HEAD updates, small change from v7

On Wed, Oct 16, 2024 at 10:18:44AM +0200, Bence Ferdinandy wrote:
Show 23 quoted lines
>
> On Wed Oct 16, 2024 at 02:26, Taylor Blau <me@ttaylorr.com> wrote:
> > On Tue, Oct 15, 2024 at 12:53:09AM +0200, Bence Ferdinandy wrote:
> >> Bence Ferdinandy (6):
> >>   refs: atomically record overwritten ref in update_symref
> >>   remote set-head: add new variable for readability
> >>   remote set-head: better output for --auto
> >>   refs: add TRANSACTION_CREATE_EXISTS error
> >>   refs: add create_only option to refs_update_symref
> >>   fetch: set remote/HEAD if it does not exist
> >
> > I integrated this new round into my copy of 'seen' today and noticed
> > some test breakage in t5505 here:
> >
> >     https://github.com/ttaylorr/git/actions/runs/11356267070
> >
> > I'm going to temporarily eject this topic out of 'seen' until we can get
> > an analysis of what's going on here.
>
> Thanks for the heads up! I see that indeed the failing output is different than
> what I have locally (e.g. no "apis/HEAD -> apis/main" in any of the test files
> I have). On the other hand I can't reproduce it so I will need some help with
> this I think.

I similarly could not reproduce it (I ran 'make test' on this topic before integrating it into 'seen', and it passed, otherwise I wouldn't have picked it up).

I am not sure what the differences are. The 'ci' directory has some more bits on how the various suites are run, and the '.github/workflows' directory has some more bits still.

Thanks, Taylor

Bence Ferdinandy· Oct 17, 2024, 15:23 UTC · re: Taylor Blau · lore

Re: [PATCH v8 0/6] set-head/fetch remote/HEAD updates, small change from v7

On Wed Oct 16, 2024 at 23:05, Taylor Blau <me@ttaylorr.com> wrote:
Show 13 quoted lines
> On Wed, Oct 16, 2024 at 10:18:44AM +0200, Bence Ferdinandy wrote:
>> Thanks for the heads up! I see that indeed the failing output is different than
>> what I have locally (e.g. no "apis/HEAD -> apis/main" in any of the test files
>> I have). On the other hand I can't reproduce it so I will need some help with
>> this I think.
>
> I similarly could not reproduce it (I ran 'make test' on this topic
> before integrating it into 'seen', and it passed, otherwise I wouldn't
> have picked it up).
>
> I am not sure what the differences are. The 'ci' directory has some more
> bits on how the various suites are run, and the '.github/workflows'
> directory has some more bits still.

I managed to reproduce it in docker once, but I'm not sure how and I wasn't able to do it again ... On the other hand (using some printf) I managed to figure out that `fetch --multiple --all` is running both in the CI and locally and the difference is actually what we see in the output (so no error per se). E.g. in t5505.55 update, if you run it locally refs/remotes/apis/HEAD does not exist, while in the CI it does.

I'm not sure why this difference exists yet, so I'm hoping someone might have an idea. I'll keep poking around in the meanwhile.

Thanks, Bence

Bence Ferdinandy· Oct 19, 2024, 22:53 UTC · re: Taylor Blau · lore

[PATCH v9 0/7] set-head/fetch remote/HEAD updates

Hi,
the two most notable changes are
- the new patch 1/7 which address
  https://lore.kernel.org/git/Zw8IKyPkG0Hr6%2F5t@nand.local/, but see
  https://lore.kernel.org/git/D4ZAELFWJMKN.S88LJ6YK31LZ@ferdinandy.com/
- the refs_update_symref -> refs_update_symref_extended change in 2/7,
  reflecting on Phillip's comments (see
  https://lore.kernel.org/git/a7cb48e5-d8ba-44c1-9dbe-d1e8f8a63e3c@gmail.com/)
Hopefully with 1/7 the series is ready to move back to seen :)

Best, Bence

Bence Ferdinandy (7):
  t/t5505-remote: set default branch to main
  refs: atomically record overwritten ref in update_symref
  remote set-head: refactor for readability
  remote set-head: better output for --auto
  refs: add TRANSACTION_CREATE_EXISTS error
  refs: add create_only option to refs_update_symref_extended
  fetch: set remote/HEAD if it does not exist
 builtin/fetch.c                  |  82 +++++++++++
 builtin/remote.c                 |  52 +++++--
 refs.c                           |  41 +++++-
 refs.h                           |   8 +-
 refs/files-backend.c             |  24 ++--
 refs/reftable-backend.c          |   6 +-
 t/t4207-log-decoration-colors.sh |   3 +-
 t/t5505-remote.sh                |  73 +++++++++-
 t/t5510-fetch.sh                 | 229 ++++++++++++++++---------------
 t/t5512-ls-remote.sh             |   2 +
 t/t5514-fetch-multiple.sh        |  17 ++-
 t/t5516-fetch-push.sh            |   3 +-
 t/t5527-fetch-odd-refs.sh        |   3 +-
 t/t7900-maintenance.sh           |   3 +-
 t/t9210-scalar.sh                |   5 +-
 t/t9211-scalar-clone.sh          |   6 +-
 t/t9902-completion.sh            |  65 +++++++++
 17 files changed, 468 insertions(+), 154 deletions(-)
-- 
2.47.0.92.g83fdbe24c3.dirty
Bence Ferdinandy· Oct 19, 2024, 22:53 UTC · re: Bence Ferdinandy · lore

[PATCH v9 1/7] t/t5505-remote: set default branch to main

Consider the bare repository called "mirror" in the test. Running `git remote add --mirror -f origin ../one` will not change HEAD, consequently if init.defaultBranch is not the same as what HEAD in the remote ("one"), HEAD in "mirror" will be pointing to a non-existent reference. Hence if "mirror" is used as a remote by yet another repository, ls-remote will not show HEAD. On the other hand, if init.defaultBranch happens to match HEAD in "one", then ls-remote will show HEAD.

Since the CI globally exports GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main, there's a drift between how the test repositories are set up in the CI and during local testing. This issue does not manifest currently, as the test does not do any remote HEAD manipulation where this would come up, but should such things be added, a locally passing test would break the CI vice-versa.

Set GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main in the test to be consistent with the CI. ---

Notes:
    v9: - new patch
        - a bandaid for the CI issue noticed by Taylor (cf:
          https://lore.kernel.org/git/Zw8IKyPkG0Hr6%2F5t@nand.local/), but
          see
          https://lore.kernel.org/git/D4ZAELFWJMKN.S88LJ6YK31LZ@ferdinandy.com/ for the root cause in detail
 t/t5505-remote.sh | 3 +++
 1 file changed, 3 insertions(+)
Show changes to t/t5505-remote.sh +3 −0
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 532035933f..9b50276646 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -2,6 +2,9 @@
 
 test_description='git remote porcelain-ish'
 
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
 TEST_PASSES_SANITIZE_LEAK=true
 . ./test-lib.sh
 
-- 
2.47.0.92.g83fdbe24c3.dirty
Bence Ferdinandy· Oct 19, 2024, 22:53 UTC · re: Bence Ferdinandy · lore

[PATCH v9 2/7] refs: atomically record overwritten ref in update_symref

When updating a symref with update_symref it's currently not possible to know for sure what was the previous value that was overwritten. Extend refs_update_symref under a new function name, to record the value after the ref has been locked if the caller of refs_update_symref_extended requests it via a new variable in the function call. Keep the original refs_update_symref function with the same signature, but now as a wrapper around refs_update_symref_extended.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    
    v5: - added before_target to reftables backend
        - added an extra safety check for transaction's existence in refs.c
    
    v6: - no change
    
    v7: - remove the whole before_target concept from the backends and
          handle checking it in refs.c instead (thanks Karthik)
        - rename the before_target to referent which is how the same concept
          is called in the backends
        - change commit prefix to be more in line with project standards
    
    v8: no change
    
    v9: - instead of adding parameters to refs_update_symref, rename what
          was in v8 as refs_update_symref_extended and make refs_update_symref
          a wrapper for that. This significantly reduces the number of files
          that need to be touched, and avoids adding a lot of dummy NULL-s
          in unrelated places.
 refs.c | 19 ++++++++++++++++---
 refs.h |  4 ++++
 2 files changed, 20 insertions(+), 3 deletions(-)
Show changes to 2 files +20 −3

refs.c, refs.h

diff --git a/refs.c b/refs.c
index 5f729ed412..24a4172cd2 100644
--- a/refs.c
+++ b/refs.c
@@ -2115,6 +2115,13 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
 
 int refs_update_symref(struct ref_store *refs, const char *ref,
 		       const char *target, const char *logmsg)
+{
+	return refs_update_symref_extended(refs, ref, target, logmsg, NULL);
+}
+
+int refs_update_symref_extended(struct ref_store *refs, const char *ref,
+		       const char *target, const char *logmsg,
+		       struct strbuf *referent)
 {
 	struct ref_transaction *transaction;
 	struct strbuf err = STRBUF_INIT;
@@ -2122,13 +2129,20 @@ int refs_update_symref(struct ref_store *refs, const char *ref,
 
 	transaction = ref_store_transaction_begin(refs, &err);
 	if (!transaction ||
-	    ref_transaction_update(transaction, ref, NULL, NULL,
+		ref_transaction_update(transaction, ref, NULL, NULL,
 				   target, NULL, REF_NO_DEREF,
 				   logmsg, &err) ||
-	    ref_transaction_commit(transaction, &err)) {
+		ref_transaction_prepare(transaction, &err)) {
 		ret = error("%s", err.buf);
+		goto cleanup;
 	}
+	if (referent)
+		refs_read_symbolic_ref(refs, ref, referent);
 
+	if (ref_transaction_commit(transaction, &err))
+		ret = error("%s", err.buf);
+
+cleanup:
 	strbuf_release(&err);
 	if (transaction)
 		ref_transaction_free(transaction);
@@ -2948,4 +2962,3 @@ int ref_update_expects_existing_old_ref(struct ref_update *update)
 	return (update->flags & REF_HAVE_OLD) &&
 		(!is_null_oid(&update->old_oid) || update->old_target);
 }
-
diff --git a/refs.h b/refs.h
index 108dfc93b3..259191a485 100644
--- a/refs.h
+++ b/refs.h
@@ -573,6 +573,10 @@ int refs_copy_existing_ref(struct ref_store *refs, const char *oldref,
 int refs_update_symref(struct ref_store *refs, const char *refname,
 		       const char *target, const char *logmsg);
 
+int refs_update_symref_extended(struct ref_store *refs, const char *refname,
+		       const char *target, const char *logmsg,
+		       struct strbuf *referent);
+
 enum action_on_err {
 	UPDATE_REFS_MSG_ON_ERR,
 	UPDATE_REFS_DIE_ON_ERR,
-- 
2.47.0.92.g83fdbe24c3.dirty
Bence Ferdinandy· Oct 19, 2024, 22:53 UTC · re: Bence Ferdinandy · lore

[PATCH v9 3/7] remote set-head: refactor for readability

Rename buf and buf2 to something more explanatory.

Instead of calling get_main_ref_store(the_repository) multiple times, call it once and store in a new refs variable. Although this change probably offers some performance benefits, the main purpose is to shorten the line lengths of function calls using this variable.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v5: new patch (split from the next patch as a preparatory step)
    
    v6: no change
    
    v7: - change commit prefix to be more in line with project standards
    
    v8: no change
    
    v9: - further improve readability by renaming buf, and buf2 consistently
          with how patch 6 was already done
 builtin/remote.c | 23 ++++++++++++-----------
 1 file changed, 12 insertions(+), 11 deletions(-)
Show changes to builtin/remote.c +12 −11
diff --git a/builtin/remote.c b/builtin/remote.c
index 76670ddd8b..1d68c5b2ba 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1402,8 +1402,9 @@ static int show(int argc, const char **argv, const char *prefix)
 static int set_head(int argc, const char **argv, const char *prefix)
 {
 	int i, opt_a = 0, opt_d = 0, result = 0;
-	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT;
 	char *head_name = NULL;
+	struct ref_store *refs = get_main_ref_store(the_repository);
 
 	struct option options[] = {
 		OPT_BOOL('a', "auto", &opt_a,
@@ -1415,7 +1416,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 	argc = parse_options(argc, argv, prefix, options,
 			     builtin_remote_sethead_usage, 0);
 	if (argc)
-		strbuf_addf(&buf, "refs/remotes/%s/HEAD", argv[0]);
+		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", argv[0]);
 
 	if (!opt_a && !opt_d && argc == 2) {
 		head_name = xstrdup(argv[1]);
@@ -1434,25 +1435,25 @@ static int set_head(int argc, const char **argv, const char *prefix)
 			head_name = xstrdup(states.heads.items[0].string);
 		free_remote_ref_states(&states);
 	} else if (opt_d && !opt_a && argc == 1) {
-		if (refs_delete_ref(get_main_ref_store(the_repository), NULL, buf.buf, NULL, REF_NO_DEREF))
-			result |= error(_("Could not delete %s"), buf.buf);
+		if (refs_delete_ref(refs, NULL, b_head.buf, NULL, REF_NO_DEREF))
+			result |= error(_("Could not delete %s"), b_head.buf);
 	} else
 		usage_with_options(builtin_remote_sethead_usage, options);
 
 	if (head_name) {
-		strbuf_addf(&buf2, "refs/remotes/%s/%s", argv[0], head_name);
+		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", argv[0], head_name);
 		/* make sure it's valid */
-		if (!refs_ref_exists(get_main_ref_store(the_repository), buf2.buf))
-			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head"))
-			result |= error(_("Could not setup %s"), buf.buf);
+		if (!refs_ref_exists(refs, b_remote_head.buf))
+			result |= error(_("Not a valid ref: %s"), b_remote_head.buf);
+		else if (refs_update_symref(refs, b_head.buf, b_remote_head.buf, "remote set-head"))
+			result |= error(_("Could not setup %s"), b_head.buf);
 		else if (opt_a)
 			printf("%s/HEAD set to %s\n", argv[0], head_name);
 		free(head_name);
 	}
 
-	strbuf_release(&buf);
-	strbuf_release(&buf2);
+	strbuf_release(&b_head);
+	strbuf_release(&b_remote_head);
 	return result;
 }
 
-- 
2.47.0.92.g83fdbe24c3.dirty
Bence Ferdinandy· Oct 19, 2024, 22:53 UTC · re: Bence Ferdinandy · lore

[PATCH v9 5/7] refs: add TRANSACTION_CREATE_EXISTS error

Currently there is only one special error for transaction, for when there is a naming conflict, all other errors are dumped under a generic error. Add a new special error case for when the caller requests the reference to be updated only when it does not yet exist and the reference actually does exist.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    v5: no change
    v6: no change
    
    v7: - change commit prefix to be more in line with project standards
        - changed error checking to Karthik's suggestion
    
    v8: no change
    
    v9: - no change
 refs.h                  |  4 +++-
 refs/files-backend.c    | 24 ++++++++++++++++--------
 refs/reftable-backend.c |  6 ++++--
 3 files changed, 23 insertions(+), 11 deletions(-)
Show changes to 3 files +23 −11

refs.h, refs/files-backend.c, refs/reftable-backend.c

diff --git a/refs.h b/refs.h
index 259191a485..a5bc25442b 100644
--- a/refs.h
+++ b/refs.h
@@ -762,8 +762,10 @@ int ref_transaction_verify(struct ref_transaction *transaction,
 
 /* Naming conflict (for example, the ref names A and A/B conflict). */
 #define TRANSACTION_NAME_CONFLICT -1
+/* When only creation was requested, but the ref already exists. */
+#define TRANSACTION_CREATE_EXISTS -2
 /* All other errors. */
-#define TRANSACTION_GENERIC_ERROR -2
+#define TRANSACTION_GENERIC_ERROR -3
 
 /*
  * Perform the preparatory stages of committing `transaction`. Acquire
diff --git a/refs/files-backend.c b/refs/files-backend.c
index 0824c0b8a9..e743ec44b5 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -2502,14 +2502,18 @@ static int split_symref_update(struct ref_update *update,
 static int check_old_oid(struct ref_update *update, struct object_id *oid,
 			 struct strbuf *err)
 {
+	int ret = TRANSACTION_GENERIC_ERROR;
+
 	if (!(update->flags & REF_HAVE_OLD) ||
 		   oideq(oid, &update->old_oid))
 		return 0;
 
-	if (is_null_oid(&update->old_oid))
+	if (is_null_oid(&update->old_oid)) {
 		strbuf_addf(err, "cannot lock ref '%s': "
 			    "reference already exists",
 			    ref_update_original_update_refname(update));
+		ret = TRANSACTION_CREATE_EXISTS;
+	}
 	else if (is_null_oid(oid))
 		strbuf_addf(err, "cannot lock ref '%s': "
 			    "reference is missing but expected %s",
@@ -2522,7 +2526,7 @@ static int check_old_oid(struct ref_update *update, struct object_id *oid,
 			    oid_to_hex(oid),
 			    oid_to_hex(&update->old_oid));
 
-	return -1;
+	return ret;
 }
 
 /*
@@ -2602,9 +2606,11 @@ static int lock_ref_for_update(struct files_ref_store *refs,
 					ret = TRANSACTION_GENERIC_ERROR;
 					goto out;
 				}
-			} else if  (check_old_oid(update, &lock->old_oid, err)) {
-				ret = TRANSACTION_GENERIC_ERROR;
-				goto out;
+			} else {
+				ret = check_old_oid(update, &lock->old_oid, err);
+				if  (ret) {
+					goto out;
+				}
 			}
 		} else {
 			/*
@@ -2635,9 +2641,11 @@ static int lock_ref_for_update(struct files_ref_store *refs,
 				    update->old_target);
 			ret = TRANSACTION_GENERIC_ERROR;
 			goto out;
-		} else if  (check_old_oid(update, &lock->old_oid, err)) {
-			ret = TRANSACTION_GENERIC_ERROR;
-			goto out;
+		} else {
+			ret = check_old_oid(update, &lock->old_oid, err);
+			if  (ret) {
+				goto out;
+			}
 		}
 
 		/*
diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
index 3c6107c7ce..b3b5ce77dd 100644
--- a/refs/reftable-backend.c
+++ b/refs/reftable-backend.c
@@ -1206,10 +1206,13 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 				goto done;
 			}
 		} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {
-			if (is_null_oid(&u->old_oid))
+			ret = TRANSACTION_NAME_CONFLICT;
+			if (is_null_oid(&u->old_oid)) {
 				strbuf_addf(err, _("cannot lock ref '%s': "
 						   "reference already exists"),
 					    ref_update_original_update_refname(u));
+				ret = TRANSACTION_CREATE_EXISTS;
+			}
 			else if (is_null_oid(&current_oid))
 				strbuf_addf(err, _("cannot lock ref '%s': "
 						   "reference is missing but expected %s"),
@@ -1221,7 +1224,6 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 					    ref_update_original_update_refname(u),
 					    oid_to_hex(&current_oid),
 					    oid_to_hex(&u->old_oid));
-			ret = -1;
 			goto done;
 		}
 
-- 
2.47.0.92.g83fdbe24c3.dirty
Bence Ferdinandy· Oct 19, 2024, 22:53 UTC · re: Bence Ferdinandy · lore

[PATCH v9 4/7] remote set-head: better output for --auto

Currently, set-head --auto will print a message saying "remote/HEAD set to branch", which implies something was changed.

Change the output of --auto, so the output actually reflects what was done: a) set a previously unset HEAD, b) change HEAD because remote changed or c) no updates. As a fourth output, if HEAD is changed from a previous value that was not a remote branch, explicitly call attention to this fact.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v1-v2:
    
        was RFC in https://lore.kernel.org/git/20240910203835.2288291-1-bence@ferdinandy.com/
    
    v3:
    
        This patch was originally sent along when I thought set-head was
        going to be invoked by fetch, but the discussion on the RFC
        concluded that it should be not. This opened the possibility to make
        it more explicit.
    
        Note: although I feel both things the patch does are really just
        cosmetic, an argument could be made for breaking it into two, one
        for the no-op part and one for the --auto print update.
    
        Was sent in:
        https://lore.kernel.org/git/20240915221055.904107-1-bence@ferdinandy.com/
    
    v4:
        - changes are now handled atomically via the ref update transaction
        - outputs have changed along the lines of Junio's suggestion
        - minor refactor to set_head for improved legibility
    
    v5: - the minor refactor has been split out into its own patch
    
    v6: - fixed uninitialized prev_head
        - fixed case of unusual previous target
        - fixed a test that would have been actually broken at this patch
          (the output was only correct with the later update to fetch)
        - added 4 tests for the 4 output cases
    
    v7: - change commit prefix to be more in line with project standards
        - fixed tests to also work with the reftable backend
        - renamed report function, fixed style issue with checking buf len
        - fixed not releasing an strbuf
    
    v8: no change
    
    v9: - mark output strings in report_set_head_auto as translatable
        - rename buf_prev to b_local_head for consistency
        - use ${SQ} in tests instead of '\''
 builtin/remote.c  | 33 +++++++++++++++++++++++++++---
 t/t5505-remote.sh | 51 ++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 80 insertions(+), 4 deletions(-)
Show changes to 2 files +80 −4

builtin/remote.c, t/t5505-remote.sh

diff --git a/builtin/remote.c b/builtin/remote.c
index 1d68c5b2ba..108f1271d3 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1399,10 +1399,35 @@ static int show(int argc, const char **argv, const char *prefix)
 	return result;
 }
 
+static void report_set_head_auto(const char *remote, const char *head_name,
+			struct strbuf *b_local_head) {
+	struct strbuf buf_prefix = STRBUF_INIT;
+	const char *prev_head = NULL;
+
+	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
+	skip_prefix(b_local_head->buf, buf_prefix.buf, &prev_head);
+
+	if (prev_head && !strcmp(prev_head, head_name))
+		printf(_("'%s/HEAD' is unchanged and points to '%s'\n"),
+			remote, head_name);
+	else if (prev_head)
+		printf(_("'%s/HEAD' has changed from '%s' and now points to '%s'\n"),
+			remote, prev_head, head_name);
+	else if (!b_local_head->len)
+		printf(_("'%s/HEAD' is now created and points to '%s'\n"),
+			remote, head_name);
+	else
+		printf(_("'%s/HEAD' used to point to '%s' "
+			"(which is not a remote branch), but now points to '%s'\n"),
+			remote, b_local_head->buf, head_name);
+	strbuf_release(&buf_prefix);
+}
+
 static int set_head(int argc, const char **argv, const char *prefix)
 {
 	int i, opt_a = 0, opt_d = 0, result = 0;
-	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
+		b_local_head = STRBUF_INIT;
 	char *head_name = NULL;
 	struct ref_store *refs = get_main_ref_store(the_repository);
 
@@ -1445,15 +1470,17 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		/* make sure it's valid */
 		if (!refs_ref_exists(refs, b_remote_head.buf))
 			result |= error(_("Not a valid ref: %s"), b_remote_head.buf);
-		else if (refs_update_symref(refs, b_head.buf, b_remote_head.buf, "remote set-head"))
+		else if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
+					"remote set-head", &b_local_head))
 			result |= error(_("Could not setup %s"), b_head.buf);
 		else if (opt_a)
-			printf("%s/HEAD set to %s\n", argv[0], head_name);
+			report_set_head_auto(argv[0], head_name, &b_local_head);
 		free(head_name);
 	}
 
 	strbuf_release(&b_head);
 	strbuf_release(&b_remote_head);
+	strbuf_release(&b_local_head);
 	return result;
 }
 
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 9b50276646..0ea86d51a4 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -432,12 +432,51 @@ test_expect_success 'set-head --auto' '
 	)
 '
 
+test_expect_success 'set-head --auto detects creation' '
+	(
+		cd test &&
+		git symbolic-ref -d refs/remotes/origin/HEAD &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} is now created and points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects no change' '
+	(
+		cd test &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} is unchanged and points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects change' '
+	(
+		cd test &&
+		git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/ahead &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} has changed from ${SQ}ahead${SQ} and now points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects strange ref' '
+	(
+		cd test &&
+		git symbolic-ref refs/remotes/origin/HEAD refs/heads/main &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} used to point to ${SQ}refs/heads/main${SQ} (which is not a remote branch), but now points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
 test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
 	(
 		cd test &&
 		git fetch two "refs/heads/*:refs/remotes/two/*" &&
 		git remote set-head --auto two >output 2>&1 &&
-		echo "two/HEAD set to main" >expect &&
+		echo "${SQ}two/HEAD${SQ} is now created and points to ${SQ}main${SQ}" >expect &&
 		test_cmp expect output
 	)
 '
@@ -456,6 +495,16 @@ test_expect_success 'set-head explicit' '
 	)
 '
 
+test_expect_success 'set-head --auto reports change' '
+	(
+		cd test &&
+		git remote set-head origin side2 &&
+		git remote set-head --auto origin >output 2>&1 &&
+		echo "${SQ}origin/HEAD${SQ} has changed from ${SQ}side2${SQ} and now points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
 cat >test/expect <<EOF
 Pruning origin
 URL: $(pwd)/one
-- 
2.47.0.92.g83fdbe24c3.dirty
Bence Ferdinandy· Oct 19, 2024, 22:53 UTC · re: Bence Ferdinandy · lore

[PATCH v9 6/7] refs: add create_only option to refs_update_symref_extended

Allow the caller to specify that it only wants to update the symref if it does not already exist. Silently ignore the error from the transaction API if the symref already exists.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    v5: no change
    
    v6: - switched from bool to int for create_only
        - refactored logic in refs_update_symref with goto as layed out by
          Junio
    
    v7: - change commit prefix to be more in line with project standards
        - refactored code to accommodate changes in the first patch, but
          otherwise no change
    
    v8: no change
    
    v9: - no change (except for following through the
          refs_update_symref_extended refactoring)
 builtin/remote.c |  2 +-
 refs.c           | 32 +++++++++++++++++++++++---------
 refs.h           |  2 +-
 3 files changed, 25 insertions(+), 11 deletions(-)
Show changes to 3 files +25 −11

builtin/remote.c, refs.c, refs.h

diff --git a/builtin/remote.c b/builtin/remote.c
index 108f1271d3..b1eba75a2b 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1471,7 +1471,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		if (!refs_ref_exists(refs, b_remote_head.buf))
 			result |= error(_("Not a valid ref: %s"), b_remote_head.buf);
 		else if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
-					"remote set-head", &b_local_head))
+					"remote set-head", &b_local_head, 0))
 			result |= error(_("Could not setup %s"), b_head.buf);
 		else if (opt_a)
 			report_set_head_auto(argv[0], head_name, &b_local_head);
diff --git a/refs.c b/refs.c
index 24a4172cd2..093ee11ab0 100644
--- a/refs.c
+++ b/refs.c
@@ -2116,31 +2116,45 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
 int refs_update_symref(struct ref_store *refs, const char *ref,
 		       const char *target, const char *logmsg)
 {
-	return refs_update_symref_extended(refs, ref, target, logmsg, NULL);
+	return refs_update_symref_extended(refs, ref, target, logmsg, NULL, 0);
 }
 
 int refs_update_symref_extended(struct ref_store *refs, const char *ref,
 		       const char *target, const char *logmsg,
-		       struct strbuf *referent)
+		       struct strbuf *referent, int create_only)
 {
 	struct ref_transaction *transaction;
 	struct strbuf err = STRBUF_INIT;
-	int ret = 0;
+	int ret = 0, prepret = 0;
 
 	transaction = ref_store_transaction_begin(refs, &err);
-	if (!transaction ||
-		ref_transaction_update(transaction, ref, NULL, NULL,
-				   target, NULL, REF_NO_DEREF,
-				   logmsg, &err) ||
-		ref_transaction_prepare(transaction, &err)) {
+	if (!transaction) {
+	error_return:
 		ret = error("%s", err.buf);
 		goto cleanup;
 	}
+	if (create_only) {
+		if (ref_transaction_create(transaction, ref, NULL, target,
+					   REF_NO_DEREF, logmsg, &err))
+			goto error_return;
+		prepret = ref_transaction_prepare(transaction, &err);
+		if (prepret && prepret != TRANSACTION_CREATE_EXISTS)
+			goto error_return;
+	} else {
+		if (ref_transaction_update(transaction, ref, NULL, NULL,
+					   target, NULL, REF_NO_DEREF,
+					   logmsg, &err) ||
+			ref_transaction_prepare(transaction, &err))
+			goto error_return;
+	}
+
 	if (referent)
 		refs_read_symbolic_ref(refs, ref, referent);
 
+	if (prepret == TRANSACTION_CREATE_EXISTS)
+		goto cleanup;
 	if (ref_transaction_commit(transaction, &err))
-		ret = error("%s", err.buf);
+		goto error_return;
 
 cleanup:
 	strbuf_release(&err);
diff --git a/refs.h b/refs.h
index a5bc25442b..458582ebcf 100644
--- a/refs.h
+++ b/refs.h
@@ -575,7 +575,7 @@ int refs_update_symref(struct ref_store *refs, const char *refname,
 
 int refs_update_symref_extended(struct ref_store *refs, const char *refname,
 		       const char *target, const char *logmsg,
-		       struct strbuf *referent);
+		       struct strbuf *referent, int create_only);
 
 enum action_on_err {
 	UPDATE_REFS_MSG_ON_ERR,
-- 
2.47.0.92.g83fdbe24c3.dirty
Bence Ferdinandy· Oct 19, 2024, 22:53 UTC · re: Bence Ferdinandy · lore

[PATCH v9 7/7] fetch: set remote/HEAD if it does not exist

If the user has remote/HEAD set already and it looks like it has changed on the server, then print a message, otherwise set it if we can. Silently pass if the user already has the same remote/HEAD set as reported by the server or if we encounter any errors along the way.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v3: - does not rely on remote set-head anymore so it only authenticates
        once
        - uses the new REF_CREATE_ONLY to atomically check if the ref exists
          and only write it if it doesn't
        - in all other cases the maximum it does is print a warning
    
    v4: - instead of the discarded REF_CREATE_ONLY, it uses the existing,
          but updated transaction api to request a silent create only
        - it now uses the atomic before_target to determine reporting
        - refactored for legibility
    
    v5: - instead of printing a not too useful message, it now fails
          silently, this in line with the objective to only set up
          remote/HEAD automatically if the right thing is trivial, for
          everything else there is remote set-head
        - fixed all failing tests
        - added two new tests, one for checking if remote/HEAD is set to the
          correct one, and one to test that we do not override remote/HEAD
          if it has changed on the server from what we have locally
    
    v6: - fixed style issues and unintended extra empty line
        - updated function call with bool to int from previous patch's
          change
        - removed calls to error(...) inherited from builtin/remote.c so we
          actually fail silently
        - set the test for remote set-head --auto to the correct value here,
          which was previously erronously set in the remote set-head patch
    
    v7: - no change
    
    v8: - changed logmsg in call to refs_update_symref from "remote
          set-head" to "fetch"
    
    v9: - follow through with refs_update_symref_extended
        - fix test errors uncovered by the new patch
 builtin/fetch.c                  |  82 +++++++++++
 t/t4207-log-decoration-colors.sh |   3 +-
 t/t5505-remote.sh                |  21 ++-
 t/t5510-fetch.sh                 | 229 ++++++++++++++++---------------
 t/t5512-ls-remote.sh             |   2 +
 t/t5514-fetch-multiple.sh        |  17 ++-
 t/t5516-fetch-push.sh            |   3 +-
 t/t5527-fetch-odd-refs.sh        |   3 +-
 t/t7900-maintenance.sh           |   3 +-
 t/t9210-scalar.sh                |   5 +-
 t/t9211-scalar-clone.sh          |   6 +-
 t/t9902-completion.sh            |  65 +++++++++
 12 files changed, 315 insertions(+), 124 deletions(-)
Show changes to 12 files +315 −124

builtin/fetch.c, t/t4207-log-decoration-colors.sh, t/t5505-remote.sh, t/t5510-fetch.sh, t/t5512-ls-remote.sh, t/t5514-fetch-multiple.sh, t/t5516-fetch-push.sh, t/t5527-fetch-odd-refs.sh, t/t7900-maintenance.sh, t/t9210-scalar.sh, t/t9211-scalar-clone.sh, t/t9902-completion.sh

diff --git a/builtin/fetch.c b/builtin/fetch.c
index d9027e4dc9..31edc67447 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1578,6 +1578,80 @@ static int backfill_tags(struct display_state *display_state,
 	return retcode;
 }
 
+static void report_set_head(const char *remote, const char *head_name,
+			struct strbuf *buf_prev) {
+	struct strbuf buf_prefix = STRBUF_INIT;
+	const char *prev_head = NULL;
+
+	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
+	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
+
+	if (prev_head && strcmp(prev_head, head_name)) {
+		printf("'HEAD' at '%s' has changed from '%s' to '%s'\n",
+			remote, prev_head, head_name);
+		printf("Run 'git remote set-head %s %s' to follow the change.\n",
+			remote, head_name);
+	}
+}
+
+static const char *strip_refshead(const char *name){
+	skip_prefix(name, "refs/heads/", &name);
+	return name;
+}
+
+static int set_head(const struct ref *remote_refs)
+{
+	int result = 0;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
+		b_local_head = STRBUF_INIT;
+	const char *remote = gtransport->remote->name;
+	char *head_name = NULL;
+	struct ref *ref, *matches;
+	struct ref *fetch_map = NULL, **fetch_map_tail = &fetch_map;
+	struct refspec_item refspec = {
+		.force = 0,
+		.pattern = 1,
+		.src = (char *) "refs/heads/*",
+		.dst = (char *) "refs/heads/*",
+	};
+	struct string_list heads = STRING_LIST_INIT_DUP;
+	struct ref_store *refs = get_main_ref_store(the_repository);
+
+	get_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);
+	matches = guess_remote_head(find_ref_by_name(remote_refs, "HEAD"),
+				    fetch_map, 1);
+	for (ref = matches; ref; ref = ref->next) {
+		string_list_append(&heads, strip_refshead(ref->name));
+	}
+
+
+	if (!heads.nr)
+		result = 1;
+	else if (heads.nr > 1)
+		result = 1;
+	else
+		head_name = xstrdup(heads.items[0].string);
+	if (head_name) {
+		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
+		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote, head_name);
+		/* make sure it's valid */
+		if (!refs_ref_exists(refs, b_remote_head.buf))
+			result = 1;
+		else if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
+					"fetch", &b_local_head, 1))
+			result = 1;
+		else
+			report_set_head(remote, head_name, &b_local_head);
+
+		free(head_name);
+	}
+
+	strbuf_release(&b_head);
+	strbuf_release(&b_local_head);
+	strbuf_release(&b_remote_head);
+	return result;
+}
+
 static int do_fetch(struct transport *transport,
 		    struct refspec *rs,
 		    const struct fetch_config *config)
@@ -1647,6 +1721,8 @@ static int do_fetch(struct transport *transport,
 				    "refs/tags/");
 	}
 
+	strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
+
 	if (must_list_refs) {
 		trace2_region_enter("fetch", "remote_refs", the_repository);
 		remote_refs = transport_get_remote_refs(transport,
@@ -1791,6 +1867,12 @@ static int do_fetch(struct transport *transport,
 				  "you need to specify exactly one branch with the --set-upstream option"));
 		}
 	}
+	if (set_head(remote_refs))
+		;
+		/*
+		 * Way too many cases where this can go wrong
+		 * so let's just fail silently for now.
+		 */
 
 cleanup:
 	if (retcode) {
diff --git a/t/t4207-log-decoration-colors.sh b/t/t4207-log-decoration-colors.sh
index 73ea9e5155..d55d22cb2f 100755
--- a/t/t4207-log-decoration-colors.sh
+++ b/t/t4207-log-decoration-colors.sh
@@ -59,7 +59,8 @@ ${c_reset}${c_tag}tag: ${c_reset}${c_tag}v1.0${c_reset}${c_commit}, \
 ${c_reset}${c_tag}tag: ${c_reset}${c_tag}B${c_reset}${c_commit})${c_reset} B
 ${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
 ${c_tag}tag: ${c_reset}${c_tag}A1${c_reset}${c_commit}, \
-${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit})${c_reset} A1
+${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit}, \
+${c_reset}${c_remoteBranch}other/HEAD${c_reset}${c_commit})${c_reset} A1
 	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
 ${c_stash}refs/stash${c_reset}${c_commit})${c_reset} On main: Changes to A.t
 	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 0ea86d51a4..4990d00209 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -74,7 +74,7 @@ test_expect_success 'add another remote' '
 		cd test &&
 		git remote add -f second ../two &&
 		tokens_match "origin second" "$(git remote)" &&
-		check_tracking_branch second main side another &&
+		check_tracking_branch second main side another HEAD &&
 		git for-each-ref "--format=%(refname)" refs/remotes |
 		sed -e "/^refs\/remotes\/origin\//d" \
 		    -e "/^refs\/remotes\/second\//d" >actual &&
@@ -476,7 +476,7 @@ test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
 		cd test &&
 		git fetch two "refs/heads/*:refs/remotes/two/*" &&
 		git remote set-head --auto two >output 2>&1 &&
-		echo "${SQ}two/HEAD${SQ} is now created and points to ${SQ}main${SQ}" >expect &&
+		echo "${SQ}two/HEAD${SQ} is unchanged and points to ${SQ}main${SQ}" >expect &&
 		test_cmp expect output
 	)
 '
@@ -764,8 +764,10 @@ test_expect_success 'reject --no-no-tags' '
 '
 
 cat >one/expect <<\EOF
+  apis/HEAD -> apis/main
   apis/main
   apis/side
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
@@ -783,11 +785,14 @@ test_expect_success 'update' '
 '
 
 cat >one/expect <<\EOF
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
+  manduca/HEAD -> manduca/main
   manduca/main
   manduca/side
+  megaloprepus/HEAD -> megaloprepus/main
   megaloprepus/main
   megaloprepus/side
 EOF
@@ -795,7 +800,7 @@ EOF
 test_expect_success 'update with arguments' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -827,10 +832,13 @@ test_expect_success 'update --prune' '
 '
 
 cat >one/expect <<-\EOF
+  apis/HEAD -> apis/main
   apis/main
   apis/side
+  manduca/HEAD -> manduca/main
   manduca/main
   manduca/side
+  megaloprepus/HEAD -> megaloprepus/main
   megaloprepus/main
   megaloprepus/side
 EOF
@@ -838,7 +846,7 @@ EOF
 test_expect_success 'update default' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -850,6 +858,7 @@ test_expect_success 'update default' '
 '
 
 cat >one/expect <<\EOF
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
@@ -858,7 +867,7 @@ EOF
 test_expect_success 'update default (overridden, with funny whitespace)' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -872,7 +881,7 @@ test_expect_success 'update default (overridden, with funny whitespace)' '
 test_expect_success 'update (with remotes.default defined)' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index 0890b9f61c..dfc8d748ba 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -75,6 +75,30 @@ test_expect_success "fetch test for-merge" '
 	cut -f -2 .git/FETCH_HEAD >actual &&
 	test_cmp expected actual'
 
+test_expect_success "fetch test remote HEAD" '
+	cd "$D" &&
+	cd two &&
+	git fetch &&
+	git rev-parse --verify refs/remotes/origin/HEAD &&
+	git rev-parse --verify refs/remotes/origin/main &&
+	head=$(git rev-parse refs/remotes/origin/HEAD) &&
+	branch=$(git rev-parse refs/remotes/origin/main) &&
+	test "z$head" = "z$branch"'
+
+test_expect_success "fetch test remote HEAD change" '
+	cd "$D" &&
+	cd two &&
+	git switch -c other &&
+	git push -u origin other &&
+	git rev-parse --verify refs/remotes/origin/HEAD &&
+	git rev-parse --verify refs/remotes/origin/main &&
+	git rev-parse --verify refs/remotes/origin/other &&
+	git remote set-head origin other &&
+	git fetch &&
+	head=$(git rev-parse refs/remotes/origin/HEAD) &&
+	branch=$(git rev-parse refs/remotes/origin/other) &&
+	test "z$head" = "z$branch"'
+
 test_expect_success 'fetch --prune on its own works as expected' '
 	cd "$D" &&
 	git clone . prune &&
@@ -478,7 +502,6 @@ test_expect_success 'unbundle 1' '
 	test_must_fail git fetch "$D/bundle1" main:main
 '
 
-
 test_expect_success 'bundle 1 has only 3 files ' '
 	cd "$D" &&
 	test_bundle_object_count bundle1 3
@@ -819,7 +842,7 @@ test_expect_success 'fetch from multiple configured URLs in single remote' '
 
 # configured prune tests
 
-set_config_tristate () {
+set_config_tristate() {
 	# var=$1 val=$2
 	case "$2" in
 	unset)
@@ -833,12 +856,12 @@ set_config_tristate () {
 	esac
 }
 
-test_configured_prune () {
+test_configured_prune() {
 	test_configured_prune_type "$@" "name"
 	test_configured_prune_type "$@" "link"
 }
 
-test_configured_prune_type () {
+test_configured_prune_type() {
 	fetch_prune=$1
 	remote_origin_prune=$2
 	fetch_prune_tags=$3
@@ -848,8 +871,7 @@ test_configured_prune_type () {
 	cmdline=$7
 	mode=$8
 
-	if test -z "$cmdline_setup"
-	then
+	if test -z "$cmdline_setup"; then
 		test_expect_success 'setup cmdline_setup variable for subsequent test' '
 			remote_url="file://$(git -C one config remote.origin.url)" &&
 			remote_fetch="$(git -C one config remote.origin.fetch)" &&
@@ -857,12 +879,10 @@ test_configured_prune_type () {
 		'
 	fi
 
-	if test "$mode" = 'link'
-	then
+	if test "$mode" = 'link'; then
 		new_cmdline=""
 
-		if test "$cmdline" = ""
-		then
+		if test "$cmdline" = ""; then
 			new_cmdline=$cmdline_setup
 		else
 			new_cmdline=$(perl -e '
@@ -873,10 +893,8 @@ test_configured_prune_type () {
 		fi
 
 		if test "$fetch_prune_tags" = 'true' ||
-		   test "$remote_origin_prune_tags" = 'true'
-		then
-			if ! printf '%s' "$cmdline\n" | grep -q refs/remotes/origin/
-			then
+			test "$remote_origin_prune_tags" = 'true'; then
+			if ! printf '%s' "$cmdline\n" | grep -q refs/remotes/origin/; then
 				new_cmdline="$new_cmdline refs/tags/*:refs/tags/*"
 			fi
 		fi
@@ -946,100 +964,100 @@ test_configured_prune_type () {
 # $7 git-fetch $cmdline:
 #
 #                     $1    $2    $3    $4    $5     $6     $7
-test_configured_prune unset unset unset unset kept   kept   ""
-test_configured_prune unset unset unset unset kept   kept   "--no-prune"
-test_configured_prune unset unset unset unset pruned kept   "--prune"
-test_configured_prune unset unset unset unset kept   pruned \
+test_configured_prune unset unset unset unset kept kept ""
+test_configured_prune unset unset unset unset kept kept "--no-prune"
+test_configured_prune unset unset unset unset pruned kept "--prune"
+test_configured_prune unset unset unset unset kept pruned \
 	"--prune origin refs/tags/*:refs/tags/*"
 test_configured_prune unset unset unset unset pruned pruned \
 	"--prune origin refs/tags/*:refs/tags/* +refs/heads/*:refs/remotes/origin/*"
 
-test_configured_prune false unset unset unset kept   kept   ""
-test_configured_prune false unset unset unset kept   kept   "--no-prune"
-test_configured_prune false unset unset unset pruned kept   "--prune"
+test_configured_prune false unset unset unset kept kept ""
+test_configured_prune false unset unset unset kept kept "--no-prune"
+test_configured_prune false unset unset unset pruned kept "--prune"
 
-test_configured_prune true  unset unset unset pruned kept   ""
-test_configured_prune true  unset unset unset pruned kept   "--prune"
-test_configured_prune true  unset unset unset kept   kept   "--no-prune"
+test_configured_prune true unset unset unset pruned kept ""
+test_configured_prune true unset unset unset pruned kept "--prune"
+test_configured_prune true unset unset unset kept kept "--no-prune"
 
-test_configured_prune unset false unset unset kept   kept   ""
-test_configured_prune unset false unset unset kept   kept   "--no-prune"
-test_configured_prune unset false unset unset pruned kept   "--prune"
+test_configured_prune unset false unset unset kept kept ""
+test_configured_prune unset false unset unset kept kept "--no-prune"
+test_configured_prune unset false unset unset pruned kept "--prune"
 
-test_configured_prune false false unset unset kept   kept   ""
-test_configured_prune false false unset unset kept   kept   "--no-prune"
-test_configured_prune false false unset unset pruned kept   "--prune"
-test_configured_prune false false unset unset kept   pruned \
+test_configured_prune false false unset unset kept kept ""
+test_configured_prune false false unset unset kept kept "--no-prune"
+test_configured_prune false false unset unset pruned kept "--prune"
+test_configured_prune false false unset unset kept pruned \
 	"--prune origin refs/tags/*:refs/tags/*"
 test_configured_prune false false unset unset pruned pruned \
 	"--prune origin refs/tags/*:refs/tags/* +refs/heads/*:refs/remotes/origin/*"
 
-test_configured_prune true  false unset unset kept   kept   ""
-test_configured_prune true  false unset unset pruned kept   "--prune"
-test_configured_prune true  false unset unset kept   kept   "--no-prune"
+test_configured_prune true false unset unset kept kept ""
+test_configured_prune true false unset unset pruned kept "--prune"
+test_configured_prune true false unset unset kept kept "--no-prune"
 
-test_configured_prune unset true  unset unset pruned kept   ""
-test_configured_prune unset true  unset unset kept   kept   "--no-prune"
-test_configured_prune unset true  unset unset pruned kept   "--prune"
+test_configured_prune unset true unset unset pruned kept ""
+test_configured_prune unset true unset unset kept kept "--no-prune"
+test_configured_prune unset true unset unset pruned kept "--prune"
 
-test_configured_prune false true  unset unset pruned kept   ""
-test_configured_prune false true  unset unset kept   kept   "--no-prune"
-test_configured_prune false true  unset unset pruned kept   "--prune"
+test_configured_prune false true unset unset pruned kept ""
+test_configured_prune false true unset unset kept kept "--no-prune"
+test_configured_prune false true unset unset pruned kept "--prune"
 
-test_configured_prune true  true  unset unset pruned kept   ""
-test_configured_prune true  true  unset unset pruned kept   "--prune"
-test_configured_prune true  true  unset unset kept   kept   "--no-prune"
-test_configured_prune true  true  unset unset kept   pruned \
+test_configured_prune true true unset unset pruned kept ""
+test_configured_prune true true unset unset pruned kept "--prune"
+test_configured_prune true true unset unset kept kept "--no-prune"
+test_configured_prune true true unset unset kept pruned \
 	"--prune origin refs/tags/*:refs/tags/*"
-test_configured_prune true  true  unset unset pruned pruned \
+test_configured_prune true true unset unset pruned pruned \
 	"--prune origin refs/tags/*:refs/tags/* +refs/heads/*:refs/remotes/origin/*"
 
 # --prune-tags on its own does nothing, needs --prune as well, same
 # for fetch.pruneTags without fetch.prune
-test_configured_prune unset unset unset unset kept kept     "--prune-tags"
-test_configured_prune unset unset true unset  kept kept     ""
-test_configured_prune unset unset unset true  kept kept     ""
+test_configured_prune unset unset unset unset kept kept "--prune-tags"
+test_configured_prune unset unset true unset kept kept ""
+test_configured_prune unset unset unset true kept kept ""
 
 # These will prune the tags
 test_configured_prune unset unset unset unset pruned pruned "--prune --prune-tags"
-test_configured_prune true  unset true  unset pruned pruned ""
-test_configured_prune unset true  unset true  pruned pruned ""
+test_configured_prune true unset true unset pruned pruned ""
+test_configured_prune unset true unset true pruned pruned ""
 
 # remote.<name>.pruneTags overrides fetch.pruneTags, just like
 # remote.<name>.prune overrides fetch.prune if set.
-test_configured_prune true  unset true unset pruned pruned  ""
-test_configured_prune false true  false true  pruned pruned ""
-test_configured_prune true  false true  false kept   kept   ""
+test_configured_prune true unset true unset pruned pruned ""
+test_configured_prune false true false true pruned pruned ""
+test_configured_prune true false true false kept kept ""
 
 # When --prune-tags is supplied it's ignored if an explicit refspec is
 # given, same for the configuration options.
 test_configured_prune unset unset unset unset pruned kept \
 	"--prune --prune-tags origin +refs/heads/*:refs/remotes/origin/*"
-test_configured_prune unset unset true  unset pruned kept \
+test_configured_prune unset unset true unset pruned kept \
 	"--prune origin +refs/heads/*:refs/remotes/origin/*"
-test_configured_prune unset unset unset true pruned  kept \
+test_configured_prune unset unset unset true pruned kept \
 	"--prune origin +refs/heads/*:refs/remotes/origin/*"
 
 # Pruning that also takes place if a file:// url replaces a named
 # remote. However, because there's no implicit
 # +refs/heads/*:refs/remotes/origin/* refspec and supplying it on the
 # command-line negates --prune-tags, the branches will not be pruned.
-test_configured_prune_type unset unset unset unset kept   kept   "origin --prune-tags" "name"
-test_configured_prune_type unset unset unset unset kept   kept   "origin --prune-tags" "link"
+test_configured_prune_type unset unset unset unset kept kept "origin --prune-tags" "name"
+test_configured_prune_type unset unset unset unset kept kept "origin --prune-tags" "link"
 test_configured_prune_type unset unset unset unset pruned pruned "origin --prune --prune-tags" "name"
-test_configured_prune_type unset unset unset unset kept   pruned "origin --prune --prune-tags" "link"
+test_configured_prune_type unset unset unset unset kept pruned "origin --prune --prune-tags" "link"
 test_configured_prune_type unset unset unset unset pruned pruned "--prune --prune-tags origin" "name"
-test_configured_prune_type unset unset unset unset kept   pruned "--prune --prune-tags origin" "link"
-test_configured_prune_type unset unset true  unset pruned pruned "--prune origin" "name"
-test_configured_prune_type unset unset true  unset kept   pruned "--prune origin" "link"
-test_configured_prune_type unset unset unset true  pruned pruned "--prune origin" "name"
-test_configured_prune_type unset unset unset true  kept   pruned "--prune origin" "link"
-test_configured_prune_type true  unset true  unset pruned pruned "origin" "name"
-test_configured_prune_type true  unset true  unset kept   pruned "origin" "link"
-test_configured_prune_type unset  true true  unset pruned pruned "origin" "name"
-test_configured_prune_type unset  true true  unset kept   pruned "origin" "link"
-test_configured_prune_type unset  true unset true  pruned pruned "origin" "name"
-test_configured_prune_type unset  true unset true  kept   pruned "origin" "link"
+test_configured_prune_type unset unset unset unset kept pruned "--prune --prune-tags origin" "link"
+test_configured_prune_type unset unset true unset pruned pruned "--prune origin" "name"
+test_configured_prune_type unset unset true unset kept pruned "--prune origin" "link"
+test_configured_prune_type unset unset unset true pruned pruned "--prune origin" "name"
+test_configured_prune_type unset unset unset true kept pruned "--prune origin" "link"
+test_configured_prune_type true unset true unset pruned pruned "origin" "name"
+test_configured_prune_type true unset true unset kept pruned "origin" "link"
+test_configured_prune_type unset true true unset pruned pruned "origin" "name"
+test_configured_prune_type unset true true unset kept pruned "origin" "link"
+test_configured_prune_type unset true unset true pruned pruned "origin" "name"
+test_configured_prune_type unset true unset true kept pruned "origin" "link"
 
 # When all remote.origin.fetch settings are deleted a --prune
 # --prune-tags still implicitly supplies refs/tags/*:refs/tags/* so
@@ -1137,8 +1155,7 @@ test_expect_success 'fetching with auto-gc does not lock up' '
 	)
 '
 
-for section in fetch transfer
-do
+for section in fetch transfer; do
 	test_expect_success "$section.hideRefs affects connectivity check" '
 		GIT_TRACE="$PWD"/trace git -c $section.hideRefs=refs -c \
 			$section.hideRefs="!refs/tags/" fetch &&
@@ -1157,21 +1174,23 @@ test_expect_success 'prepare source branch' '
 	test 3 -le $(wc -l <actual)
 '
 
-validate_store_type () {
+validate_store_type() {
 	git -C dest count-objects -v >actual &&
-	case "$store_type" in
-	packed)
-		grep "^count: 0$" actual ;;
-	loose)
-		grep "^packs: 0$" actual ;;
-	esac || {
+		case "$store_type" in
+		packed)
+			grep "^count: 0$" actual
+			;;
+		loose)
+			grep "^packs: 0$" actual
+			;;
+		esac || {
 		echo "store_type is $store_type"
 		cat actual
 		false
 	}
 }
 
-test_unpack_limit () {
+test_unpack_limit() {
 	store_type=$1
 
 	case "$store_type" in
@@ -1192,43 +1211,39 @@ test_unpack_limit () {
 test_unpack_limit packed
 test_unpack_limit loose
 
-setup_negotiation_tip () {
+setup_negotiation_tip() {
 	SERVER="$1"
 	URL="$2"
 	USE_PROTOCOL_V2="$3"
 
 	rm -rf "$SERVER" client trace &&
-	git init -b main "$SERVER" &&
-	test_commit -C "$SERVER" alpha_1 &&
-	test_commit -C "$SERVER" alpha_2 &&
-	git -C "$SERVER" checkout --orphan beta &&
-	test_commit -C "$SERVER" beta_1 &&
-	test_commit -C "$SERVER" beta_2 &&
-
-	git clone "$URL" client &&
-
-	if test "$USE_PROTOCOL_V2" -eq 1
-	then
-		git -C "$SERVER" config protocol.version 2 &&
-		git -C client config protocol.version 2
-	fi &&
-
-	test_commit -C "$SERVER" beta_s &&
-	git -C "$SERVER" checkout main &&
-	test_commit -C "$SERVER" alpha_s &&
-	git -C "$SERVER" tag -d alpha_1 alpha_2 beta_1 beta_2
+		git init -b main "$SERVER" &&
+		test_commit -C "$SERVER" alpha_1 &&
+		test_commit -C "$SERVER" alpha_2 &&
+		git -C "$SERVER" checkout --orphan beta &&
+		test_commit -C "$SERVER" beta_1 &&
+		test_commit -C "$SERVER" beta_2 &&
+		git clone "$URL" client &&
+		if test "$USE_PROTOCOL_V2" -eq 1; then
+			git -C "$SERVER" config protocol.version 2 &&
+				git -C client config protocol.version 2
+		fi &&
+		test_commit -C "$SERVER" beta_s &&
+		git -C "$SERVER" checkout main &&
+		test_commit -C "$SERVER" alpha_s &&
+		git -C "$SERVER" tag -d alpha_1 alpha_2 beta_1 beta_2
 }
 
-check_negotiation_tip () {
+check_negotiation_tip() {
 	# Ensure that {alpha,beta}_1 are sent as "have", but not {alpha_beta}_2
 	ALPHA_1=$(git -C client rev-parse alpha_1) &&
-	grep "fetch> have $ALPHA_1" trace &&
-	BETA_1=$(git -C client rev-parse beta_1) &&
-	grep "fetch> have $BETA_1" trace &&
-	ALPHA_2=$(git -C client rev-parse alpha_2) &&
-	! grep "fetch> have $ALPHA_2" trace &&
-	BETA_2=$(git -C client rev-parse beta_2) &&
-	! grep "fetch> have $BETA_2" trace
+		grep "fetch> have $ALPHA_1" trace &&
+		BETA_1=$(git -C client rev-parse beta_1) &&
+		grep "fetch> have $BETA_1" trace &&
+		ALPHA_2=$(git -C client rev-parse alpha_2) &&
+		! grep "fetch> have $ALPHA_2" trace &&
+		BETA_2=$(git -C client rev-parse beta_2) &&
+		! grep "fetch> have $BETA_2" trace
 }
 
 test_expect_success '--negotiation-tip limits "have" lines sent' '
diff --git a/t/t5512-ls-remote.sh b/t/t5512-ls-remote.sh
index 64b3491e4e..1b3865e154 100755
--- a/t/t5512-ls-remote.sh
+++ b/t/t5512-ls-remote.sh
@@ -293,6 +293,8 @@ test_expect_success 'ls-remote with filtered symref (refname)' '
 	cat >expect <<-EOF &&
 	ref: refs/heads/main	HEAD
 	$rev	HEAD
+	ref: refs/remotes/origin/main	refs/remotes/origin/HEAD
+	$rev	refs/remotes/origin/HEAD
 	EOF
 	git ls-remote --symref . HEAD >actual &&
 	test_cmp expect actual
diff --git a/t/t5514-fetch-multiple.sh b/t/t5514-fetch-multiple.sh
index 579872c258..e3482b27b2 100755
--- a/t/t5514-fetch-multiple.sh
+++ b/t/t5514-fetch-multiple.sh
@@ -45,14 +45,17 @@ test_expect_success setup '
 '
 
 cat > test/expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
   origin/HEAD -> origin/main
   origin/main
   origin/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -97,6 +100,7 @@ cat > expect << EOF
   origin/HEAD -> origin/main
   origin/main
   origin/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
@@ -112,8 +116,10 @@ test_expect_success 'git fetch --multiple (but only one remote)' '
 '
 
 cat > expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -141,7 +147,7 @@ test_expect_success 'git fetch --multiple (bad remote names)' '
 
 test_expect_success 'git fetch --all (skipFetchAll)' '
 	(cd test4 &&
-	 for b in $(git branch -r)
+	 for b in $(git branch -r | grep -v HEAD)
 	 do
 		git branch -r -d $b || exit 1
 	 done &&
@@ -153,11 +159,14 @@ test_expect_success 'git fetch --all (skipFetchAll)' '
 '
 
 cat > expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -165,7 +174,7 @@ EOF
 
 test_expect_success 'git fetch --multiple (ignoring skipFetchAll)' '
 	(cd test4 &&
-	 for b in $(git branch -r)
+	 for b in $(git branch -r | grep -v HEAD)
 	 do
 		git branch -r -d $b || exit 1
 	 done &&
@@ -221,14 +230,17 @@ test_expect_success 'git fetch --multiple --jobs=0 picks a default' '
 
 create_fetch_all_expect () {
 	cat >expect <<-\EOF
+	  one/HEAD -> one/main
 	  one/main
 	  one/side
 	  origin/HEAD -> origin/main
 	  origin/main
 	  origin/side
+	  three/HEAD -> three/main
 	  three/another
 	  three/main
 	  three/side
+	  two/HEAD -> two/main
 	  two/another
 	  two/main
 	  two/side
@@ -265,6 +277,7 @@ test_expect_success 'git fetch (fetch all remotes with fetch.all = true)' '
 
 create_fetch_one_expect () {
 	cat >expect <<-\EOF
+	  one/HEAD -> one/main
 	  one/main
 	  one/side
 	  origin/HEAD -> origin/main
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 331778bd42..5a051aa0c7 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1395,7 +1395,8 @@ test_expect_success 'fetch follows tags by default' '
 		git tag -m "annotated" tag &&
 		git for-each-ref >tmp1 &&
 		sed -n "p; s|refs/heads/main$|refs/remotes/origin/main|p" tmp1 |
-		sort -k 3 >../expect
+		sed -n "p; s|refs/heads/main$|refs/remotes/origin/HEAD|p"  |
+		sort -k 4 >../expect
 	) &&
 	test_when_finished "rm -rf dst" &&
 	git init dst &&
diff --git a/t/t5527-fetch-odd-refs.sh b/t/t5527-fetch-odd-refs.sh
index 98ece27c6a..d3996af6ee 100755
--- a/t/t5527-fetch-odd-refs.sh
+++ b/t/t5527-fetch-odd-refs.sh
@@ -52,7 +52,8 @@ test_expect_success LONG_REF 'fetch handles extremely long refname' '
 	long
 	main
 	EOF
-	git for-each-ref --format="%(subject)" refs/remotes/long >actual &&
+	git for-each-ref --format="%(subject)" refs/remotes/long \
+		--exclude=refs/remotes/long/HEAD >actual &&
 	test_cmp expect actual
 '
 
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index a66d0e089d..88331d1ceb 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -329,7 +329,8 @@ test_expect_success 'incremental-repack task' '
 
 	# Delete refs that have not been repacked in these packs.
 	git for-each-ref --format="delete %(refname)" \
-		refs/prefetch refs/tags refs/remotes >refs &&
+		refs/prefetch refs/tags refs/remotes \
+		--exclude=refs/remotes/*/HEAD >refs &&
 	git update-ref --stdin <refs &&
 
 	# Replace the object directory with this pack layout.
diff --git a/t/t9210-scalar.sh b/t/t9210-scalar.sh
index a30b2c9f70..2237844550 100755
--- a/t/t9210-scalar.sh
+++ b/t/t9210-scalar.sh
@@ -151,7 +151,8 @@ test_expect_success 'scalar clone' '
 			"$(pwd)" &&
 
 		git for-each-ref --format="%(refname)" refs/remotes/origin/ >actual &&
-		echo "refs/remotes/origin/parallel" >expect &&
+		echo "refs/remotes/origin/HEAD" >>expect &&
+		echo "refs/remotes/origin/parallel" >>expect &&
 		test_cmp expect actual &&
 
 		test_path_is_missing 1/2 &&
@@ -220,7 +221,7 @@ test_expect_success 'scalar reconfigure --all with includeIf.onbranch' '
 	done
 '
 
- test_expect_success 'scalar reconfigure --all with detached HEADs' '
+test_expect_success 'scalar reconfigure --all with detached HEADs' '
 	repos="two three four" &&
 	for num in $repos
 	do
diff --git a/t/t9211-scalar-clone.sh b/t/t9211-scalar-clone.sh
index c16ea67c1d..d9cb6b9a3e 100755
--- a/t/t9211-scalar-clone.sh
+++ b/t/t9211-scalar-clone.sh
@@ -32,7 +32,7 @@ test_expect_success 'set up repository to clone' '
 	)
 '
 
-cleanup_clone () {
+cleanup_clone() {
 	rm -rf "$1"
 }
 
@@ -128,7 +128,7 @@ test_expect_success '--single-branch clones HEAD only' '
 	(
 		cd $enlistment/src &&
 		git for-each-ref refs/remotes/origin >out &&
-		test_line_count = 1 out &&
+		test_line_count = 2 out &&
 		grep "refs/remotes/origin/base" out
 	) &&
 
@@ -142,7 +142,7 @@ test_expect_success '--no-single-branch clones all branches' '
 	(
 		cd $enlistment/src &&
 		git for-each-ref refs/remotes/origin >out &&
-		test_line_count = 2 out &&
+		test_line_count = 3 out &&
 		grep "refs/remotes/origin/base" out &&
 		grep "refs/remotes/origin/parallel" out
 	) &&
diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh
index cc6aa9f0cd..b663c4609e 100755
--- a/t/t9902-completion.sh
+++ b/t/t9902-completion.sh
@@ -658,6 +658,7 @@ test_expect_success '__git_refs - simple' '
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	matching-tag
@@ -673,6 +674,7 @@ test_expect_success '__git_refs - full refs' '
 	cat >expected <<-EOF &&
 	refs/heads/main
 	refs/heads/matching-branch
+	refs/remotes/other/HEAD
 	refs/remotes/other/branch-in-other
 	refs/remotes/other/main-in-other
 	refs/tags/matching-tag
@@ -729,6 +731,7 @@ test_expect_success '__git_refs - remote on local file system - full refs' '
 test_expect_success '__git_refs - configured remote' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -756,6 +759,7 @@ test_expect_success '__git_refs - configured remote - full refs' '
 test_expect_success '__git_refs - configured remote - repo given on the command line' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -787,6 +791,7 @@ test_expect_success '__git_refs - configured remote - full refs - repo given on
 test_expect_success '__git_refs - configured remote - remote name matches a directory' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -875,12 +880,14 @@ test_expect_success '__git_refs - unique remote branches for git checkout DWIMer
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/ambiguous
 	other/branch-in-other
 	other/main-in-other
 	remote/ambiguous
 	remote/branch-in-remote
 	matching-tag
+	HEAD
 	branch-in-other
 	branch-in-remote
 	main-in-other
@@ -904,6 +911,7 @@ test_expect_success '__git_refs - after --opt=' '
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	matching-tag
@@ -919,6 +927,7 @@ test_expect_success '__git_refs - after --opt= - full refs' '
 	cat >expected <<-EOF &&
 	refs/heads/main
 	refs/heads/matching-branch
+	refs/remotes/other/HEAD
 	refs/remotes/other/branch-in-other
 	refs/remotes/other/main-in-other
 	refs/tags/matching-tag
@@ -935,6 +944,7 @@ test_expect_success '__git refs - excluding refs' '
 	^HEAD
 	^main
 	^matching-branch
+	^other/HEAD
 	^other/branch-in-other
 	^other/main-in-other
 	^matching-tag
@@ -950,6 +960,7 @@ test_expect_success '__git refs - excluding full refs' '
 	cat >expected <<-EOF &&
 	^refs/heads/main
 	^refs/heads/matching-branch
+	^refs/remotes/other/HEAD
 	^refs/remotes/other/branch-in-other
 	^refs/remotes/other/main-in-other
 	^refs/tags/matching-tag
@@ -975,6 +986,7 @@ test_expect_success '__git_refs - do not filter refs unless told so' '
 	main
 	matching-branch
 	matching/branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	other/matching/branch-in-other
@@ -1095,6 +1107,7 @@ test_expect_success '__git_complete_refs - simple' '
 	HEAD Z
 	main Z
 	matching-branch Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	matching-tag Z
@@ -1123,6 +1136,7 @@ test_expect_success '__git_complete_refs - matching' '
 test_expect_success '__git_complete_refs - remote' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main-in-other Z
 	EOF
@@ -1139,9 +1153,11 @@ test_expect_success '__git_complete_refs - track' '
 	HEAD Z
 	main Z
 	matching-branch Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	matching-tag Z
+	HEAD Z
 	branch-in-other Z
 	main-in-other Z
 	EOF
@@ -1184,6 +1200,7 @@ test_expect_success '__git_complete_refs - suffix' '
 	HEAD.
 	main.
 	matching-branch.
+	other/HEAD.
 	other/branch-in-other.
 	other/main-in-other.
 	matching-tag.
@@ -1199,6 +1216,7 @@ test_expect_success '__git_complete_refs - suffix' '
 test_expect_success '__git_complete_fetch_refspecs - simple' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	HEAD:HEAD Z
+	HEAD:HEAD Z
 	branch-in-other:branch-in-other Z
 	main-in-other:main-in-other Z
 	EOF
@@ -1225,6 +1243,7 @@ test_expect_success '__git_complete_fetch_refspecs - matching' '
 test_expect_success '__git_complete_fetch_refspecs - prefix' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	+HEAD:HEAD Z
+	+HEAD:HEAD Z
 	+branch-in-other:branch-in-other Z
 	+main-in-other:main-in-other Z
 	EOF
@@ -1289,6 +1308,7 @@ test_expect_success '__git_complete_worktree_paths with -C' '
 
 test_expect_success 'git switch - with no options, complete local branches and unique remote branch names for DWIM logic' '
 	test_completion "git switch " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1435,11 +1455,13 @@ test_expect_success 'git-bisect - existing view subcommand is recognized and ena
 test_expect_success 'git checkout - completes refs and unique remote branches for DWIM' '
 	test_completion "git checkout " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1461,6 +1483,7 @@ test_expect_success 'git switch - with GIT_COMPLETION_CHECKOUT_NO_GUESS=1, compl
 
 test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_GUESS=1, complete local branches and unique remote names for DWIM logic' '
 	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git switch --guess " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1470,6 +1493,7 @@ test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_G
 
 test_expect_success 'git switch - a later --guess overrides previous --no-guess, complete local and remote unique branches for DWIM' '
 	test_completion "git switch --no-guess --guess " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1490,6 +1514,7 @@ test_expect_success 'git checkout - with GIT_COMPLETION_NO_GUESS=1 only complete
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1498,11 +1523,13 @@ test_expect_success 'git checkout - with GIT_COMPLETION_NO_GUESS=1 only complete
 test_expect_success 'git checkout - --guess overrides GIT_COMPLETION_NO_GUESS=1, complete refs and unique remote branches for DWIM' '
 	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git checkout --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1514,6 +1541,7 @@ test_expect_success 'git checkout - with --no-guess, only completes refs' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1522,11 +1550,13 @@ test_expect_success 'git checkout - with --no-guess, only completes refs' '
 test_expect_success 'git checkout - a later --guess overrides previous --no-guess, complete refs and unique remote branches for DWIM' '
 	test_completion "git checkout --no-guess --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1538,6 +1568,7 @@ test_expect_success 'git checkout - a later --no-guess overrides previous --gues
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1550,6 +1581,7 @@ test_expect_success 'git checkout - with checkout.guess = false, only completes
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1559,11 +1591,13 @@ test_expect_success 'git checkout - with checkout.guess = true, completes refs a
 	test_config checkout.guess true &&
 	test_completion "git checkout " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1573,11 +1607,13 @@ test_expect_success 'git checkout - a later --guess overrides previous checkout.
 	test_config checkout.guess false &&
 	test_completion "git checkout --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1590,6 +1626,7 @@ test_expect_success 'git checkout - a later --no-guess overrides previous checko
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1601,6 +1638,7 @@ test_expect_success 'git switch - with --detach, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1612,6 +1650,7 @@ test_expect_success 'git checkout - with --detach, complete only references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1783,6 +1822,7 @@ test_expect_success 'git switch - with -d, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1794,6 +1834,7 @@ test_expect_success 'git checkout - with -d, complete only references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1801,10 +1842,12 @@ test_expect_success 'git checkout - with -d, complete only references' '
 
 test_expect_success 'git switch - with --track, complete only remote branches' '
 	test_completion "git switch --track " <<-\EOF &&
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
 	test_completion "git switch -t " <<-\EOF
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1812,10 +1855,12 @@ test_expect_success 'git switch - with --track, complete only remote branches' '
 
 test_expect_success 'git checkout - with --track, complete only remote branches' '
 	test_completion "git checkout --track " <<-\EOF &&
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
 	test_completion "git checkout -t " <<-\EOF
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1834,6 +1879,7 @@ test_expect_success 'git checkout - with --no-track, complete only local referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1845,6 +1891,7 @@ test_expect_success 'git switch - with -c, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1856,6 +1903,7 @@ test_expect_success 'git switch - with -C, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1867,6 +1915,7 @@ test_expect_success 'git switch - with -c and --track, complete all references'
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1878,6 +1927,7 @@ test_expect_success 'git switch - with -C and --track, complete all references'
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1889,6 +1939,7 @@ test_expect_success 'git switch - with -c and --no-track, complete all reference
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1900,6 +1951,7 @@ test_expect_success 'git switch - with -C and --no-track, complete all reference
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1911,6 +1963,7 @@ test_expect_success 'git checkout - with -b, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1922,6 +1975,7 @@ test_expect_success 'git checkout - with -B, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1933,6 +1987,7 @@ test_expect_success 'git checkout - with -b and --track, complete all references
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1944,6 +1999,7 @@ test_expect_success 'git checkout - with -B and --track, complete all references
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1955,6 +2011,7 @@ test_expect_success 'git checkout - with -b and --no-track, complete all referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1966,6 +2023,7 @@ test_expect_success 'git checkout - with -B and --no-track, complete all referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1973,6 +2031,7 @@ test_expect_success 'git checkout - with -B and --no-track, complete all referen
 
 test_expect_success 'git switch - for -c, complete local branches and unique remote branches' '
 	test_completion "git switch -c " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1982,6 +2041,7 @@ test_expect_success 'git switch - for -c, complete local branches and unique rem
 
 test_expect_success 'git switch - for -C, complete local branches and unique remote branches' '
 	test_completion "git switch -C " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2019,6 +2079,7 @@ test_expect_success 'git switch - for -C with --no-track, complete local branche
 
 test_expect_success 'git checkout - for -b, complete local branches and unique remote branches' '
 	test_completion "git checkout -b " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2028,6 +2089,7 @@ test_expect_success 'git checkout - for -b, complete local branches and unique r
 
 test_expect_success 'git checkout - for -B, complete local branches and unique remote branches' '
 	test_completion "git checkout -B " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2065,6 +2127,7 @@ test_expect_success 'git checkout - for -B with --no-track, complete local branc
 
 test_expect_success 'git switch - with --orphan completes local branch names and unique remote branch names' '
 	test_completion "git switch --orphan " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2080,6 +2143,7 @@ test_expect_success 'git switch - --orphan with branch already provided complete
 
 test_expect_success 'git checkout - with --orphan completes local branch names and unique remote branch names' '
 	test_completion "git checkout --orphan " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2093,6 +2157,7 @@ test_expect_success 'git checkout - --orphan with branch already provided comple
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
-- 
2.47.0.92.g83fdbe24c3.dirty
Bence Ferdinandy· Oct 21, 2024, 21:13 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v9 0/7] set-head/fetch remote/HEAD updates

Hi,
just a heads-up that due to me not being careful, v10 is at a new thread:
 
https://lore.kernel.org/git/4836c7c9-58d7-4e2a-ab78-be4e7296dc1f@ferdinandy.com/T/, 
with a wrong noreply address in Cc.
Sorry for the noise.

Best, Bence

Bence Ferdinandy· Oct 22, 2024, 19:45 UTC · re: Bence Ferdinandy · lore

[PATCH v11 0/8] set-head/fetch remote/HEAD updates

v11: fixed leaks found by running tests after `make SANITIZE=leak`
    green pipeline:
    https://github.com/ferdinandyb/git/actions/runs/11466803523

v10: added a new patch (8/8) on top of v9 to handle HEAD in mirrored bare repositories, rest of the patches are unchanged

v9:
the two most notable changes are
- the new patch 1/7 which address
  https://lore.kernel.org/git/Zw8IKyPkG0Hr6%2F5t@nand.local/, but see
  https://lore.kernel.org/git/D4ZAELFWJMKN.S88LJ6YK31LZ@ferdinandy.com/
- the refs_update_symref -> refs_update_symref_extended change in 2/7,
  reflecting on Phillip's comments (see
  https://lore.kernel.org/git/a7cb48e5-d8ba-44c1-9dbe-d1e8f8a63e3c@gmail.com/)
Bence Ferdinandy (8):
  t/t5505-remote: set default branch to main
  refs: atomically record overwritten ref in update_symref
  remote set-head: refactor for readability
  remote set-head: better output for --auto
  refs: add TRANSACTION_CREATE_EXISTS error
  refs: add create_only option to refs_update_symref_extended
  fetch: set remote/HEAD if it does not exist
  fetch set_head: handle mirrored bare repositories
 builtin/fetch.c                  |  94 +++++++++++++
 builtin/remote.c                 |  52 +++++--
 refs.c                           |  41 +++++-
 refs.h                           |   8 +-
 refs/files-backend.c             |  24 ++--
 refs/reftable-backend.c          |   6 +-
 t/t4207-log-decoration-colors.sh |   3 +-
 t/t5505-remote.sh                |  83 ++++++++++-
 t/t5510-fetch.sh                 | 229 ++++++++++++++++---------------
 t/t5512-ls-remote.sh             |   2 +
 t/t5514-fetch-multiple.sh        |  17 ++-
 t/t5516-fetch-push.sh            |   3 +-
 t/t5527-fetch-odd-refs.sh        |   3 +-
 t/t7900-maintenance.sh           |   3 +-
 t/t9210-scalar.sh                |   5 +-
 t/t9211-scalar-clone.sh          |   6 +-
 t/t9902-completion.sh            |  65 +++++++++
 17 files changed, 490 insertions(+), 154 deletions(-)
-- 
2.47.0.94.gb64850d498
Bence Ferdinandy· Oct 22, 2024, 19:45 UTC · re: Bence Ferdinandy · lore

[PATCH v11 1/8] t/t5505-remote: set default branch to main

Consider the bare repository called "mirror" in the test. Running `git remote add --mirror -f origin ../one` will not change HEAD, consequently if init.defaultBranch is not the same as what HEAD in the remote ("one"), HEAD in "mirror" will be pointing to a non-existent reference. Hence if "mirror" is used as a remote by yet another repository, ls-remote will not show HEAD. On the other hand, if init.defaultBranch happens to match HEAD in "one", then ls-remote will show HEAD.

Since the CI globally exports GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main, there's a drift between how the test repositories are set up in the CI and during local testing. This issue does not manifest currently, as the test does not do any remote HEAD manipulation where this would come up, but should such things be added, a locally passing test would break the CI vice-versa.

Set GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main in the test to be consistent with the CI. ---

Notes:
    v9: - new patch
        - a bandaid for the CI issue noticed by Taylor (cf:
          https://lore.kernel.org/git/Zw8IKyPkG0Hr6%2F5t@nand.local/), but
          see
          https://lore.kernel.org/git/D4ZAELFWJMKN.S88LJ6YK31LZ@ferdinandy.com/ for the root cause in detail
    
    v10: no change
    
    v11: no change
 t/t5505-remote.sh | 3 +++
 1 file changed, 3 insertions(+)
Show changes to t/t5505-remote.sh +3 −0
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 532035933f..9b50276646 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -2,6 +2,9 @@
 
 test_description='git remote porcelain-ish'
 
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
 TEST_PASSES_SANITIZE_LEAK=true
 . ./test-lib.sh
 
-- 
2.47.0.94.gb64850d498
Kristoffer Haugsbakk· Oct 22, 2024, 19:53 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v11 1/8] t/t5505-remote: set default branch to main

On Tue, Oct 22, 2024, at 21:45, Bence Ferdinandy wrote:
Show 28 quoted lines
> Consider the bare repository called "mirror" in the test.  Running `git
> remote add --mirror -f origin ../one` will not change HEAD, consequently
> if init.defaultBranch is not the same as what HEAD in the remote
> ("one"), HEAD in "mirror" will be pointing to a non-existent reference.
> Hence if "mirror" is used as a remote by yet another repository,
> ls-remote will not show HEAD. On the other hand, if init.defaultBranch
> happens to match HEAD in "one", then ls-remote will show HEAD.
>
> Since the CI globally exports GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main,
> there's a drift between how the test repositories are set up in the CI
> and during local testing. This issue does not manifest currently, as the
> test does not do any remote HEAD manipulation where this would come up,
> but should such things be added, a locally passing test would break the
> CI vice-versa.
>
> Set GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main in the test to be
> consistent with the CI.
> ---
>
> Notes:
>     v9: - new patch
>         - a bandaid for the CI issue noticed by Taylor (cf:
>           https://lore.kernel.org/git/Zw8IKyPkG0Hr6%2F5t@nand.local/), 
> but
>           see
>           
> https://lore.kernel.org/git/D4ZAELFWJMKN.S88LJ6YK31LZ@ferdinandy.com/ 
> for the root cause in detail
Why no signoff?
-- 
Kristoffer Haugsbakk
Bence Ferdinandy· Oct 22, 2024, 20:14 UTC · re: Kristoffer Haugsbakk · lore

Re: [PATCH v11 1/8] t/t5505-remote: set default branch to main

On Tue Oct 22, 2024 at 21:53, Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com> wrote:
Show 31 quoted lines
> On Tue, Oct 22, 2024, at 21:45, Bence Ferdinandy wrote:
>> Consider the bare repository called "mirror" in the test.  Running `git
>> remote add --mirror -f origin ../one` will not change HEAD, consequently
>> if init.defaultBranch is not the same as what HEAD in the remote
>> ("one"), HEAD in "mirror" will be pointing to a non-existent reference.
>> Hence if "mirror" is used as a remote by yet another repository,
>> ls-remote will not show HEAD. On the other hand, if init.defaultBranch
>> happens to match HEAD in "one", then ls-remote will show HEAD.
>>
>> Since the CI globally exports GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main,
>> there's a drift between how the test repositories are set up in the CI
>> and during local testing. This issue does not manifest currently, as the
>> test does not do any remote HEAD manipulation where this would come up,
>> but should such things be added, a locally passing test would break the
>> CI vice-versa.
>>
>> Set GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main in the test to be
>> consistent with the CI.
>> ---
>>
>> Notes:
>>     v9: - new patch
>>         - a bandaid for the CI issue noticed by Taylor (cf:
>>           https://lore.kernel.org/git/Zw8IKyPkG0Hr6%2F5t@nand.local/), 
>> but
>>           see
>>           
>> https://lore.kernel.org/git/D4ZAELFWJMKN.S88LJ6YK31LZ@ferdinandy.com/ 
>> for the root cause in detail
>
> Why no signoff?

Because I always forget :( I've been thinking on making it by default, but I didn't want to add it to all the work related commits, where I'd be the only one doing it, but I just found the format.signoff setting, so I should be covered for future patches.

I'll wait some time before sending a fix for this :)

Thanks, Bence

-- 
bence.ferdinandy.com
Taylor Blau· Oct 23, 2024, 15:09 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v11 1/8] t/t5505-remote: set default branch to main

On Tue, Oct 22, 2024 at 10:14:12PM +0200, Bence Ferdinandy wrote:
Show 40 quoted lines
>
> On Tue Oct 22, 2024 at 21:53, Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com> wrote:
> > On Tue, Oct 22, 2024, at 21:45, Bence Ferdinandy wrote:
> >> Consider the bare repository called "mirror" in the test.  Running `git
> >> remote add --mirror -f origin ../one` will not change HEAD, consequently
> >> if init.defaultBranch is not the same as what HEAD in the remote
> >> ("one"), HEAD in "mirror" will be pointing to a non-existent reference.
> >> Hence if "mirror" is used as a remote by yet another repository,
> >> ls-remote will not show HEAD. On the other hand, if init.defaultBranch
> >> happens to match HEAD in "one", then ls-remote will show HEAD.
> >>
> >> Since the CI globally exports GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main,
> >> there's a drift between how the test repositories are set up in the CI
> >> and during local testing. This issue does not manifest currently, as the
> >> test does not do any remote HEAD manipulation where this would come up,
> >> but should such things be added, a locally passing test would break the
> >> CI vice-versa.
> >>
> >> Set GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main in the test to be
> >> consistent with the CI.
> >> ---
> >>
> >> Notes:
> >>     v9: - new patch
> >>         - a bandaid for the CI issue noticed by Taylor (cf:
> >>           https://lore.kernel.org/git/Zw8IKyPkG0Hr6%2F5t@nand.local/),
> >> but
> >>           see
> >>
> >> https://lore.kernel.org/git/D4ZAELFWJMKN.S88LJ6YK31LZ@ferdinandy.com/
> >> for the root cause in detail
> >
> > Why no signoff?
>
> Because I always forget :( I've been thinking on making it by default, but
> I didn't want to add it to all the work related commits, where I'd be the only
> one doing it, but I just found the format.signoff setting, so I should be
> covered for future patches.
>
> I'll wait some time before sending a fix for this :)

Yeah, I was going to suggest format.signOff as well. If you don't want to add your S-o-b to work commits but do want to add it to patches you send to the Git list, you could just stop there (assuming that you don't use format-patch for work-related patches).

Alternatively, you could set it in your copy of git.git's own configuration in $GIT_DIR/config, so that it doesn't apply to work you do outside of that repository.

Either way, I'm going to avoid queueing this round since it is missing the required Signed-off-by line.

Thanks, Taylor

Bence Ferdinandy· Oct 23, 2024, 15:34 UTC · re: Taylor Blau · lore

Re: [PATCH v11 1/8] t/t5505-remote: set default branch to main

On Wed Oct 23, 2024 at 17:09, Taylor Blau <me@ttaylorr.com> wrote:
Show 46 quoted lines
> On Tue, Oct 22, 2024 at 10:14:12PM +0200, Bence Ferdinandy wrote:
>>
>> On Tue Oct 22, 2024 at 21:53, Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com> wrote:
>> > On Tue, Oct 22, 2024, at 21:45, Bence Ferdinandy wrote:
>> >> Consider the bare repository called "mirror" in the test.  Running `git
>> >> remote add --mirror -f origin ../one` will not change HEAD, consequently
>> >> if init.defaultBranch is not the same as what HEAD in the remote
>> >> ("one"), HEAD in "mirror" will be pointing to a non-existent reference.
>> >> Hence if "mirror" is used as a remote by yet another repository,
>> >> ls-remote will not show HEAD. On the other hand, if init.defaultBranch
>> >> happens to match HEAD in "one", then ls-remote will show HEAD.
>> >>
>> >> Since the CI globally exports GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main,
>> >> there's a drift between how the test repositories are set up in the CI
>> >> and during local testing. This issue does not manifest currently, as the
>> >> test does not do any remote HEAD manipulation where this would come up,
>> >> but should such things be added, a locally passing test would break the
>> >> CI vice-versa.
>> >>
>> >> Set GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main in the test to be
>> >> consistent with the CI.
>> >> ---
>> >>
>> >> Notes:
>> >>     v9: - new patch
>> >>         - a bandaid for the CI issue noticed by Taylor (cf:
>> >>           https://lore.kernel.org/git/Zw8IKyPkG0Hr6%2F5t@nand.local/),
>> >> but
>> >>           see
>> >>
>> >> https://lore.kernel.org/git/D4ZAELFWJMKN.S88LJ6YK31LZ@ferdinandy.com/
>> >> for the root cause in detail
>> >
>> > Why no signoff?
>>
>> Because I always forget :( I've been thinking on making it by default, but
>> I didn't want to add it to all the work related commits, where I'd be the only
>> one doing it, but I just found the format.signoff setting, so I should be
>> covered for future patches.
>>
>> I'll wait some time before sending a fix for this :)
>
> Yeah, I was going to suggest format.signOff as well. If you don't want
> to add your S-o-b to work commits but do want to add it to patches you
> send to the Git list, you could just stop there (assuming that you don't
> use format-patch for work-related patches).

The projects I contribute to seem to have a bidirectional mapping of requiring S-o-b and format patch so I should be good with this :)

Show 7 quoted lines
>
> Alternatively, you could set it in your copy of git.git's own
> configuration in $GIT_DIR/config, so that it doesn't apply to work you
> do outside of that repository.
>
> Either way, I'm going to avoid queueing this round since it is missing
> the required Signed-off-by line.

In that case I'll send a v12 with the S-o-b so the series can get back into circulation.

Best, Bence

>
> Thanks,
> Taylor
-- 
bence.ferdinandy.com
Taylor Blau· Oct 23, 2024, 18:45 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v11 1/8] t/t5505-remote: set default branch to main

On Wed, Oct 23, 2024 at 05:34:23PM +0200, Bence Ferdinandy wrote:
Show 9 quoted lines
> > Alternatively, you could set it in your copy of git.git's own
> > configuration in $GIT_DIR/config, so that it doesn't apply to work you
> > do outside of that repository.
> >
> > Either way, I'm going to avoid queueing this round since it is missing
> > the required Signed-off-by line.
>
> In that case I'll send a v12 with the S-o-b so the series can get back into
> circulation.
Thanks, will queue.

Thanks, Taylor

Bence Ferdinandy· Oct 22, 2024, 19:45 UTC · re: Bence Ferdinandy · lore

[PATCH v11 2/8] refs: atomically record overwritten ref in update_symref

When updating a symref with update_symref it's currently not possible to know for sure what was the previous value that was overwritten. Extend refs_update_symref under a new function name, to record the value after the ref has been locked if the caller of refs_update_symref_extended requests it via a new variable in the function call. Keep the original refs_update_symref function with the same signature, but now as a wrapper around refs_update_symref_extended.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    
    v5: - added before_target to reftables backend
        - added an extra safety check for transaction's existence in refs.c
    
    v6: - no change
    
    v7: - remove the whole before_target concept from the backends and
          handle checking it in refs.c instead (thanks Karthik)
        - rename the before_target to referent which is how the same concept
          is called in the backends
        - change commit prefix to be more in line with project standards
    
    v8: no change
    
    v9: - instead of adding parameters to refs_update_symref, rename what
          was in v8 as refs_update_symref_extended and make refs_update_symref
          a wrapper for that. This significantly reduces the number of files
          that need to be touched, and avoids adding a lot of dummy NULL-s
          in unrelated places.
    
    v10: no change
    
    v11: no change
 refs.c | 19 ++++++++++++++++---
 refs.h |  4 ++++
 2 files changed, 20 insertions(+), 3 deletions(-)
Show changes to 2 files +20 −3

refs.c, refs.h

diff --git a/refs.c b/refs.c
index 5f729ed412..24a4172cd2 100644
--- a/refs.c
+++ b/refs.c
@@ -2115,6 +2115,13 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
 
 int refs_update_symref(struct ref_store *refs, const char *ref,
 		       const char *target, const char *logmsg)
+{
+	return refs_update_symref_extended(refs, ref, target, logmsg, NULL);
+}
+
+int refs_update_symref_extended(struct ref_store *refs, const char *ref,
+		       const char *target, const char *logmsg,
+		       struct strbuf *referent)
 {
 	struct ref_transaction *transaction;
 	struct strbuf err = STRBUF_INIT;
@@ -2122,13 +2129,20 @@ int refs_update_symref(struct ref_store *refs, const char *ref,
 
 	transaction = ref_store_transaction_begin(refs, &err);
 	if (!transaction ||
-	    ref_transaction_update(transaction, ref, NULL, NULL,
+		ref_transaction_update(transaction, ref, NULL, NULL,
 				   target, NULL, REF_NO_DEREF,
 				   logmsg, &err) ||
-	    ref_transaction_commit(transaction, &err)) {
+		ref_transaction_prepare(transaction, &err)) {
 		ret = error("%s", err.buf);
+		goto cleanup;
 	}
+	if (referent)
+		refs_read_symbolic_ref(refs, ref, referent);
 
+	if (ref_transaction_commit(transaction, &err))
+		ret = error("%s", err.buf);
+
+cleanup:
 	strbuf_release(&err);
 	if (transaction)
 		ref_transaction_free(transaction);
@@ -2948,4 +2962,3 @@ int ref_update_expects_existing_old_ref(struct ref_update *update)
 	return (update->flags & REF_HAVE_OLD) &&
 		(!is_null_oid(&update->old_oid) || update->old_target);
 }
-
diff --git a/refs.h b/refs.h
index 108dfc93b3..259191a485 100644
--- a/refs.h
+++ b/refs.h
@@ -573,6 +573,10 @@ int refs_copy_existing_ref(struct ref_store *refs, const char *oldref,
 int refs_update_symref(struct ref_store *refs, const char *refname,
 		       const char *target, const char *logmsg);
 
+int refs_update_symref_extended(struct ref_store *refs, const char *refname,
+		       const char *target, const char *logmsg,
+		       struct strbuf *referent);
+
 enum action_on_err {
 	UPDATE_REFS_MSG_ON_ERR,
 	UPDATE_REFS_DIE_ON_ERR,
-- 
2.47.0.94.gb64850d498
Bence Ferdinandy· Oct 22, 2024, 19:45 UTC · re: Bence Ferdinandy · lore

[PATCH v11 4/8] remote set-head: better output for --auto

Currently, set-head --auto will print a message saying "remote/HEAD set to branch", which implies something was changed.

Change the output of --auto, so the output actually reflects what was done: a) set a previously unset HEAD, b) change HEAD because remote changed or c) no updates. As a fourth output, if HEAD is changed from a previous value that was not a remote branch, explicitly call attention to this fact.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v1-v2:
    
        was RFC in https://lore.kernel.org/git/20240910203835.2288291-1-bence@ferdinandy.com/
    
    v3:
    
        This patch was originally sent along when I thought set-head was
        going to be invoked by fetch, but the discussion on the RFC
        concluded that it should be not. This opened the possibility to make
        it more explicit.
    
        Note: although I feel both things the patch does are really just
        cosmetic, an argument could be made for breaking it into two, one
        for the no-op part and one for the --auto print update.
    
        Was sent in:
        https://lore.kernel.org/git/20240915221055.904107-1-bence@ferdinandy.com/
    
    v4:
        - changes are now handled atomically via the ref update transaction
        - outputs have changed along the lines of Junio's suggestion
        - minor refactor to set_head for improved legibility
    
    v5: - the minor refactor has been split out into its own patch
    
    v6: - fixed uninitialized prev_head
        - fixed case of unusual previous target
        - fixed a test that would have been actually broken at this patch
          (the output was only correct with the later update to fetch)
        - added 4 tests for the 4 output cases
    
    v7: - change commit prefix to be more in line with project standards
        - fixed tests to also work with the reftable backend
        - renamed report function, fixed style issue with checking buf len
        - fixed not releasing an strbuf
    
    v8: no change
    
    v9: - mark output strings in report_set_head_auto as translatable
        - rename buf_prev to b_local_head for consistency
        - use ${SQ} in tests instead of '\''
    
    v10: no change
    
    v11: no change
 builtin/remote.c  | 33 +++++++++++++++++++++++++++---
 t/t5505-remote.sh | 51 ++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 80 insertions(+), 4 deletions(-)
Show changes to 2 files +80 −4

builtin/remote.c, t/t5505-remote.sh

diff --git a/builtin/remote.c b/builtin/remote.c
index 1d68c5b2ba..108f1271d3 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1399,10 +1399,35 @@ static int show(int argc, const char **argv, const char *prefix)
 	return result;
 }
 
+static void report_set_head_auto(const char *remote, const char *head_name,
+			struct strbuf *b_local_head) {
+	struct strbuf buf_prefix = STRBUF_INIT;
+	const char *prev_head = NULL;
+
+	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
+	skip_prefix(b_local_head->buf, buf_prefix.buf, &prev_head);
+
+	if (prev_head && !strcmp(prev_head, head_name))
+		printf(_("'%s/HEAD' is unchanged and points to '%s'\n"),
+			remote, head_name);
+	else if (prev_head)
+		printf(_("'%s/HEAD' has changed from '%s' and now points to '%s'\n"),
+			remote, prev_head, head_name);
+	else if (!b_local_head->len)
+		printf(_("'%s/HEAD' is now created and points to '%s'\n"),
+			remote, head_name);
+	else
+		printf(_("'%s/HEAD' used to point to '%s' "
+			"(which is not a remote branch), but now points to '%s'\n"),
+			remote, b_local_head->buf, head_name);
+	strbuf_release(&buf_prefix);
+}
+
 static int set_head(int argc, const char **argv, const char *prefix)
 {
 	int i, opt_a = 0, opt_d = 0, result = 0;
-	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
+		b_local_head = STRBUF_INIT;
 	char *head_name = NULL;
 	struct ref_store *refs = get_main_ref_store(the_repository);
 
@@ -1445,15 +1470,17 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		/* make sure it's valid */
 		if (!refs_ref_exists(refs, b_remote_head.buf))
 			result |= error(_("Not a valid ref: %s"), b_remote_head.buf);
-		else if (refs_update_symref(refs, b_head.buf, b_remote_head.buf, "remote set-head"))
+		else if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
+					"remote set-head", &b_local_head))
 			result |= error(_("Could not setup %s"), b_head.buf);
 		else if (opt_a)
-			printf("%s/HEAD set to %s\n", argv[0], head_name);
+			report_set_head_auto(argv[0], head_name, &b_local_head);
 		free(head_name);
 	}
 
 	strbuf_release(&b_head);
 	strbuf_release(&b_remote_head);
+	strbuf_release(&b_local_head);
 	return result;
 }
 
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 9b50276646..0ea86d51a4 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -432,12 +432,51 @@ test_expect_success 'set-head --auto' '
 	)
 '
 
+test_expect_success 'set-head --auto detects creation' '
+	(
+		cd test &&
+		git symbolic-ref -d refs/remotes/origin/HEAD &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} is now created and points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects no change' '
+	(
+		cd test &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} is unchanged and points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects change' '
+	(
+		cd test &&
+		git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/ahead &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} has changed from ${SQ}ahead${SQ} and now points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects strange ref' '
+	(
+		cd test &&
+		git symbolic-ref refs/remotes/origin/HEAD refs/heads/main &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} used to point to ${SQ}refs/heads/main${SQ} (which is not a remote branch), but now points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
 test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
 	(
 		cd test &&
 		git fetch two "refs/heads/*:refs/remotes/two/*" &&
 		git remote set-head --auto two >output 2>&1 &&
-		echo "two/HEAD set to main" >expect &&
+		echo "${SQ}two/HEAD${SQ} is now created and points to ${SQ}main${SQ}" >expect &&
 		test_cmp expect output
 	)
 '
@@ -456,6 +495,16 @@ test_expect_success 'set-head explicit' '
 	)
 '
 
+test_expect_success 'set-head --auto reports change' '
+	(
+		cd test &&
+		git remote set-head origin side2 &&
+		git remote set-head --auto origin >output 2>&1 &&
+		echo "${SQ}origin/HEAD${SQ} has changed from ${SQ}side2${SQ} and now points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
 cat >test/expect <<EOF
 Pruning origin
 URL: $(pwd)/one
-- 
2.47.0.94.gb64850d498
Bence Ferdinandy· Oct 22, 2024, 19:45 UTC · re: Bence Ferdinandy · lore

[PATCH v11 5/8] refs: add TRANSACTION_CREATE_EXISTS error

Currently there is only one special error for transaction, for when there is a naming conflict, all other errors are dumped under a generic error. Add a new special error case for when the caller requests the reference to be updated only when it does not yet exist and the reference actually does exist.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    v5: no change
    v6: no change
    
    v7: - change commit prefix to be more in line with project standards
        - changed error checking to Karthik's suggestion
    
    v8: no change
    
    v9: - no change
    
    v10: no change
    
    v11: no change
 refs.h                  |  4 +++-
 refs/files-backend.c    | 24 ++++++++++++++++--------
 refs/reftable-backend.c |  6 ++++--
 3 files changed, 23 insertions(+), 11 deletions(-)
Show changes to 3 files +23 −11

refs.h, refs/files-backend.c, refs/reftable-backend.c

diff --git a/refs.h b/refs.h
index 259191a485..a5bc25442b 100644
--- a/refs.h
+++ b/refs.h
@@ -762,8 +762,10 @@ int ref_transaction_verify(struct ref_transaction *transaction,
 
 /* Naming conflict (for example, the ref names A and A/B conflict). */
 #define TRANSACTION_NAME_CONFLICT -1
+/* When only creation was requested, but the ref already exists. */
+#define TRANSACTION_CREATE_EXISTS -2
 /* All other errors. */
-#define TRANSACTION_GENERIC_ERROR -2
+#define TRANSACTION_GENERIC_ERROR -3
 
 /*
  * Perform the preparatory stages of committing `transaction`. Acquire
diff --git a/refs/files-backend.c b/refs/files-backend.c
index 0824c0b8a9..e743ec44b5 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -2502,14 +2502,18 @@ static int split_symref_update(struct ref_update *update,
 static int check_old_oid(struct ref_update *update, struct object_id *oid,
 			 struct strbuf *err)
 {
+	int ret = TRANSACTION_GENERIC_ERROR;
+
 	if (!(update->flags & REF_HAVE_OLD) ||
 		   oideq(oid, &update->old_oid))
 		return 0;
 
-	if (is_null_oid(&update->old_oid))
+	if (is_null_oid(&update->old_oid)) {
 		strbuf_addf(err, "cannot lock ref '%s': "
 			    "reference already exists",
 			    ref_update_original_update_refname(update));
+		ret = TRANSACTION_CREATE_EXISTS;
+	}
 	else if (is_null_oid(oid))
 		strbuf_addf(err, "cannot lock ref '%s': "
 			    "reference is missing but expected %s",
@@ -2522,7 +2526,7 @@ static int check_old_oid(struct ref_update *update, struct object_id *oid,
 			    oid_to_hex(oid),
 			    oid_to_hex(&update->old_oid));
 
-	return -1;
+	return ret;
 }
 
 /*
@@ -2602,9 +2606,11 @@ static int lock_ref_for_update(struct files_ref_store *refs,
 					ret = TRANSACTION_GENERIC_ERROR;
 					goto out;
 				}
-			} else if  (check_old_oid(update, &lock->old_oid, err)) {
-				ret = TRANSACTION_GENERIC_ERROR;
-				goto out;
+			} else {
+				ret = check_old_oid(update, &lock->old_oid, err);
+				if  (ret) {
+					goto out;
+				}
 			}
 		} else {
 			/*
@@ -2635,9 +2641,11 @@ static int lock_ref_for_update(struct files_ref_store *refs,
 				    update->old_target);
 			ret = TRANSACTION_GENERIC_ERROR;
 			goto out;
-		} else if  (check_old_oid(update, &lock->old_oid, err)) {
-			ret = TRANSACTION_GENERIC_ERROR;
-			goto out;
+		} else {
+			ret = check_old_oid(update, &lock->old_oid, err);
+			if  (ret) {
+				goto out;
+			}
 		}
 
 		/*
diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
index 3c6107c7ce..b3b5ce77dd 100644
--- a/refs/reftable-backend.c
+++ b/refs/reftable-backend.c
@@ -1206,10 +1206,13 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 				goto done;
 			}
 		} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {
-			if (is_null_oid(&u->old_oid))
+			ret = TRANSACTION_NAME_CONFLICT;
+			if (is_null_oid(&u->old_oid)) {
 				strbuf_addf(err, _("cannot lock ref '%s': "
 						   "reference already exists"),
 					    ref_update_original_update_refname(u));
+				ret = TRANSACTION_CREATE_EXISTS;
+			}
 			else if (is_null_oid(&current_oid))
 				strbuf_addf(err, _("cannot lock ref '%s': "
 						   "reference is missing but expected %s"),
@@ -1221,7 +1224,6 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 					    ref_update_original_update_refname(u),
 					    oid_to_hex(&current_oid),
 					    oid_to_hex(&u->old_oid));
-			ret = -1;
 			goto done;
 		}
 
-- 
2.47.0.94.gb64850d498
Bence Ferdinandy· Oct 22, 2024, 19:45 UTC · re: Bence Ferdinandy · lore

[PATCH v11 6/8] refs: add create_only option to refs_update_symref_extended

Allow the caller to specify that it only wants to update the symref if it does not already exist. Silently ignore the error from the transaction API if the symref already exists.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    v5: no change
    
    v6: - switched from bool to int for create_only
        - refactored logic in refs_update_symref with goto as layed out by
          Junio
    
    v7: - change commit prefix to be more in line with project standards
        - refactored code to accommodate changes in the first patch, but
          otherwise no change
    
    v8: no change
    
    v9: - no change (except for following through the
          refs_update_symref_extended refactoring)
    
    v10: no change
    
    v11: no change
 builtin/remote.c |  2 +-
 refs.c           | 32 +++++++++++++++++++++++---------
 refs.h           |  2 +-
 3 files changed, 25 insertions(+), 11 deletions(-)
Show changes to 3 files +25 −11

builtin/remote.c, refs.c, refs.h

diff --git a/builtin/remote.c b/builtin/remote.c
index 108f1271d3..b1eba75a2b 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1471,7 +1471,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		if (!refs_ref_exists(refs, b_remote_head.buf))
 			result |= error(_("Not a valid ref: %s"), b_remote_head.buf);
 		else if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
-					"remote set-head", &b_local_head))
+					"remote set-head", &b_local_head, 0))
 			result |= error(_("Could not setup %s"), b_head.buf);
 		else if (opt_a)
 			report_set_head_auto(argv[0], head_name, &b_local_head);
diff --git a/refs.c b/refs.c
index 24a4172cd2..093ee11ab0 100644
--- a/refs.c
+++ b/refs.c
@@ -2116,31 +2116,45 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
 int refs_update_symref(struct ref_store *refs, const char *ref,
 		       const char *target, const char *logmsg)
 {
-	return refs_update_symref_extended(refs, ref, target, logmsg, NULL);
+	return refs_update_symref_extended(refs, ref, target, logmsg, NULL, 0);
 }
 
 int refs_update_symref_extended(struct ref_store *refs, const char *ref,
 		       const char *target, const char *logmsg,
-		       struct strbuf *referent)
+		       struct strbuf *referent, int create_only)
 {
 	struct ref_transaction *transaction;
 	struct strbuf err = STRBUF_INIT;
-	int ret = 0;
+	int ret = 0, prepret = 0;
 
 	transaction = ref_store_transaction_begin(refs, &err);
-	if (!transaction ||
-		ref_transaction_update(transaction, ref, NULL, NULL,
-				   target, NULL, REF_NO_DEREF,
-				   logmsg, &err) ||
-		ref_transaction_prepare(transaction, &err)) {
+	if (!transaction) {
+	error_return:
 		ret = error("%s", err.buf);
 		goto cleanup;
 	}
+	if (create_only) {
+		if (ref_transaction_create(transaction, ref, NULL, target,
+					   REF_NO_DEREF, logmsg, &err))
+			goto error_return;
+		prepret = ref_transaction_prepare(transaction, &err);
+		if (prepret && prepret != TRANSACTION_CREATE_EXISTS)
+			goto error_return;
+	} else {
+		if (ref_transaction_update(transaction, ref, NULL, NULL,
+					   target, NULL, REF_NO_DEREF,
+					   logmsg, &err) ||
+			ref_transaction_prepare(transaction, &err))
+			goto error_return;
+	}
+
 	if (referent)
 		refs_read_symbolic_ref(refs, ref, referent);
 
+	if (prepret == TRANSACTION_CREATE_EXISTS)
+		goto cleanup;
 	if (ref_transaction_commit(transaction, &err))
-		ret = error("%s", err.buf);
+		goto error_return;
 
 cleanup:
 	strbuf_release(&err);
diff --git a/refs.h b/refs.h
index a5bc25442b..458582ebcf 100644
--- a/refs.h
+++ b/refs.h
@@ -575,7 +575,7 @@ int refs_update_symref(struct ref_store *refs, const char *refname,
 
 int refs_update_symref_extended(struct ref_store *refs, const char *refname,
 		       const char *target, const char *logmsg,
-		       struct strbuf *referent);
+		       struct strbuf *referent, int create_only);
 
 enum action_on_err {
 	UPDATE_REFS_MSG_ON_ERR,
-- 
2.47.0.94.gb64850d498
Bence Ferdinandy· Oct 22, 2024, 19:45 UTC · re: Bence Ferdinandy · lore

[PATCH v11 7/8] fetch: set remote/HEAD if it does not exist

If the user has remote/HEAD set already and it looks like it has changed on the server, then print a message, otherwise set it if we can. Silently pass if the user already has the same remote/HEAD set as reported by the server or if we encounter any errors along the way.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v3: - does not rely on remote set-head anymore so it only authenticates
        once
        - uses the new REF_CREATE_ONLY to atomically check if the ref exists
          and only write it if it doesn't
        - in all other cases the maximum it does is print a warning
    
    v4: - instead of the discarded REF_CREATE_ONLY, it uses the existing,
          but updated transaction api to request a silent create only
        - it now uses the atomic before_target to determine reporting
        - refactored for legibility
    
    v5: - instead of printing a not too useful message, it now fails
          silently, this in line with the objective to only set up
          remote/HEAD automatically if the right thing is trivial, for
          everything else there is remote set-head
        - fixed all failing tests
        - added two new tests, one for checking if remote/HEAD is set to the
          correct one, and one to test that we do not override remote/HEAD
          if it has changed on the server from what we have locally
    
    v6: - fixed style issues and unintended extra empty line
        - updated function call with bool to int from previous patch's
          change
        - removed calls to error(...) inherited from builtin/remote.c so we
          actually fail silently
        - set the test for remote set-head --auto to the correct value here,
          which was previously erronously set in the remote set-head patch
    
    v7: - no change
    
    v8: - changed logmsg in call to refs_update_symref from "remote
          set-head" to "fetch"
    
    v9: - follow through with refs_update_symref_extended
        - fix test errors uncovered by the new patch
    
    v10: no change
    
    v11: fixed some memory leaks
 builtin/fetch.c                  |  86 ++++++++++++
 t/t4207-log-decoration-colors.sh |   3 +-
 t/t5505-remote.sh                |  21 ++-
 t/t5510-fetch.sh                 | 229 ++++++++++++++++---------------
 t/t5512-ls-remote.sh             |   2 +
 t/t5514-fetch-multiple.sh        |  17 ++-
 t/t5516-fetch-push.sh            |   3 +-
 t/t5527-fetch-odd-refs.sh        |   3 +-
 t/t7900-maintenance.sh           |   3 +-
 t/t9210-scalar.sh                |   5 +-
 t/t9211-scalar-clone.sh          |   6 +-
 t/t9902-completion.sh            |  65 +++++++++
 12 files changed, 319 insertions(+), 124 deletions(-)
Show changes to 12 files +319 −124

builtin/fetch.c, t/t4207-log-decoration-colors.sh, t/t5505-remote.sh, t/t5510-fetch.sh, t/t5512-ls-remote.sh, t/t5514-fetch-multiple.sh, t/t5516-fetch-push.sh, t/t5527-fetch-odd-refs.sh, t/t7900-maintenance.sh, t/t9210-scalar.sh, t/t9211-scalar-clone.sh, t/t9902-completion.sh

diff --git a/builtin/fetch.c b/builtin/fetch.c
index d9027e4dc9..cadfd2407a 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1578,6 +1578,84 @@ static int backfill_tags(struct display_state *display_state,
 	return retcode;
 }
 
+static void report_set_head(const char *remote, const char *head_name,
+			struct strbuf *buf_prev) {
+	struct strbuf buf_prefix = STRBUF_INIT;
+	const char *prev_head = NULL;
+
+	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
+	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
+
+	if (prev_head && strcmp(prev_head, head_name)) {
+		printf("'HEAD' at '%s' has changed from '%s' to '%s'\n",
+			remote, prev_head, head_name);
+		printf("Run 'git remote set-head %s %s' to follow the change.\n",
+			remote, head_name);
+	}
+	strbuf_release(&buf_prefix);
+}
+
+static const char *strip_refshead(const char *name){
+	skip_prefix(name, "refs/heads/", &name);
+	return name;
+}
+
+static int set_head(const struct ref *remote_refs)
+{
+	int result = 0;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
+		b_local_head = STRBUF_INIT;
+	const char *remote = gtransport->remote->name;
+	char *head_name = NULL;
+	struct ref *ref, *matches;
+	struct ref *fetch_map = NULL, **fetch_map_tail = &fetch_map;
+	struct refspec_item refspec = {
+		.force = 0,
+		.pattern = 1,
+		.src = (char *) "refs/heads/*",
+		.dst = (char *) "refs/heads/*",
+	};
+	struct string_list heads = STRING_LIST_INIT_DUP;
+	struct ref_store *refs = get_main_ref_store(the_repository);
+
+	get_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);
+	matches = guess_remote_head(find_ref_by_name(remote_refs, "HEAD"),
+				    fetch_map, 1);
+	for (ref = matches; ref; ref = ref->next) {
+		string_list_append(&heads, strip_refshead(ref->name));
+	}
+
+
+	if (!heads.nr)
+		result = 1;
+	else if (heads.nr > 1)
+		result = 1;
+	else
+		head_name = xstrdup(heads.items[0].string);
+	if (head_name) {
+		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
+		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote, head_name);
+		/* make sure it's valid */
+		if (!refs_ref_exists(refs, b_remote_head.buf))
+			result = 1;
+		else if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
+					"fetch", &b_local_head, 1))
+			result = 1;
+		else
+			report_set_head(remote, head_name, &b_local_head);
+
+		free(head_name);
+	}
+
+	free_refs(fetch_map);
+	free_refs(matches);
+	string_list_clear(&heads, 0);
+	strbuf_release(&b_head);
+	strbuf_release(&b_local_head);
+	strbuf_release(&b_remote_head);
+	return result;
+}
+
 static int do_fetch(struct transport *transport,
 		    struct refspec *rs,
 		    const struct fetch_config *config)
@@ -1647,6 +1725,8 @@ static int do_fetch(struct transport *transport,
 				    "refs/tags/");
 	}
 
+	strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
+
 	if (must_list_refs) {
 		trace2_region_enter("fetch", "remote_refs", the_repository);
 		remote_refs = transport_get_remote_refs(transport,
@@ -1791,6 +1871,12 @@ static int do_fetch(struct transport *transport,
 				  "you need to specify exactly one branch with the --set-upstream option"));
 		}
 	}
+	if (set_head(remote_refs))
+		;
+		/*
+		 * Way too many cases where this can go wrong
+		 * so let's just fail silently for now.
+		 */
 
 cleanup:
 	if (retcode) {
diff --git a/t/t4207-log-decoration-colors.sh b/t/t4207-log-decoration-colors.sh
index 73ea9e5155..d55d22cb2f 100755
--- a/t/t4207-log-decoration-colors.sh
+++ b/t/t4207-log-decoration-colors.sh
@@ -59,7 +59,8 @@ ${c_reset}${c_tag}tag: ${c_reset}${c_tag}v1.0${c_reset}${c_commit}, \
 ${c_reset}${c_tag}tag: ${c_reset}${c_tag}B${c_reset}${c_commit})${c_reset} B
 ${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
 ${c_tag}tag: ${c_reset}${c_tag}A1${c_reset}${c_commit}, \
-${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit})${c_reset} A1
+${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit}, \
+${c_reset}${c_remoteBranch}other/HEAD${c_reset}${c_commit})${c_reset} A1
 	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
 ${c_stash}refs/stash${c_reset}${c_commit})${c_reset} On main: Changes to A.t
 	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 0ea86d51a4..4990d00209 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -74,7 +74,7 @@ test_expect_success 'add another remote' '
 		cd test &&
 		git remote add -f second ../two &&
 		tokens_match "origin second" "$(git remote)" &&
-		check_tracking_branch second main side another &&
+		check_tracking_branch second main side another HEAD &&
 		git for-each-ref "--format=%(refname)" refs/remotes |
 		sed -e "/^refs\/remotes\/origin\//d" \
 		    -e "/^refs\/remotes\/second\//d" >actual &&
@@ -476,7 +476,7 @@ test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
 		cd test &&
 		git fetch two "refs/heads/*:refs/remotes/two/*" &&
 		git remote set-head --auto two >output 2>&1 &&
-		echo "${SQ}two/HEAD${SQ} is now created and points to ${SQ}main${SQ}" >expect &&
+		echo "${SQ}two/HEAD${SQ} is unchanged and points to ${SQ}main${SQ}" >expect &&
 		test_cmp expect output
 	)
 '
@@ -764,8 +764,10 @@ test_expect_success 'reject --no-no-tags' '
 '
 
 cat >one/expect <<\EOF
+  apis/HEAD -> apis/main
   apis/main
   apis/side
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
@@ -783,11 +785,14 @@ test_expect_success 'update' '
 '
 
 cat >one/expect <<\EOF
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
+  manduca/HEAD -> manduca/main
   manduca/main
   manduca/side
+  megaloprepus/HEAD -> megaloprepus/main
   megaloprepus/main
   megaloprepus/side
 EOF
@@ -795,7 +800,7 @@ EOF
 test_expect_success 'update with arguments' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -827,10 +832,13 @@ test_expect_success 'update --prune' '
 '
 
 cat >one/expect <<-\EOF
+  apis/HEAD -> apis/main
   apis/main
   apis/side
+  manduca/HEAD -> manduca/main
   manduca/main
   manduca/side
+  megaloprepus/HEAD -> megaloprepus/main
   megaloprepus/main
   megaloprepus/side
 EOF
@@ -838,7 +846,7 @@ EOF
 test_expect_success 'update default' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -850,6 +858,7 @@ test_expect_success 'update default' '
 '
 
 cat >one/expect <<\EOF
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
@@ -858,7 +867,7 @@ EOF
 test_expect_success 'update default (overridden, with funny whitespace)' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -872,7 +881,7 @@ test_expect_success 'update default (overridden, with funny whitespace)' '
 test_expect_success 'update (with remotes.default defined)' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index 0890b9f61c..dfc8d748ba 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -75,6 +75,30 @@ test_expect_success "fetch test for-merge" '
 	cut -f -2 .git/FETCH_HEAD >actual &&
 	test_cmp expected actual'
 
+test_expect_success "fetch test remote HEAD" '
+	cd "$D" &&
+	cd two &&
+	git fetch &&
+	git rev-parse --verify refs/remotes/origin/HEAD &&
+	git rev-parse --verify refs/remotes/origin/main &&
+	head=$(git rev-parse refs/remotes/origin/HEAD) &&
+	branch=$(git rev-parse refs/remotes/origin/main) &&
+	test "z$head" = "z$branch"'
+
+test_expect_success "fetch test remote HEAD change" '
+	cd "$D" &&
+	cd two &&
+	git switch -c other &&
+	git push -u origin other &&
+	git rev-parse --verify refs/remotes/origin/HEAD &&
+	git rev-parse --verify refs/remotes/origin/main &&
+	git rev-parse --verify refs/remotes/origin/other &&
+	git remote set-head origin other &&
+	git fetch &&
+	head=$(git rev-parse refs/remotes/origin/HEAD) &&
+	branch=$(git rev-parse refs/remotes/origin/other) &&
+	test "z$head" = "z$branch"'
+
 test_expect_success 'fetch --prune on its own works as expected' '
 	cd "$D" &&
 	git clone . prune &&
@@ -478,7 +502,6 @@ test_expect_success 'unbundle 1' '
 	test_must_fail git fetch "$D/bundle1" main:main
 '
 
-
 test_expect_success 'bundle 1 has only 3 files ' '
 	cd "$D" &&
 	test_bundle_object_count bundle1 3
@@ -819,7 +842,7 @@ test_expect_success 'fetch from multiple configured URLs in single remote' '
 
 # configured prune tests
 
-set_config_tristate () {
+set_config_tristate() {
 	# var=$1 val=$2
 	case "$2" in
 	unset)
@@ -833,12 +856,12 @@ set_config_tristate () {
 	esac
 }
 
-test_configured_prune () {
+test_configured_prune() {
 	test_configured_prune_type "$@" "name"
 	test_configured_prune_type "$@" "link"
 }
 
-test_configured_prune_type () {
+test_configured_prune_type() {
 	fetch_prune=$1
 	remote_origin_prune=$2
 	fetch_prune_tags=$3
@@ -848,8 +871,7 @@ test_configured_prune_type () {
 	cmdline=$7
 	mode=$8
 
-	if test -z "$cmdline_setup"
-	then
+	if test -z "$cmdline_setup"; then
 		test_expect_success 'setup cmdline_setup variable for subsequent test' '
 			remote_url="file://$(git -C one config remote.origin.url)" &&
 			remote_fetch="$(git -C one config remote.origin.fetch)" &&
@@ -857,12 +879,10 @@ test_configured_prune_type () {
 		'
 	fi
 
-	if test "$mode" = 'link'
-	then
+	if test "$mode" = 'link'; then
 		new_cmdline=""
 
-		if test "$cmdline" = ""
-		then
+		if test "$cmdline" = ""; then
 			new_cmdline=$cmdline_setup
 		else
 			new_cmdline=$(perl -e '
@@ -873,10 +893,8 @@ test_configured_prune_type () {
 		fi
 
 		if test "$fetch_prune_tags" = 'true' ||
-		   test "$remote_origin_prune_tags" = 'true'
-		then
-			if ! printf '%s' "$cmdline\n" | grep -q refs/remotes/origin/
-			then
+			test "$remote_origin_prune_tags" = 'true'; then
+			if ! printf '%s' "$cmdline\n" | grep -q refs/remotes/origin/; then
 				new_cmdline="$new_cmdline refs/tags/*:refs/tags/*"
 			fi
 		fi
@@ -946,100 +964,100 @@ test_configured_prune_type () {
 # $7 git-fetch $cmdline:
 #
 #                     $1    $2    $3    $4    $5     $6     $7
-test_configured_prune unset unset unset unset kept   kept   ""
-test_configured_prune unset unset unset unset kept   kept   "--no-prune"
-test_configured_prune unset unset unset unset pruned kept   "--prune"
-test_configured_prune unset unset unset unset kept   pruned \
+test_configured_prune unset unset unset unset kept kept ""
+test_configured_prune unset unset unset unset kept kept "--no-prune"
+test_configured_prune unset unset unset unset pruned kept "--prune"
+test_configured_prune unset unset unset unset kept pruned \
 	"--prune origin refs/tags/*:refs/tags/*"
 test_configured_prune unset unset unset unset pruned pruned \
 	"--prune origin refs/tags/*:refs/tags/* +refs/heads/*:refs/remotes/origin/*"
 
-test_configured_prune false unset unset unset kept   kept   ""
-test_configured_prune false unset unset unset kept   kept   "--no-prune"
-test_configured_prune false unset unset unset pruned kept   "--prune"
+test_configured_prune false unset unset unset kept kept ""
+test_configured_prune false unset unset unset kept kept "--no-prune"
+test_configured_prune false unset unset unset pruned kept "--prune"
 
-test_configured_prune true  unset unset unset pruned kept   ""
-test_configured_prune true  unset unset unset pruned kept   "--prune"
-test_configured_prune true  unset unset unset kept   kept   "--no-prune"
+test_configured_prune true unset unset unset pruned kept ""
+test_configured_prune true unset unset unset pruned kept "--prune"
+test_configured_prune true unset unset unset kept kept "--no-prune"
 
-test_configured_prune unset false unset unset kept   kept   ""
-test_configured_prune unset false unset unset kept   kept   "--no-prune"
-test_configured_prune unset false unset unset pruned kept   "--prune"
+test_configured_prune unset false unset unset kept kept ""
+test_configured_prune unset false unset unset kept kept "--no-prune"
+test_configured_prune unset false unset unset pruned kept "--prune"
 
-test_configured_prune false false unset unset kept   kept   ""
-test_configured_prune false false unset unset kept   kept   "--no-prune"
-test_configured_prune false false unset unset pruned kept   "--prune"
-test_configured_prune false false unset unset kept   pruned \
+test_configured_prune false false unset unset kept kept ""
+test_configured_prune false false unset unset kept kept "--no-prune"
+test_configured_prune false false unset unset pruned kept "--prune"
+test_configured_prune false false unset unset kept pruned \
 	"--prune origin refs/tags/*:refs/tags/*"
 test_configured_prune false false unset unset pruned pruned \
 	"--prune origin refs/tags/*:refs/tags/* +refs/heads/*:refs/remotes/origin/*"
 
-test_configured_prune true  false unset unset kept   kept   ""
-test_configured_prune true  false unset unset pruned kept   "--prune"
-test_configured_prune true  false unset unset kept   kept   "--no-prune"
+test_configured_prune true false unset unset kept kept ""
+test_configured_prune true false unset unset pruned kept "--prune"
+test_configured_prune true false unset unset kept kept "--no-prune"
 
-test_configured_prune unset true  unset unset pruned kept   ""
-test_configured_prune unset true  unset unset kept   kept   "--no-prune"
-test_configured_prune unset true  unset unset pruned kept   "--prune"
+test_configured_prune unset true unset unset pruned kept ""
+test_configured_prune unset true unset unset kept kept "--no-prune"
+test_configured_prune unset true unset unset pruned kept "--prune"
 
-test_configured_prune false true  unset unset pruned kept   ""
-test_configured_prune false true  unset unset kept   kept   "--no-prune"
-test_configured_prune false true  unset unset pruned kept   "--prune"
+test_configured_prune false true unset unset pruned kept ""
+test_configured_prune false true unset unset kept kept "--no-prune"
+test_configured_prune false true unset unset pruned kept "--prune"
 
-test_configured_prune true  true  unset unset pruned kept   ""
-test_configured_prune true  true  unset unset pruned kept   "--prune"
-test_configured_prune true  true  unset unset kept   kept   "--no-prune"
-test_configured_prune true  true  unset unset kept   pruned \
+test_configured_prune true true unset unset pruned kept ""
+test_configured_prune true true unset unset pruned kept "--prune"
+test_configured_prune true true unset unset kept kept "--no-prune"
+test_configured_prune true true unset unset kept pruned \
 	"--prune origin refs/tags/*:refs/tags/*"
-test_configured_prune true  true  unset unset pruned pruned \
+test_configured_prune true true unset unset pruned pruned \
 	"--prune origin refs/tags/*:refs/tags/* +refs/heads/*:refs/remotes/origin/*"
 
 # --prune-tags on its own does nothing, needs --prune as well, same
 # for fetch.pruneTags without fetch.prune
-test_configured_prune unset unset unset unset kept kept     "--prune-tags"
-test_configured_prune unset unset true unset  kept kept     ""
-test_configured_prune unset unset unset true  kept kept     ""
+test_configured_prune unset unset unset unset kept kept "--prune-tags"
+test_configured_prune unset unset true unset kept kept ""
+test_configured_prune unset unset unset true kept kept ""
 
 # These will prune the tags
 test_configured_prune unset unset unset unset pruned pruned "--prune --prune-tags"
-test_configured_prune true  unset true  unset pruned pruned ""
-test_configured_prune unset true  unset true  pruned pruned ""
+test_configured_prune true unset true unset pruned pruned ""
+test_configured_prune unset true unset true pruned pruned ""
 
 # remote.<name>.pruneTags overrides fetch.pruneTags, just like
 # remote.<name>.prune overrides fetch.prune if set.
-test_configured_prune true  unset true unset pruned pruned  ""
-test_configured_prune false true  false true  pruned pruned ""
-test_configured_prune true  false true  false kept   kept   ""
+test_configured_prune true unset true unset pruned pruned ""
+test_configured_prune false true false true pruned pruned ""
+test_configured_prune true false true false kept kept ""
 
 # When --prune-tags is supplied it's ignored if an explicit refspec is
 # given, same for the configuration options.
 test_configured_prune unset unset unset unset pruned kept \
 	"--prune --prune-tags origin +refs/heads/*:refs/remotes/origin/*"
-test_configured_prune unset unset true  unset pruned kept \
+test_configured_prune unset unset true unset pruned kept \
 	"--prune origin +refs/heads/*:refs/remotes/origin/*"
-test_configured_prune unset unset unset true pruned  kept \
+test_configured_prune unset unset unset true pruned kept \
 	"--prune origin +refs/heads/*:refs/remotes/origin/*"
 
 # Pruning that also takes place if a file:// url replaces a named
 # remote. However, because there's no implicit
 # +refs/heads/*:refs/remotes/origin/* refspec and supplying it on the
 # command-line negates --prune-tags, the branches will not be pruned.
-test_configured_prune_type unset unset unset unset kept   kept   "origin --prune-tags" "name"
-test_configured_prune_type unset unset unset unset kept   kept   "origin --prune-tags" "link"
+test_configured_prune_type unset unset unset unset kept kept "origin --prune-tags" "name"
+test_configured_prune_type unset unset unset unset kept kept "origin --prune-tags" "link"
 test_configured_prune_type unset unset unset unset pruned pruned "origin --prune --prune-tags" "name"
-test_configured_prune_type unset unset unset unset kept   pruned "origin --prune --prune-tags" "link"
+test_configured_prune_type unset unset unset unset kept pruned "origin --prune --prune-tags" "link"
 test_configured_prune_type unset unset unset unset pruned pruned "--prune --prune-tags origin" "name"
-test_configured_prune_type unset unset unset unset kept   pruned "--prune --prune-tags origin" "link"
-test_configured_prune_type unset unset true  unset pruned pruned "--prune origin" "name"
-test_configured_prune_type unset unset true  unset kept   pruned "--prune origin" "link"
-test_configured_prune_type unset unset unset true  pruned pruned "--prune origin" "name"
-test_configured_prune_type unset unset unset true  kept   pruned "--prune origin" "link"
-test_configured_prune_type true  unset true  unset pruned pruned "origin" "name"
-test_configured_prune_type true  unset true  unset kept   pruned "origin" "link"
-test_configured_prune_type unset  true true  unset pruned pruned "origin" "name"
-test_configured_prune_type unset  true true  unset kept   pruned "origin" "link"
-test_configured_prune_type unset  true unset true  pruned pruned "origin" "name"
-test_configured_prune_type unset  true unset true  kept   pruned "origin" "link"
+test_configured_prune_type unset unset unset unset kept pruned "--prune --prune-tags origin" "link"
+test_configured_prune_type unset unset true unset pruned pruned "--prune origin" "name"
+test_configured_prune_type unset unset true unset kept pruned "--prune origin" "link"
+test_configured_prune_type unset unset unset true pruned pruned "--prune origin" "name"
+test_configured_prune_type unset unset unset true kept pruned "--prune origin" "link"
+test_configured_prune_type true unset true unset pruned pruned "origin" "name"
+test_configured_prune_type true unset true unset kept pruned "origin" "link"
+test_configured_prune_type unset true true unset pruned pruned "origin" "name"
+test_configured_prune_type unset true true unset kept pruned "origin" "link"
+test_configured_prune_type unset true unset true pruned pruned "origin" "name"
+test_configured_prune_type unset true unset true kept pruned "origin" "link"
 
 # When all remote.origin.fetch settings are deleted a --prune
 # --prune-tags still implicitly supplies refs/tags/*:refs/tags/* so
@@ -1137,8 +1155,7 @@ test_expect_success 'fetching with auto-gc does not lock up' '
 	)
 '
 
-for section in fetch transfer
-do
+for section in fetch transfer; do
 	test_expect_success "$section.hideRefs affects connectivity check" '
 		GIT_TRACE="$PWD"/trace git -c $section.hideRefs=refs -c \
 			$section.hideRefs="!refs/tags/" fetch &&
@@ -1157,21 +1174,23 @@ test_expect_success 'prepare source branch' '
 	test 3 -le $(wc -l <actual)
 '
 
-validate_store_type () {
+validate_store_type() {
 	git -C dest count-objects -v >actual &&
-	case "$store_type" in
-	packed)
-		grep "^count: 0$" actual ;;
-	loose)
-		grep "^packs: 0$" actual ;;
-	esac || {
+		case "$store_type" in
+		packed)
+			grep "^count: 0$" actual
+			;;
+		loose)
+			grep "^packs: 0$" actual
+			;;
+		esac || {
 		echo "store_type is $store_type"
 		cat actual
 		false
 	}
 }
 
-test_unpack_limit () {
+test_unpack_limit() {
 	store_type=$1
 
 	case "$store_type" in
@@ -1192,43 +1211,39 @@ test_unpack_limit () {
 test_unpack_limit packed
 test_unpack_limit loose
 
-setup_negotiation_tip () {
+setup_negotiation_tip() {
 	SERVER="$1"
 	URL="$2"
 	USE_PROTOCOL_V2="$3"
 
 	rm -rf "$SERVER" client trace &&
-	git init -b main "$SERVER" &&
-	test_commit -C "$SERVER" alpha_1 &&
-	test_commit -C "$SERVER" alpha_2 &&
-	git -C "$SERVER" checkout --orphan beta &&
-	test_commit -C "$SERVER" beta_1 &&
-	test_commit -C "$SERVER" beta_2 &&
-
-	git clone "$URL" client &&
-
-	if test "$USE_PROTOCOL_V2" -eq 1
-	then
-		git -C "$SERVER" config protocol.version 2 &&
-		git -C client config protocol.version 2
-	fi &&
-
-	test_commit -C "$SERVER" beta_s &&
-	git -C "$SERVER" checkout main &&
-	test_commit -C "$SERVER" alpha_s &&
-	git -C "$SERVER" tag -d alpha_1 alpha_2 beta_1 beta_2
+		git init -b main "$SERVER" &&
+		test_commit -C "$SERVER" alpha_1 &&
+		test_commit -C "$SERVER" alpha_2 &&
+		git -C "$SERVER" checkout --orphan beta &&
+		test_commit -C "$SERVER" beta_1 &&
+		test_commit -C "$SERVER" beta_2 &&
+		git clone "$URL" client &&
+		if test "$USE_PROTOCOL_V2" -eq 1; then
+			git -C "$SERVER" config protocol.version 2 &&
+				git -C client config protocol.version 2
+		fi &&
+		test_commit -C "$SERVER" beta_s &&
+		git -C "$SERVER" checkout main &&
+		test_commit -C "$SERVER" alpha_s &&
+		git -C "$SERVER" tag -d alpha_1 alpha_2 beta_1 beta_2
 }
 
-check_negotiation_tip () {
+check_negotiation_tip() {
 	# Ensure that {alpha,beta}_1 are sent as "have", but not {alpha_beta}_2
 	ALPHA_1=$(git -C client rev-parse alpha_1) &&
-	grep "fetch> have $ALPHA_1" trace &&
-	BETA_1=$(git -C client rev-parse beta_1) &&
-	grep "fetch> have $BETA_1" trace &&
-	ALPHA_2=$(git -C client rev-parse alpha_2) &&
-	! grep "fetch> have $ALPHA_2" trace &&
-	BETA_2=$(git -C client rev-parse beta_2) &&
-	! grep "fetch> have $BETA_2" trace
+		grep "fetch> have $ALPHA_1" trace &&
+		BETA_1=$(git -C client rev-parse beta_1) &&
+		grep "fetch> have $BETA_1" trace &&
+		ALPHA_2=$(git -C client rev-parse alpha_2) &&
+		! grep "fetch> have $ALPHA_2" trace &&
+		BETA_2=$(git -C client rev-parse beta_2) &&
+		! grep "fetch> have $BETA_2" trace
 }
 
 test_expect_success '--negotiation-tip limits "have" lines sent' '
diff --git a/t/t5512-ls-remote.sh b/t/t5512-ls-remote.sh
index 64b3491e4e..1b3865e154 100755
--- a/t/t5512-ls-remote.sh
+++ b/t/t5512-ls-remote.sh
@@ -293,6 +293,8 @@ test_expect_success 'ls-remote with filtered symref (refname)' '
 	cat >expect <<-EOF &&
 	ref: refs/heads/main	HEAD
 	$rev	HEAD
+	ref: refs/remotes/origin/main	refs/remotes/origin/HEAD
+	$rev	refs/remotes/origin/HEAD
 	EOF
 	git ls-remote --symref . HEAD >actual &&
 	test_cmp expect actual
diff --git a/t/t5514-fetch-multiple.sh b/t/t5514-fetch-multiple.sh
index 579872c258..e3482b27b2 100755
--- a/t/t5514-fetch-multiple.sh
+++ b/t/t5514-fetch-multiple.sh
@@ -45,14 +45,17 @@ test_expect_success setup '
 '
 
 cat > test/expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
   origin/HEAD -> origin/main
   origin/main
   origin/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -97,6 +100,7 @@ cat > expect << EOF
   origin/HEAD -> origin/main
   origin/main
   origin/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
@@ -112,8 +116,10 @@ test_expect_success 'git fetch --multiple (but only one remote)' '
 '
 
 cat > expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -141,7 +147,7 @@ test_expect_success 'git fetch --multiple (bad remote names)' '
 
 test_expect_success 'git fetch --all (skipFetchAll)' '
 	(cd test4 &&
-	 for b in $(git branch -r)
+	 for b in $(git branch -r | grep -v HEAD)
 	 do
 		git branch -r -d $b || exit 1
 	 done &&
@@ -153,11 +159,14 @@ test_expect_success 'git fetch --all (skipFetchAll)' '
 '
 
 cat > expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -165,7 +174,7 @@ EOF
 
 test_expect_success 'git fetch --multiple (ignoring skipFetchAll)' '
 	(cd test4 &&
-	 for b in $(git branch -r)
+	 for b in $(git branch -r | grep -v HEAD)
 	 do
 		git branch -r -d $b || exit 1
 	 done &&
@@ -221,14 +230,17 @@ test_expect_success 'git fetch --multiple --jobs=0 picks a default' '
 
 create_fetch_all_expect () {
 	cat >expect <<-\EOF
+	  one/HEAD -> one/main
 	  one/main
 	  one/side
 	  origin/HEAD -> origin/main
 	  origin/main
 	  origin/side
+	  three/HEAD -> three/main
 	  three/another
 	  three/main
 	  three/side
+	  two/HEAD -> two/main
 	  two/another
 	  two/main
 	  two/side
@@ -265,6 +277,7 @@ test_expect_success 'git fetch (fetch all remotes with fetch.all = true)' '
 
 create_fetch_one_expect () {
 	cat >expect <<-\EOF
+	  one/HEAD -> one/main
 	  one/main
 	  one/side
 	  origin/HEAD -> origin/main
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 331778bd42..5a051aa0c7 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1395,7 +1395,8 @@ test_expect_success 'fetch follows tags by default' '
 		git tag -m "annotated" tag &&
 		git for-each-ref >tmp1 &&
 		sed -n "p; s|refs/heads/main$|refs/remotes/origin/main|p" tmp1 |
-		sort -k 3 >../expect
+		sed -n "p; s|refs/heads/main$|refs/remotes/origin/HEAD|p"  |
+		sort -k 4 >../expect
 	) &&
 	test_when_finished "rm -rf dst" &&
 	git init dst &&
diff --git a/t/t5527-fetch-odd-refs.sh b/t/t5527-fetch-odd-refs.sh
index 98ece27c6a..d3996af6ee 100755
--- a/t/t5527-fetch-odd-refs.sh
+++ b/t/t5527-fetch-odd-refs.sh
@@ -52,7 +52,8 @@ test_expect_success LONG_REF 'fetch handles extremely long refname' '
 	long
 	main
 	EOF
-	git for-each-ref --format="%(subject)" refs/remotes/long >actual &&
+	git for-each-ref --format="%(subject)" refs/remotes/long \
+		--exclude=refs/remotes/long/HEAD >actual &&
 	test_cmp expect actual
 '
 
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index a66d0e089d..88331d1ceb 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -329,7 +329,8 @@ test_expect_success 'incremental-repack task' '
 
 	# Delete refs that have not been repacked in these packs.
 	git for-each-ref --format="delete %(refname)" \
-		refs/prefetch refs/tags refs/remotes >refs &&
+		refs/prefetch refs/tags refs/remotes \
+		--exclude=refs/remotes/*/HEAD >refs &&
 	git update-ref --stdin <refs &&
 
 	# Replace the object directory with this pack layout.
diff --git a/t/t9210-scalar.sh b/t/t9210-scalar.sh
index a30b2c9f70..2237844550 100755
--- a/t/t9210-scalar.sh
+++ b/t/t9210-scalar.sh
@@ -151,7 +151,8 @@ test_expect_success 'scalar clone' '
 			"$(pwd)" &&
 
 		git for-each-ref --format="%(refname)" refs/remotes/origin/ >actual &&
-		echo "refs/remotes/origin/parallel" >expect &&
+		echo "refs/remotes/origin/HEAD" >>expect &&
+		echo "refs/remotes/origin/parallel" >>expect &&
 		test_cmp expect actual &&
 
 		test_path_is_missing 1/2 &&
@@ -220,7 +221,7 @@ test_expect_success 'scalar reconfigure --all with includeIf.onbranch' '
 	done
 '
 
- test_expect_success 'scalar reconfigure --all with detached HEADs' '
+test_expect_success 'scalar reconfigure --all with detached HEADs' '
 	repos="two three four" &&
 	for num in $repos
 	do
diff --git a/t/t9211-scalar-clone.sh b/t/t9211-scalar-clone.sh
index c16ea67c1d..d9cb6b9a3e 100755
--- a/t/t9211-scalar-clone.sh
+++ b/t/t9211-scalar-clone.sh
@@ -32,7 +32,7 @@ test_expect_success 'set up repository to clone' '
 	)
 '
 
-cleanup_clone () {
+cleanup_clone() {
 	rm -rf "$1"
 }
 
@@ -128,7 +128,7 @@ test_expect_success '--single-branch clones HEAD only' '
 	(
 		cd $enlistment/src &&
 		git for-each-ref refs/remotes/origin >out &&
-		test_line_count = 1 out &&
+		test_line_count = 2 out &&
 		grep "refs/remotes/origin/base" out
 	) &&
 
@@ -142,7 +142,7 @@ test_expect_success '--no-single-branch clones all branches' '
 	(
 		cd $enlistment/src &&
 		git for-each-ref refs/remotes/origin >out &&
-		test_line_count = 2 out &&
+		test_line_count = 3 out &&
 		grep "refs/remotes/origin/base" out &&
 		grep "refs/remotes/origin/parallel" out
 	) &&
diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh
index cc6aa9f0cd..b663c4609e 100755
--- a/t/t9902-completion.sh
+++ b/t/t9902-completion.sh
@@ -658,6 +658,7 @@ test_expect_success '__git_refs - simple' '
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	matching-tag
@@ -673,6 +674,7 @@ test_expect_success '__git_refs - full refs' '
 	cat >expected <<-EOF &&
 	refs/heads/main
 	refs/heads/matching-branch
+	refs/remotes/other/HEAD
 	refs/remotes/other/branch-in-other
 	refs/remotes/other/main-in-other
 	refs/tags/matching-tag
@@ -729,6 +731,7 @@ test_expect_success '__git_refs - remote on local file system - full refs' '
 test_expect_success '__git_refs - configured remote' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -756,6 +759,7 @@ test_expect_success '__git_refs - configured remote - full refs' '
 test_expect_success '__git_refs - configured remote - repo given on the command line' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -787,6 +791,7 @@ test_expect_success '__git_refs - configured remote - full refs - repo given on
 test_expect_success '__git_refs - configured remote - remote name matches a directory' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -875,12 +880,14 @@ test_expect_success '__git_refs - unique remote branches for git checkout DWIMer
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/ambiguous
 	other/branch-in-other
 	other/main-in-other
 	remote/ambiguous
 	remote/branch-in-remote
 	matching-tag
+	HEAD
 	branch-in-other
 	branch-in-remote
 	main-in-other
@@ -904,6 +911,7 @@ test_expect_success '__git_refs - after --opt=' '
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	matching-tag
@@ -919,6 +927,7 @@ test_expect_success '__git_refs - after --opt= - full refs' '
 	cat >expected <<-EOF &&
 	refs/heads/main
 	refs/heads/matching-branch
+	refs/remotes/other/HEAD
 	refs/remotes/other/branch-in-other
 	refs/remotes/other/main-in-other
 	refs/tags/matching-tag
@@ -935,6 +944,7 @@ test_expect_success '__git refs - excluding refs' '
 	^HEAD
 	^main
 	^matching-branch
+	^other/HEAD
 	^other/branch-in-other
 	^other/main-in-other
 	^matching-tag
@@ -950,6 +960,7 @@ test_expect_success '__git refs - excluding full refs' '
 	cat >expected <<-EOF &&
 	^refs/heads/main
 	^refs/heads/matching-branch
+	^refs/remotes/other/HEAD
 	^refs/remotes/other/branch-in-other
 	^refs/remotes/other/main-in-other
 	^refs/tags/matching-tag
@@ -975,6 +986,7 @@ test_expect_success '__git_refs - do not filter refs unless told so' '
 	main
 	matching-branch
 	matching/branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	other/matching/branch-in-other
@@ -1095,6 +1107,7 @@ test_expect_success '__git_complete_refs - simple' '
 	HEAD Z
 	main Z
 	matching-branch Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	matching-tag Z
@@ -1123,6 +1136,7 @@ test_expect_success '__git_complete_refs - matching' '
 test_expect_success '__git_complete_refs - remote' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main-in-other Z
 	EOF
@@ -1139,9 +1153,11 @@ test_expect_success '__git_complete_refs - track' '
 	HEAD Z
 	main Z
 	matching-branch Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	matching-tag Z
+	HEAD Z
 	branch-in-other Z
 	main-in-other Z
 	EOF
@@ -1184,6 +1200,7 @@ test_expect_success '__git_complete_refs - suffix' '
 	HEAD.
 	main.
 	matching-branch.
+	other/HEAD.
 	other/branch-in-other.
 	other/main-in-other.
 	matching-tag.
@@ -1199,6 +1216,7 @@ test_expect_success '__git_complete_refs - suffix' '
 test_expect_success '__git_complete_fetch_refspecs - simple' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	HEAD:HEAD Z
+	HEAD:HEAD Z
 	branch-in-other:branch-in-other Z
 	main-in-other:main-in-other Z
 	EOF
@@ -1225,6 +1243,7 @@ test_expect_success '__git_complete_fetch_refspecs - matching' '
 test_expect_success '__git_complete_fetch_refspecs - prefix' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	+HEAD:HEAD Z
+	+HEAD:HEAD Z
 	+branch-in-other:branch-in-other Z
 	+main-in-other:main-in-other Z
 	EOF
@@ -1289,6 +1308,7 @@ test_expect_success '__git_complete_worktree_paths with -C' '
 
 test_expect_success 'git switch - with no options, complete local branches and unique remote branch names for DWIM logic' '
 	test_completion "git switch " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1435,11 +1455,13 @@ test_expect_success 'git-bisect - existing view subcommand is recognized and ena
 test_expect_success 'git checkout - completes refs and unique remote branches for DWIM' '
 	test_completion "git checkout " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1461,6 +1483,7 @@ test_expect_success 'git switch - with GIT_COMPLETION_CHECKOUT_NO_GUESS=1, compl
 
 test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_GUESS=1, complete local branches and unique remote names for DWIM logic' '
 	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git switch --guess " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1470,6 +1493,7 @@ test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_G
 
 test_expect_success 'git switch - a later --guess overrides previous --no-guess, complete local and remote unique branches for DWIM' '
 	test_completion "git switch --no-guess --guess " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1490,6 +1514,7 @@ test_expect_success 'git checkout - with GIT_COMPLETION_NO_GUESS=1 only complete
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1498,11 +1523,13 @@ test_expect_success 'git checkout - with GIT_COMPLETION_NO_GUESS=1 only complete
 test_expect_success 'git checkout - --guess overrides GIT_COMPLETION_NO_GUESS=1, complete refs and unique remote branches for DWIM' '
 	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git checkout --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1514,6 +1541,7 @@ test_expect_success 'git checkout - with --no-guess, only completes refs' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1522,11 +1550,13 @@ test_expect_success 'git checkout - with --no-guess, only completes refs' '
 test_expect_success 'git checkout - a later --guess overrides previous --no-guess, complete refs and unique remote branches for DWIM' '
 	test_completion "git checkout --no-guess --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1538,6 +1568,7 @@ test_expect_success 'git checkout - a later --no-guess overrides previous --gues
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1550,6 +1581,7 @@ test_expect_success 'git checkout - with checkout.guess = false, only completes
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1559,11 +1591,13 @@ test_expect_success 'git checkout - with checkout.guess = true, completes refs a
 	test_config checkout.guess true &&
 	test_completion "git checkout " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1573,11 +1607,13 @@ test_expect_success 'git checkout - a later --guess overrides previous checkout.
 	test_config checkout.guess false &&
 	test_completion "git checkout --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1590,6 +1626,7 @@ test_expect_success 'git checkout - a later --no-guess overrides previous checko
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1601,6 +1638,7 @@ test_expect_success 'git switch - with --detach, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1612,6 +1650,7 @@ test_expect_success 'git checkout - with --detach, complete only references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1783,6 +1822,7 @@ test_expect_success 'git switch - with -d, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1794,6 +1834,7 @@ test_expect_success 'git checkout - with -d, complete only references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1801,10 +1842,12 @@ test_expect_success 'git checkout - with -d, complete only references' '
 
 test_expect_success 'git switch - with --track, complete only remote branches' '
 	test_completion "git switch --track " <<-\EOF &&
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
 	test_completion "git switch -t " <<-\EOF
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1812,10 +1855,12 @@ test_expect_success 'git switch - with --track, complete only remote branches' '
 
 test_expect_success 'git checkout - with --track, complete only remote branches' '
 	test_completion "git checkout --track " <<-\EOF &&
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
 	test_completion "git checkout -t " <<-\EOF
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1834,6 +1879,7 @@ test_expect_success 'git checkout - with --no-track, complete only local referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1845,6 +1891,7 @@ test_expect_success 'git switch - with -c, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1856,6 +1903,7 @@ test_expect_success 'git switch - with -C, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1867,6 +1915,7 @@ test_expect_success 'git switch - with -c and --track, complete all references'
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1878,6 +1927,7 @@ test_expect_success 'git switch - with -C and --track, complete all references'
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1889,6 +1939,7 @@ test_expect_success 'git switch - with -c and --no-track, complete all reference
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1900,6 +1951,7 @@ test_expect_success 'git switch - with -C and --no-track, complete all reference
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1911,6 +1963,7 @@ test_expect_success 'git checkout - with -b, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1922,6 +1975,7 @@ test_expect_success 'git checkout - with -B, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1933,6 +1987,7 @@ test_expect_success 'git checkout - with -b and --track, complete all references
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1944,6 +1999,7 @@ test_expect_success 'git checkout - with -B and --track, complete all references
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1955,6 +2011,7 @@ test_expect_success 'git checkout - with -b and --no-track, complete all referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1966,6 +2023,7 @@ test_expect_success 'git checkout - with -B and --no-track, complete all referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1973,6 +2031,7 @@ test_expect_success 'git checkout - with -B and --no-track, complete all referen
 
 test_expect_success 'git switch - for -c, complete local branches and unique remote branches' '
 	test_completion "git switch -c " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1982,6 +2041,7 @@ test_expect_success 'git switch - for -c, complete local branches and unique rem
 
 test_expect_success 'git switch - for -C, complete local branches and unique remote branches' '
 	test_completion "git switch -C " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2019,6 +2079,7 @@ test_expect_success 'git switch - for -C with --no-track, complete local branche
 
 test_expect_success 'git checkout - for -b, complete local branches and unique remote branches' '
 	test_completion "git checkout -b " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2028,6 +2089,7 @@ test_expect_success 'git checkout - for -b, complete local branches and unique r
 
 test_expect_success 'git checkout - for -B, complete local branches and unique remote branches' '
 	test_completion "git checkout -B " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2065,6 +2127,7 @@ test_expect_success 'git checkout - for -B with --no-track, complete local branc
 
 test_expect_success 'git switch - with --orphan completes local branch names and unique remote branch names' '
 	test_completion "git switch --orphan " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2080,6 +2143,7 @@ test_expect_success 'git switch - --orphan with branch already provided complete
 
 test_expect_success 'git checkout - with --orphan completes local branch names and unique remote branch names' '
 	test_completion "git checkout --orphan " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2093,6 +2157,7 @@ test_expect_success 'git checkout - --orphan with branch already provided comple
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
-- 
2.47.0.94.gb64850d498
Bence Ferdinandy· Oct 22, 2024, 19:45 UTC · re: Bence Ferdinandy · lore

[PATCH v11 8/8] fetch set_head: handle mirrored bare repositories

When adding a remote to bare repository with "git remote add --mirror",
running fetch will fail to update HEAD to the remote's HEAD, since it
does not know how to handle bare repositories. On the other hand HEAD
already has content, since "git init --bare" has already set HEAD to
whatever is the default branch set for the user. Unless this - by chance
- is the same as the remote's HEAD, HEAD will be pointing to a bad
symref. Teach set_head to handle bare repositories, by overwriting HEAD
so it mirrors the remote's HEAD.

Note, that in this case overriding the local HEAD reference is necessary, since HEAD will exist before fetch can be run, but this should not be an issue, since the whole purpose of --mirror is to be an exact mirror of the remote, so following any changes to HEAD makes sense.

Also note, that although "git remote set-head" also fails when trying to update the remote's locally tracked HEAD in a mirrored bare repository, the usage of the command does not make much sense after this patch: fetch will update the remote HEAD correctly, and setting it manually to something else is antithetical to the concept of mirroring.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v10: - new patch
         - handles the issue discovered in
           https://lore.kernel.org/git/D4ZAELFWJMKN.S88LJ6YK31LZ@ferdinandy.com/T/
    
    v11: no change
 builtin/fetch.c   | 16 ++++++++++++----
 t/t5505-remote.sh | 10 ++++++++++
 2 files changed, 22 insertions(+), 4 deletions(-)
Show changes to 2 files +22 −4

builtin/fetch.c, t/t5505-remote.sh

diff --git a/builtin/fetch.c b/builtin/fetch.c
index cadfd2407a..8af3efd6ac 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1633,14 +1633,22 @@ static int set_head(const struct ref *remote_refs)
 	else
 		head_name = xstrdup(heads.items[0].string);
 	if (head_name) {
-		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
-		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote, head_name);
+		int is_bare = is_bare_repository();
+		if (is_bare) {
+			strbuf_addstr(&b_head, "HEAD");
+			strbuf_addf(&b_remote_head, "refs/heads/%s", head_name);
+		} else {
+			strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
+			strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote, head_name);
+		}
 		/* make sure it's valid */
-		if (!refs_ref_exists(refs, b_remote_head.buf))
+		if (!is_bare && !refs_ref_exists(refs, b_remote_head.buf)) {
 			result = 1;
+		}
 		else if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
-					"fetch", &b_local_head, 1))
+					"fetch", &b_local_head, !is_bare)) {
 			result = 1;
+		}
 		else
 			report_set_head(remote, head_name, &b_local_head);
 
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 4990d00209..dfa78f3e8d 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -545,6 +545,16 @@ test_expect_success 'add --mirror && prune' '
 	)
 '
 
+test_expect_success 'add --mirror setting HEAD' '
+	mkdir headmirror &&
+	(
+		cd headmirror &&
+		git init --bare -b notmain &&
+		git remote add --mirror -f origin ../one &&
+		test "$(git symbolic-ref HEAD)" = "refs/heads/main"
+	)
+'
+
 test_expect_success 'add --mirror=fetch' '
 	mkdir mirror-fetch &&
 	git init -b main mirror-fetch/parent &&
-- 
2.47.0.94.gb64850d498
Bence Ferdinandy· Oct 22, 2024, 19:45 UTC · re: Bence Ferdinandy · lore

[PATCH v11 3/8] remote set-head: refactor for readability

Rename buf and buf2 to something more explanatory.

Instead of calling get_main_ref_store(the_repository) multiple times, call it once and store in a new refs variable. Although this change probably offers some performance benefits, the main purpose is to shorten the line lengths of function calls using this variable.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v5: new patch (split from the next patch as a preparatory step)
    
    v6: no change
    
    v7: - change commit prefix to be more in line with project standards
    
    v8: no change
    
    v9: - further improve readability by renaming buf, and buf2 consistently
          with how patch 6 was already done
    
    v10: no change
    
    v11: no change
 builtin/remote.c | 23 ++++++++++++-----------
 1 file changed, 12 insertions(+), 11 deletions(-)
Show changes to builtin/remote.c +12 −11
diff --git a/builtin/remote.c b/builtin/remote.c
index 76670ddd8b..1d68c5b2ba 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1402,8 +1402,9 @@ static int show(int argc, const char **argv, const char *prefix)
 static int set_head(int argc, const char **argv, const char *prefix)
 {
 	int i, opt_a = 0, opt_d = 0, result = 0;
-	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT;
 	char *head_name = NULL;
+	struct ref_store *refs = get_main_ref_store(the_repository);
 
 	struct option options[] = {
 		OPT_BOOL('a', "auto", &opt_a,
@@ -1415,7 +1416,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 	argc = parse_options(argc, argv, prefix, options,
 			     builtin_remote_sethead_usage, 0);
 	if (argc)
-		strbuf_addf(&buf, "refs/remotes/%s/HEAD", argv[0]);
+		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", argv[0]);
 
 	if (!opt_a && !opt_d && argc == 2) {
 		head_name = xstrdup(argv[1]);
@@ -1434,25 +1435,25 @@ static int set_head(int argc, const char **argv, const char *prefix)
 			head_name = xstrdup(states.heads.items[0].string);
 		free_remote_ref_states(&states);
 	} else if (opt_d && !opt_a && argc == 1) {
-		if (refs_delete_ref(get_main_ref_store(the_repository), NULL, buf.buf, NULL, REF_NO_DEREF))
-			result |= error(_("Could not delete %s"), buf.buf);
+		if (refs_delete_ref(refs, NULL, b_head.buf, NULL, REF_NO_DEREF))
+			result |= error(_("Could not delete %s"), b_head.buf);
 	} else
 		usage_with_options(builtin_remote_sethead_usage, options);
 
 	if (head_name) {
-		strbuf_addf(&buf2, "refs/remotes/%s/%s", argv[0], head_name);
+		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", argv[0], head_name);
 		/* make sure it's valid */
-		if (!refs_ref_exists(get_main_ref_store(the_repository), buf2.buf))
-			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head"))
-			result |= error(_("Could not setup %s"), buf.buf);
+		if (!refs_ref_exists(refs, b_remote_head.buf))
+			result |= error(_("Not a valid ref: %s"), b_remote_head.buf);
+		else if (refs_update_symref(refs, b_head.buf, b_remote_head.buf, "remote set-head"))
+			result |= error(_("Could not setup %s"), b_head.buf);
 		else if (opt_a)
 			printf("%s/HEAD set to %s\n", argv[0], head_name);
 		free(head_name);
 	}
 
-	strbuf_release(&buf);
-	strbuf_release(&buf2);
+	strbuf_release(&b_head);
+	strbuf_release(&b_remote_head);
 	return result;
 }
 
-- 
2.47.0.94.gb64850d498
Bence Ferdinandy· Oct 23, 2024, 15:36 UTC · re: Bence Ferdinandy · lore

[PATCH v12 0/8] set-head/fetch remote/HEAD updates

v12: added missing S-o-b
v11: fixed leaks found by running tests after `make SANITIZE=leak`
    green pipeline:
    https://github.com/ferdinandyb/git/actions/runs/11466803523

v10: added a new patch (8/8) on top of v9 to handle HEAD in mirrored bare repositories, rest of the patches are unchanged

v9:
the two most notable changes are
- the new patch 1/7 which address
  https://lore.kernel.org/git/Zw8IKyPkG0Hr6%2F5t@nand.local/, but see
  https://lore.kernel.org/git/D4ZAELFWJMKN.S88LJ6YK31LZ@ferdinandy.com/
- the refs_update_symref -> refs_update_symref_extended change in 2/7,
  reflecting on Phillip's comments (see
  https://lore.kernel.org/git/a7cb48e5-d8ba-44c1-9dbe-d1e8f8a63e3c@gmail.com/)
Bence Ferdinandy (8):
  t/t5505-remote: set default branch to main
  refs: atomically record overwritten ref in update_symref
  remote set-head: refactor for readability
  remote set-head: better output for --auto
  refs: add TRANSACTION_CREATE_EXISTS error
  refs: add create_only option to refs_update_symref_extended
  fetch: set remote/HEAD if it does not exist
  fetch set_head: handle mirrored bare repositories
 builtin/fetch.c                  |  94 +++++++++++++
 builtin/remote.c                 |  52 +++++--
 refs.c                           |  41 +++++-
 refs.h                           |   8 +-
 refs/files-backend.c             |  24 ++--
 refs/reftable-backend.c          |   6 +-
 t/t4207-log-decoration-colors.sh |   3 +-
 t/t5505-remote.sh                |  83 ++++++++++-
 t/t5510-fetch.sh                 | 229 ++++++++++++++++---------------
 t/t5512-ls-remote.sh             |   2 +
 t/t5514-fetch-multiple.sh        |  17 ++-
 t/t5516-fetch-push.sh            |   3 +-
 t/t5527-fetch-odd-refs.sh        |   3 +-
 t/t7900-maintenance.sh           |   3 +-
 t/t9210-scalar.sh                |   5 +-
 t/t9211-scalar-clone.sh          |   6 +-
 t/t9902-completion.sh            |  65 +++++++++
 17 files changed, 490 insertions(+), 154 deletions(-)
-- 
2.47.0.94.g1247fb88fd.dirty
Bence Ferdinandy· Oct 23, 2024, 15:36 UTC · re: Bence Ferdinandy · lore

[PATCH v12 1/8] t/t5505-remote: set default branch to main

Consider the bare repository called "mirror" in the test. Running `git remote add --mirror -f origin ../one` will not change HEAD, consequently if init.defaultBranch is not the same as what HEAD in the remote ("one"), HEAD in "mirror" will be pointing to a non-existent reference. Hence if "mirror" is used as a remote by yet another repository, ls-remote will not show HEAD. On the other hand, if init.defaultBranch happens to match HEAD in "one", then ls-remote will show HEAD.

Since the CI globally exports GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main, there's a drift between how the test repositories are set up in the CI and during local testing. This issue does not manifest currently, as the test does not do any remote HEAD manipulation where this would come up, but should such things be added, a locally passing test would break the CI vice-versa.

Set GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main in the test to be consistent with the CI.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v9: - new patch
        - a bandaid for the CI issue noticed by Taylor (cf:
          https://lore.kernel.org/git/Zw8IKyPkG0Hr6%2F5t@nand.local/), but
          see
          https://lore.kernel.org/git/D4ZAELFWJMKN.S88LJ6YK31LZ@ferdinandy.com/ for the root cause in detail
    
    v10: no change
    
    v11: no change
    
    v12: added forgotten sign-off
 t/t5505-remote.sh | 3 +++
 1 file changed, 3 insertions(+)
Show changes to t/t5505-remote.sh +3 −0
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 532035933f..9b50276646 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -2,6 +2,9 @@
 
 test_description='git remote porcelain-ish'
 
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
 TEST_PASSES_SANITIZE_LEAK=true
 . ./test-lib.sh
 
-- 
2.47.0.94.g1247fb88fd.dirty
Junio C Hamano· Nov 15, 2024, 04:52 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v12 1/8] t/t5505-remote: set default branch to main

Bence Ferdinandy <bence@ferdinandy.com> writes:
Show 7 quoted lines
> Consider the bare repository called "mirror" in the test.  Running `git
> remote add --mirror -f origin ../one` will not change HEAD, consequently
> if init.defaultBranch is not the same as what HEAD in the remote
> ("one"), HEAD in "mirror" will be pointing to a non-existent reference.
> Hence if "mirror" is used as a remote by yet another repository,
> ls-remote will not show HEAD. On the other hand, if init.defaultBranch
> happens to match HEAD in "one", then ls-remote will show HEAD.

Making sure that the hardcoded (in the Git binary) default branch name would not affect the outcome of the test is a good thing to do. I like the patch text, but ...

Show 6 quoted lines
> Since the CI globally exports GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main,
> there's a drift between how the test repositories are set up in the CI
> and during local testing. This issue does not manifest currently, as the
> test does not do any remote HEAD manipulation where this would come up,
> but should such things be added, a locally passing test would break the
> CI vice-versa.

... this description may not quite be accurate. Don't some jobs of CI use 'main' while the rest use 'master'?

Bence Ferdinandy· Nov 15, 2024, 22:03 UTC · re: Junio C Hamano · lore

Re: [PATCH v12 1/8] t/t5505-remote: set default branch to main

On Fri Nov 15, 2024 at 05:52, Junio C Hamano <gitster@pobox.com> wrote:
Show 23 quoted lines
> Bence Ferdinandy <bence@ferdinandy.com> writes:
>
>> Consider the bare repository called "mirror" in the test.  Running `git
>> remote add --mirror -f origin ../one` will not change HEAD, consequently
>> if init.defaultBranch is not the same as what HEAD in the remote
>> ("one"), HEAD in "mirror" will be pointing to a non-existent reference.
>> Hence if "mirror" is used as a remote by yet another repository,
>> ls-remote will not show HEAD. On the other hand, if init.defaultBranch
>> happens to match HEAD in "one", then ls-remote will show HEAD.
>
> Making sure that the hardcoded (in the Git binary) default branch
> name would not affect the outcome of the test is a good thing to do.
> I like the patch text, but ...
>
>> Since the CI globally exports GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main,
>> there's a drift between how the test repositories are set up in the CI
>> and during local testing. This issue does not manifest currently, as the
>> test does not do any remote HEAD manipulation where this would come up,
>> but should such things be added, a locally passing test would break the
>> CI vice-versa.
>
> ... this description may not quite be accurate.  Don't some jobs of
> CI use 'main' while the rest use 'master'?

True, it's that one specific job, and of course "there may be a drift depending on the particular test" is more accurate. I'll update for a v13.

Bence Ferdinandy· Oct 23, 2024, 15:36 UTC · re: Bence Ferdinandy · lore

[PATCH v12 2/8] refs: atomically record overwritten ref in update_symref

When updating a symref with update_symref it's currently not possible to know for sure what was the previous value that was overwritten. Extend refs_update_symref under a new function name, to record the value after the ref has been locked if the caller of refs_update_symref_extended requests it via a new variable in the function call. Keep the original refs_update_symref function with the same signature, but now as a wrapper around refs_update_symref_extended.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    
    v5: - added before_target to reftables backend
        - added an extra safety check for transaction's existence in refs.c
    
    v6: - no change
    
    v7: - remove the whole before_target concept from the backends and
          handle checking it in refs.c instead (thanks Karthik)
        - rename the before_target to referent which is how the same concept
          is called in the backends
        - change commit prefix to be more in line with project standards
    
    v8: no change
    
    v9: - instead of adding parameters to refs_update_symref, rename what
          was in v8 as refs_update_symref_extended and make refs_update_symref
          a wrapper for that. This significantly reduces the number of files
          that need to be touched, and avoids adding a lot of dummy NULL-s
          in unrelated places.
    
    v10: no change
    
    v11: no change
    
    v12: no change
 refs.c | 19 ++++++++++++++++---
 refs.h |  4 ++++
 2 files changed, 20 insertions(+), 3 deletions(-)
Show changes to 2 files +20 −3

refs.c, refs.h

diff --git a/refs.c b/refs.c
index 5f729ed412..24a4172cd2 100644
--- a/refs.c
+++ b/refs.c
@@ -2115,6 +2115,13 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
 
 int refs_update_symref(struct ref_store *refs, const char *ref,
 		       const char *target, const char *logmsg)
+{
+	return refs_update_symref_extended(refs, ref, target, logmsg, NULL);
+}
+
+int refs_update_symref_extended(struct ref_store *refs, const char *ref,
+		       const char *target, const char *logmsg,
+		       struct strbuf *referent)
 {
 	struct ref_transaction *transaction;
 	struct strbuf err = STRBUF_INIT;
@@ -2122,13 +2129,20 @@ int refs_update_symref(struct ref_store *refs, const char *ref,
 
 	transaction = ref_store_transaction_begin(refs, &err);
 	if (!transaction ||
-	    ref_transaction_update(transaction, ref, NULL, NULL,
+		ref_transaction_update(transaction, ref, NULL, NULL,
 				   target, NULL, REF_NO_DEREF,
 				   logmsg, &err) ||
-	    ref_transaction_commit(transaction, &err)) {
+		ref_transaction_prepare(transaction, &err)) {
 		ret = error("%s", err.buf);
+		goto cleanup;
 	}
+	if (referent)
+		refs_read_symbolic_ref(refs, ref, referent);
 
+	if (ref_transaction_commit(transaction, &err))
+		ret = error("%s", err.buf);
+
+cleanup:
 	strbuf_release(&err);
 	if (transaction)
 		ref_transaction_free(transaction);
@@ -2948,4 +2962,3 @@ int ref_update_expects_existing_old_ref(struct ref_update *update)
 	return (update->flags & REF_HAVE_OLD) &&
 		(!is_null_oid(&update->old_oid) || update->old_target);
 }
-
diff --git a/refs.h b/refs.h
index 108dfc93b3..259191a485 100644
--- a/refs.h
+++ b/refs.h
@@ -573,6 +573,10 @@ int refs_copy_existing_ref(struct ref_store *refs, const char *oldref,
 int refs_update_symref(struct ref_store *refs, const char *refname,
 		       const char *target, const char *logmsg);
 
+int refs_update_symref_extended(struct ref_store *refs, const char *refname,
+		       const char *target, const char *logmsg,
+		       struct strbuf *referent);
+
 enum action_on_err {
 	UPDATE_REFS_MSG_ON_ERR,
 	UPDATE_REFS_DIE_ON_ERR,
-- 
2.47.0.94.g1247fb88fd.dirty
Junio C Hamano· Nov 15, 2024, 05:50 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v12 2/8] refs: atomically record overwritten ref in update_symref

Bence Ferdinandy <bence@ferdinandy.com> writes:
Show 5 quoted lines
>  int refs_update_symref(struct ref_store *refs, const char *ref,
>  		       const char *target, const char *logmsg)
> +{
> +	return refs_update_symref_extended(refs, ref, target, logmsg, NULL);
> +}

OK. As the enhanced and renamed function is also external, we do not have to worry about reordering the old one to come after the new one.

Show 12 quoted lines
> +int refs_update_symref_extended(struct ref_store *refs, const char *ref,
> +		       const char *target, const char *logmsg,
> +		       struct strbuf *referent)
>  {
>  	struct ref_transaction *transaction;
>  	struct strbuf err = STRBUF_INIT;
> @@ -2122,13 +2129,20 @@ int refs_update_symref(struct ref_store *refs, const char *ref,
>  
>  	transaction = ref_store_transaction_begin(refs, &err);
>  	if (!transaction ||
> -	    ref_transaction_update(transaction, ref, NULL, NULL,
> +		ref_transaction_update(transaction, ref, NULL, NULL,
An unwanted patch noise?
>  				   target, NULL, REF_NO_DEREF,
>  				   logmsg, &err) ||
> -	    ref_transaction_commit(transaction, &err)) {
> +		ref_transaction_prepare(transaction, &err)) {

Likewise, but the noise distracts from the real change made on this line, which is even worse.

The real change here is to only call _prepare(), which also asks the transaction hook if we are OK to proceed. If we fail, we stop here

>  		ret = error("%s", err.buf);
> +		goto cleanup;

This is also a real change. We could instead make the additional code below into the else clause (see below).

>  	}
> +	if (referent)
> +		refs_read_symbolic_ref(refs, ref, referent);

And if we were asked to give the value of the symbolic ref, we make this call. What should this code do when reading fails (I know it ignores, as written, but I am asking what it _should_ do)?

> +	if (ref_transaction_commit(transaction, &err))
> +		ret = error("%s", err.buf);
And then we commit, or we fail to commit.
> +cleanup:

We could write the whole thing as a single "do these and leave as soon as we see any failure" ||-cascade,

	if (!(transaction = ref_store_transaction_begin(refs, &err)) ||
	    ref_transaction_update(transaction, ref, NULL, NULL,
				   target, NULL, REF_NO_DEREF,
				   logmsg, &err) ||
	    ref_transaction_prepare(transaction, &err)) ||
	    (referent
	    ? refs_read_symbolic_ref(refs, ref, referent)
	    : 0) ||
	    ref_transaction_commit(transaction, &err)) {
		if (!err.len)
			... stuff default error message to err ...;
		ret = error("%s", err.buf);
	}

which may not necessarily easier to follow (and in fact it is rather ugly), but at least, the resulting code does not have to special case the "optionally peek into the symref" step.

Bence Ferdinandy· Nov 15, 2024, 22:18 UTC · re: Junio C Hamano · lore

Re: [PATCH v12 2/8] refs: atomically record overwritten ref in update_symref

On Fri Nov 15, 2024 at 06:50, Junio C Hamano <gitster@pobox.com> wrote:
Show 11 quoted lines
> Bence Ferdinandy <bence@ferdinandy.com> writes:
>
>>  int refs_update_symref(struct ref_store *refs, const char *ref,
>>  		       const char *target, const char *logmsg)
>> +{
>> +	return refs_update_symref_extended(refs, ref, target, logmsg, NULL);
>> +}
>
> OK.  As the enhanced and renamed function is also external, we do
> not have to worry about reordering the old one to come after the new
> one.
I guess this also decides that the name "_extended" is fine :)
Show 23 quoted lines
>
>> +int refs_update_symref_extended(struct ref_store *refs, const char *ref,
>> +		       const char *target, const char *logmsg,
>> +		       struct strbuf *referent)
>>  {
>>  	struct ref_transaction *transaction;
>>  	struct strbuf err = STRBUF_INIT;
>> @@ -2122,13 +2129,20 @@ int refs_update_symref(struct ref_store *refs, const char *ref,
>>  
>>  	transaction = ref_store_transaction_begin(refs, &err);
>>  	if (!transaction ||
>> -	    ref_transaction_update(transaction, ref, NULL, NULL,
>> +		ref_transaction_update(transaction, ref, NULL, NULL,
>
> An unwanted patch noise?
>
>>  				   target, NULL, REF_NO_DEREF,
>>  				   logmsg, &err) ||
>> -	    ref_transaction_commit(transaction, &err)) {
>> +		ref_transaction_prepare(transaction, &err)) {
>
> Likewise, but the noise distracts from the real change made on this
> line, which is even worse.
Mea culpa, I'll get this cleaned up.
Show 17 quoted lines
>
> The real change here is to only call _prepare(), which also asks the
> transaction hook if we are OK to proceed.  If we fail, we stop here
>
>>  		ret = error("%s", err.buf);
>> +		goto cleanup;
>
> This is also a real change.  We could instead make the additional
> code below into the else clause (see below).
>
>>  	}
>> +	if (referent)
>> +		refs_read_symbolic_ref(refs, ref, referent);
>
> And if we were asked to give the value of the symbolic ref, we make
> this call.  What should this code do when reading fails (I know it
> ignores, as written, but I am asking what it _should_ do)?

I think this should do _nothing_ if it fails (although should it stay this way, I guess it should be marked with a comment that this is on purpose). My reasoning is that running `git fetch` will be running this part of the code, which means that should reading the symbolic ref fail for any reason, a `fetch` that previously ran without error would now fail. We now pass up an empty string as the previous which does mask that there was an error here. What I think we could maybe do is pass up a special string that means there was an error? Something that for sure cannot be a valid value for an existing reference? I'm not sure how much sense that makes.

Show 32 quoted lines
>
>> +	if (ref_transaction_commit(transaction, &err))
>> +		ret = error("%s", err.buf);
>
> And then we commit, or we fail to commit.
>
>> +cleanup:
>
> We could write the whole thing as a single "do these and leave as
> soon as we see any failure" ||-cascade,
>
> 	if (!(transaction = ref_store_transaction_begin(refs, &err)) ||
>
> 	    ref_transaction_update(transaction, ref, NULL, NULL,
> 				   target, NULL, REF_NO_DEREF,
> 				   logmsg, &err) ||
>
> 	    ref_transaction_prepare(transaction, &err)) ||
>
> 	    (referent
> 	    ? refs_read_symbolic_ref(refs, ref, referent)
> 	    : 0) ||
>
> 	    ref_transaction_commit(transaction, &err)) {
> 		if (!err.len)
> 			... stuff default error message to err ...;
> 		ret = error("%s", err.buf);
> 	}
>
> which may not necessarily easier to follow (and in fact it is rather
> ugly), but at least, the resulting code does not have to special
> case the "optionally peek into the symref" step.

As I said above, I don't think we actually want to fail the update even if the symbolic ref reading fails, so I think the special casing should stay. I'll wait here to see more clearly on what to do here.

Bence Ferdinandy· Nov 15, 2024, 23:27 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v12 2/8] refs: atomically record overwritten ref in update_symref

2024. nov. 15. 23:18:10 Bence Ferdinandy <bence@ferdinandy.com>:
Show 78 quoted lines
>
> On Fri Nov 15, 2024 at 06:50, Junio C Hamano <gitster@pobox.com> wrote:
>> Bence Ferdinandy <bence@ferdinandy.com> writes:
>>
>>> int refs_update_symref(struct ref_store *refs, const char *ref,
>>>                const char *target, const char *logmsg)
>>> +{
>>> +   return refs_update_symref_extended(refs, ref, target, logmsg, 
>>> NULL);
>>> +}
>>
>> OK.  As the enhanced and renamed function is also external, we do
>> not have to worry about reordering the old one to come after the new
>> one.
>
> I guess this also decides that the name "_extended" is fine :)
>
>>
>>> +int refs_update_symref_extended(struct ref_store *refs, const char 
>>> *ref,
>>> +              const char *target, const char *logmsg,
>>> +              struct strbuf *referent)
>>> {
>>>     struct ref_transaction *transaction;
>>>     struct strbuf err = STRBUF_INIT;
>>> @@ -2122,13 +2129,20 @@ int refs_update_symref(struct ref_store 
>>> *refs, const char *ref,
>>>
>>>     transaction = ref_store_transaction_begin(refs, &err);
>>>     if (!transaction ||
>>> -       ref_transaction_update(transaction, ref, NULL, NULL,
>>> +       ref_transaction_update(transaction, ref, NULL, NULL,
>>
>> An unwanted patch noise?
>>
>>>                    target, NULL, REF_NO_DEREF,
>>>                    logmsg, &err) ||
>>> -       ref_transaction_commit(transaction, &err)) {
>>> +       ref_transaction_prepare(transaction, &err)) {
>>
>> Likewise, but the noise distracts from the real change made on this
>> line, which is even worse.
>
> Mea culpa, I'll get this cleaned up.
>
>>
>> The real change here is to only call _prepare(), which also asks the
>> transaction hook if we are OK to proceed.  If we fail, we stop here
>>
>>>         ret = error("%s", err.buf);
>>> +       goto cleanup;
>>
>> This is also a real change.  We could instead make the additional
>> code below into the else clause (see below).
>>
>>>     }
>>> +   if (referent)
>>> +       refs_read_symbolic_ref(refs, ref, referent);
>>
>> And if we were asked to give the value of the symbolic ref, we make
>> this call.  What should this code do when reading fails (I know it
>> ignores, as written, but I am asking what it _should_ do)?
>
> I think this should do _nothing_ if it fails (although should it stay 
> this way,
> I guess it should be marked with a comment that this is on purpose). My
> reasoning is that running `git fetch` will be running this part of the 
> code,
> which means that should reading the symbolic ref fail for any reason, a 
> `fetch`
> that previously ran without error would now fail. We now pass up an 
> empty
> string as the previous which does mask that there was an error here. 
> What
> I think we could maybe do is pass up a special string that means there 
> was an
> error? Something that for sure cannot be a valid value for an existing
> reference? I'm not sure how much sense that makes.

Sorry, it's late. The above is slightly bollocks since fetch ignores any set_head errors later :) But the idea stands that if we can set the head, let's do it. The previous head is not important enough to die on.

As mentioned on the other patch, we could try to read a non-symref instead also, if symref fails.

Show 39 quoted lines
>
>>
>>> +   if (ref_transaction_commit(transaction, &err))
>>> +       ret = error("%s", err.buf);
>>
>> And then we commit, or we fail to commit.
>>
>>> +cleanup:
>>
>> We could write the whole thing as a single "do these and leave as
>> soon as we see any failure" ||-cascade,
>>
>>     if (!(transaction = ref_store_transaction_begin(refs, &err)) ||
>>
>>         ref_transaction_update(transaction, ref, NULL, NULL,
>>                    target, NULL, REF_NO_DEREF,
>>                    logmsg, &err) ||
>>
>>         ref_transaction_prepare(transaction, &err)) ||
>>
>>         (referent
>>         ? refs_read_symbolic_ref(refs, ref, referent)
>>         : 0) ||
>>
>>         ref_transaction_commit(transaction, &err)) {
>>         if (!err.len)
>>             ... stuff default error message to err ...;
>>         ret = error("%s", err.buf);
>>     }
>>
>> which may not necessarily easier to follow (and in fact it is rather
>> ugly), but at least, the resulting code does not have to special
>> case the "optionally peek into the symref" step.
>
> As I said above, I don't think we actually want to fail the update even 
> if the
> symbolic ref reading fails, so I think the special casing should stay. 
> I'll
> wait here to see more clearly on what to do here.
Junio C Hamano· Nov 16, 2024, 07:58 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v12 2/8] refs: atomically record overwritten ref in update_symref

Bence Ferdinandy <bence@ferdinandy.com> writes:
> Sorry, it's late. The above is slightly bollocks since fetch ignores
> any set_head errors later :)
> But the idea stands that if we can set the head, let's do it.
> The previous head is not important enough to die on.

Yes, I didn't mean to suggest aborting the fetch. I just wanted to make sure we _react_ correctly to a failing call to read a symref, possibly because the refs/remotes/$there/HEAD (1) does not exist, or (2) is not a symbolic ref but signals that the other side has its HEAD detached. Treating any failure to read a symref as if the symref does not exist would miss the latter case.

Thanks.
Bence Ferdinandy· Nov 17, 2024, 23:39 UTC · re: Junio C Hamano · lore

Re: [PATCH v12 2/8] refs: atomically record overwritten ref in update_symref

>
>>  	} +	if (referent) +		refs_read_symbolic_ref(refs, ref,
>>  	referent);

So I've been working on detecting a detached remote/HEAD, and it seems that "refs_read_symbolic_ref" behaves differently for the files and the reftables backend. These are the exit codes in the various states:

	     reftables files
detached	-1	 1	
doesn't exist   -1	-1

I would assume this is a bug in reftables? At least the behaviour of files is more useful for this case ...

This now works fine with the files backend:
	if (referent && refs_read_symbolic_ref(refs, ref, referent) == 1) {
		struct object_id oid;
		refs_read_ref(refs, ref, &oid);
		strbuf_addstr(referent, oid_to_hex(&oid));
		ret = -1;
	}

And 4/8 can now also detect being detached, by checking the return value using the test you suggested, but this fails for reftables. Just in case it might be something about the test not being correct:

test_expect_success 'set-head --auto to update a non symbolic ref' '
	(
		cd test &&
		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
		git update-ref refs/remotes/origin/HEAD HEAD &&
		HEAD=$(git log --pretty="%H") &&
		git remote set-head --auto origin >output &&
		echo "${SQ}origin/HEAD${SQ} was detached at ${SQ}${HEAD}${SQ} and now points to ${SQ}main${SQ}" >expect &&
		test_cmp expect output
	)
'

Should I fix the reftables problem or should I mark the above test as a known breakage? Can that be done for just reftables somehow? The only problem reftables suffers this way is to incorrectly report "creation" instead of correctly saying it was a detached head.

Show 24 quoted lines
> We could write the whole thing as a single "do these and leave as
> soon as we see any failure" ||-cascade,
>
> 	if (!(transaction = ref_store_transaction_begin(refs, &err)) ||
>
> 	    ref_transaction_update(transaction, ref, NULL, NULL,
> 				   target, NULL, REF_NO_DEREF,
> 				   logmsg, &err) ||
>
> 	    ref_transaction_prepare(transaction, &err)) ||
>
> 	    (referent
> 	    ? refs_read_symbolic_ref(refs, ref, referent)
> 	    : 0) ||
>
> 	    ref_transaction_commit(transaction, &err)) {
> 		if (!err.len)
> 			... stuff default error message to err ...;
> 		ret = error("%s", err.buf);
> 	}
>
> which may not necessarily easier to follow (and in fact it is rather
> ugly), but at least, the resulting code does not have to special
> case the "optionally peek into the symref" step.

I realised I misread the above snippet the last time I looked at it, but at a first glance that would make 6/8 painful.

Thanks, Bence

-- 
bence.ferdinandy.com
Junio C Hamano· Nov 18, 2024, 00:39 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v12 2/8] refs: atomically record overwritten ref in update_symref

"Bence Ferdinandy" <ferdinandy.bence@ttk.elte.hu> writes:
Show 16 quoted lines
>>
>>>  	} +	if (referent) +		refs_read_symbolic_ref(refs, ref,
>>>  	referent);
>
>
> So I've been working on detecting a detached remote/HEAD, and it seems that
> "refs_read_symbolic_ref" behaves differently for the files and the reftables
> backend. These are the exit codes in the various states:
>
>
> 	     reftables files
> detached	-1	 1	
> doesn't exist   -1	-1
>
> I would assume this is a bug in reftables? At least the behaviour of files is
> more useful for this case ...
Interesting.  Patrick, comments?
Show 30 quoted lines
> This now works fine with the files backend:
>
> 	if (referent && refs_read_symbolic_ref(refs, ref, referent) == 1) {
> 		struct object_id oid;
> 		refs_read_ref(refs, ref, &oid);
> 		strbuf_addstr(referent, oid_to_hex(&oid));
> 		ret = -1;
> 	}
>
> And 4/8 can now also detect being detached, by checking the return value using
> the test you suggested, but this fails for reftables. Just in case it might be
> something about the test not being correct:
>
>
> test_expect_success 'set-head --auto to update a non symbolic ref' '
> 	(
> 		cd test &&
> 		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
> 		git update-ref refs/remotes/origin/HEAD HEAD &&
> 		HEAD=$(git log --pretty="%H") &&
> 		git remote set-head --auto origin >output &&
> 		echo "${SQ}origin/HEAD${SQ} was detached at ${SQ}${HEAD}${SQ} and now points to ${SQ}main${SQ}" >expect &&
> 		test_cmp expect output
> 	)
> '
>
> Should I fix the reftables problem or should I mark the above test as a known
> breakage? Can that be done for just reftables somehow? The only problem
> reftables suffers this way is to incorrectly report "creation" instead of
> correctly saying it was a detached head.
Thanks.
Patrick Steinhardt· Nov 18, 2024, 07:22 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v12 2/8] refs: atomically record overwritten ref in update_symref

On Mon, Nov 18, 2024 at 12:39:43AM +0100, Bence Ferdinandy wrote:
Show 29 quoted lines
> >
> >>  	} +	if (referent) +		refs_read_symbolic_ref(refs, ref,
> >>  	referent);
> 
> 
> So I've been working on detecting a detached remote/HEAD, and it seems that
> "refs_read_symbolic_ref" behaves differently for the files and the reftables
> backend. These are the exit codes in the various states:
> 
> 
> 	     reftables files
> detached	-1	 1	
> doesn't exist   -1	-1
> 
> I would assume this is a bug in reftables? At least the behaviour of files is
> more useful for this case ...
> 
> This now works fine with the files backend:
> 
> 	if (referent && refs_read_symbolic_ref(refs, ref, referent) == 1) {
> 		struct object_id oid;
> 		refs_read_ref(refs, ref, &oid);
> 		strbuf_addstr(referent, oid_to_hex(&oid));
> 		ret = -1;
> 	}
> 
> And 4/8 can now also detect being detached, by checking the return value using
> the test you suggested, but this fails for reftables. Just in case it might be
> something about the test not being correct:

So from what I understand you try to execute `refs_read_symbolic_ref()` on a non-symbolic-ref, and your expectation is:

  - It returns -1 when reading the ref has failed.
  - It returns 0 when reading the ref was successful and it was a
    symref.
  - It retuns 1 when reading the ref was successful, but it was a
    regular ref.

This behaviour isn't documented anywhere, so I wouldn't declare it a bug in the reftable backend. But what is a bug is that the two backends behave differently, and that should be fixed indeed.

I couldn't find any callsites of `refs_read_symbolic_ref()` where we rely on the current behaviour of either of the backends. We do have a check whether `refs_read_symbolic_ref()` returns negative in "refs.c" in `migrate_one_ref()`, but that one should be mostly fine given that we check for the type of the ref beforehand. "Mostly" though because it can happen that we race with another writer that happened to convert the ref we are about to migrate from a symbolic ref into a normal ref. Unlikely, but it can happen in theory.

I think it's an easy mistake to make to check for a negative return code. So maybe we should adapt both backends to return -1 for generic failures and -2 in case the ref is a regular ref?

Patrick
Bence Ferdinandy· Nov 18, 2024, 08:08 UTC · re: Patrick Steinhardt · lore

Re: [PATCH v12 2/8] refs: atomically record overwritten ref in update_symref

On Mon Nov 18, 2024 at 08:22, Patrick Steinhardt <ps@pks.im> wrote:
Show 11 quoted lines
>
> So from what I understand you try to execute `refs_read_symbolic_ref()`
> on a non-symbolic-ref, and your expectation is:
>
>   - It returns -1 when reading the ref has failed.
>
>   - It returns 0 when reading the ref was successful and it was a
>     symref.
>
>   - It retuns 1 when reading the ref was successful, but it was a
>     regular ref.

Well, the other way around (it's how files backend does it), but I guess that is irrelevant at this point.

Show 17 quoted lines
>
> This behaviour isn't documented anywhere, so I wouldn't declare it a bug
> in the reftable backend. But what is a bug is that the two backends
> behave differently, and that should be fixed indeed.
>
> I couldn't find any callsites of `refs_read_symbolic_ref()` where we
> rely on the current behaviour of either of the backends. We do have a
> check whether `refs_read_symbolic_ref()` returns negative in "refs.c" in
> `migrate_one_ref()`, but that one should be mostly fine given that we
> check for the type of the ref beforehand. "Mostly" though because it can
> happen that we race with another writer that happened to convert the ref
> we are about to migrate from a symbolic ref into a normal ref. Unlikely,
> but it can happen in theory.
>
> I think it's an easy mistake to make to check for a negative return
> code. So maybe we should adapt both backends to return -1 for generic
> failures and -2 in case the ref is a regular ref?

I've been wondering about this when writing other parts of the series and now is a good a time as any to ask: I've already seen this pattern of returning various negative integers as error codes, but never quite got the logic behind it. Why not just return the same numbers but positive?

Anyhow, the proposed solution sounds good and as far as I see how things are done in the code. I guess if I want the series to land I should just fix that as well, there are already a couple of not-entirely-related fixes in there :)

Two questions about that:
- what would be the ideal place to document this behaviour? In refs.c with
  `refs_read_symbolic_ref` or with the `struct ref_storage_be` in
  refs/refs-internal.h?
- should I look into adding specific tests for this? Since the rest of the
  series will depend on this behaviour it will be implicit tested anyway, so
  I don't particularly think it would be necessary, but I don't know what the
  general approach is.

Thanks, Bence

Patrick Steinhardt· Nov 18, 2024, 08:24 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v12 2/8] refs: atomically record overwritten ref in update_symref

On Mon, Nov 18, 2024 at 09:08:13AM +0100, Bence Ferdinandy wrote:
Show 22 quoted lines
> On Mon Nov 18, 2024 at 08:22, Patrick Steinhardt <ps@pks.im> wrote:
> > This behaviour isn't documented anywhere, so I wouldn't declare it a bug
> > in the reftable backend. But what is a bug is that the two backends
> > behave differently, and that should be fixed indeed.
> >
> > I couldn't find any callsites of `refs_read_symbolic_ref()` where we
> > rely on the current behaviour of either of the backends. We do have a
> > check whether `refs_read_symbolic_ref()` returns negative in "refs.c" in
> > `migrate_one_ref()`, but that one should be mostly fine given that we
> > check for the type of the ref beforehand. "Mostly" though because it can
> > happen that we race with another writer that happened to convert the ref
> > we are about to migrate from a symbolic ref into a normal ref. Unlikely,
> > but it can happen in theory.
> >
> > I think it's an easy mistake to make to check for a negative return
> > code. So maybe we should adapt both backends to return -1 for generic
> > failures and -2 in case the ref is a regular ref?
> 
> I've been wondering about this when writing other parts of the series and now
> is a good a time as any to ask: I've already seen this pattern of returning
> various negative integers as error codes, but never quite got the logic behind
> it. Why not just return the same numbers but positive?

It's a matter of style, I guess. Many functions use the return value as both an indicator for error and as the actual returned value. Think e.g. function calls like open(3p), where a negative value indicates an error and everything else is an actual file descriptor. This carries over into our codebase for many functions, but we're not consistent.

Show 9 quoted lines
> Anyhow, the proposed solution sounds good and as far as I see how things are
> done in the code. I guess if I want the series to land I should just fix that
> as well, there are already a couple of not-entirely-related fixes in there :)
> 
> Two questions about that:
> 
> - what would be the ideal place to document this behaviour? In refs.c with
>   `refs_read_symbolic_ref` or with the `struct ref_storage_be` in
>   refs/refs-internal.h?

I'd document this in "refs.h", where the user-facing function is declared, and in "refs-internal.h", where the callback is defined.

> - should I look into adding specific tests for this? Since the rest of the
>   series will depend on this behaviour it will be implicit tested anyway, so
>   I don't particularly think it would be necessary, but I don't know what the
>   general approach is.

I had a look and couldn't find another way to test the behaviour because we use `refs_read_symbolic_ref()` sparingly, only. So I think it's okay to implicitly test this, only.

Patrick
Bence Ferdinandy· Nov 18, 2024, 11:37 UTC · re: Patrick Steinhardt · lore

Re: [PATCH v12 2/8] refs: atomically record overwritten ref in update_symref

On Mon Nov 18, 2024 at 09:24, Patrick Steinhardt <ps@pks.im> wrote:
Show 51 quoted lines
> On Mon, Nov 18, 2024 at 09:08:13AM +0100, Bence Ferdinandy wrote:
>> On Mon Nov 18, 2024 at 08:22, Patrick Steinhardt <ps@pks.im> wrote:
>> > This behaviour isn't documented anywhere, so I wouldn't declare it a bug
>> > in the reftable backend. But what is a bug is that the two backends
>> > behave differently, and that should be fixed indeed.
>> >
>> > I couldn't find any callsites of `refs_read_symbolic_ref()` where we
>> > rely on the current behaviour of either of the backends. We do have a
>> > check whether `refs_read_symbolic_ref()` returns negative in "refs.c" in
>> > `migrate_one_ref()`, but that one should be mostly fine given that we
>> > check for the type of the ref beforehand. "Mostly" though because it can
>> > happen that we race with another writer that happened to convert the ref
>> > we are about to migrate from a symbolic ref into a normal ref. Unlikely,
>> > but it can happen in theory.
>> >
>> > I think it's an easy mistake to make to check for a negative return
>> > code. So maybe we should adapt both backends to return -1 for generic
>> > failures and -2 in case the ref is a regular ref?
>> 
>> I've been wondering about this when writing other parts of the series and now
>> is a good a time as any to ask: I've already seen this pattern of returning
>> various negative integers as error codes, but never quite got the logic behind
>> it. Why not just return the same numbers but positive?
>
> It's a matter of style, I guess. Many functions use the return value as
> both an indicator for error and as the actual returned value. Think e.g.
> function calls like open(3p), where a negative value indicates an error
> and everything else is an actual file descriptor. This carries over into
> our codebase for many functions, but we're not consistent.
>
>> Anyhow, the proposed solution sounds good and as far as I see how things are
>> done in the code. I guess if I want the series to land I should just fix that
>> as well, there are already a couple of not-entirely-related fixes in there :)
>> 
>> Two questions about that:
>> 
>> - what would be the ideal place to document this behaviour? In refs.c with
>>   `refs_read_symbolic_ref` or with the `struct ref_storage_be` in
>>   refs/refs-internal.h?
>
> I'd document this in "refs.h", where the user-facing function is
> declared, and in "refs-internal.h", where the callback is defined.
>
>> - should I look into adding specific tests for this? Since the rest of the
>>   series will depend on this behaviour it will be implicit tested anyway, so
>>   I don't particularly think it would be necessary, but I don't know what the
>>   general approach is.
>
> I had a look and couldn't find another way to test the behaviour because
> we use `refs_read_symbolic_ref()` sparingly, only. So I think it's okay
> to implicitly test this, only.

Thanks, it was surprisingly easy to do, I'll do a touchup of the other patches and will send a v13 today with the fix.

Best, Bence

Bence Ferdinandy· Oct 23, 2024, 15:36 UTC · re: Bence Ferdinandy · lore

[PATCH v12 3/8] remote set-head: refactor for readability

Rename buf and buf2 to something more explanatory.

Instead of calling get_main_ref_store(the_repository) multiple times, call it once and store in a new refs variable. Although this change probably offers some performance benefits, the main purpose is to shorten the line lengths of function calls using this variable.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v5: new patch (split from the next patch as a preparatory step)
    
    v6: no change
    
    v7: - change commit prefix to be more in line with project standards
    
    v8: no change
    
    v9: - further improve readability by renaming buf, and buf2 consistently
          with how patch 6 was already done
    
    v10: no change
    
    v11: no change
    
    v12: no change
 builtin/remote.c | 23 ++++++++++++-----------
 1 file changed, 12 insertions(+), 11 deletions(-)
Show changes to builtin/remote.c +12 −11
diff --git a/builtin/remote.c b/builtin/remote.c
index 76670ddd8b..1d68c5b2ba 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1402,8 +1402,9 @@ static int show(int argc, const char **argv, const char *prefix)
 static int set_head(int argc, const char **argv, const char *prefix)
 {
 	int i, opt_a = 0, opt_d = 0, result = 0;
-	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT;
 	char *head_name = NULL;
+	struct ref_store *refs = get_main_ref_store(the_repository);
 
 	struct option options[] = {
 		OPT_BOOL('a', "auto", &opt_a,
@@ -1415,7 +1416,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 	argc = parse_options(argc, argv, prefix, options,
 			     builtin_remote_sethead_usage, 0);
 	if (argc)
-		strbuf_addf(&buf, "refs/remotes/%s/HEAD", argv[0]);
+		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", argv[0]);
 
 	if (!opt_a && !opt_d && argc == 2) {
 		head_name = xstrdup(argv[1]);
@@ -1434,25 +1435,25 @@ static int set_head(int argc, const char **argv, const char *prefix)
 			head_name = xstrdup(states.heads.items[0].string);
 		free_remote_ref_states(&states);
 	} else if (opt_d && !opt_a && argc == 1) {
-		if (refs_delete_ref(get_main_ref_store(the_repository), NULL, buf.buf, NULL, REF_NO_DEREF))
-			result |= error(_("Could not delete %s"), buf.buf);
+		if (refs_delete_ref(refs, NULL, b_head.buf, NULL, REF_NO_DEREF))
+			result |= error(_("Could not delete %s"), b_head.buf);
 	} else
 		usage_with_options(builtin_remote_sethead_usage, options);
 
 	if (head_name) {
-		strbuf_addf(&buf2, "refs/remotes/%s/%s", argv[0], head_name);
+		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", argv[0], head_name);
 		/* make sure it's valid */
-		if (!refs_ref_exists(get_main_ref_store(the_repository), buf2.buf))
-			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head"))
-			result |= error(_("Could not setup %s"), buf.buf);
+		if (!refs_ref_exists(refs, b_remote_head.buf))
+			result |= error(_("Not a valid ref: %s"), b_remote_head.buf);
+		else if (refs_update_symref(refs, b_head.buf, b_remote_head.buf, "remote set-head"))
+			result |= error(_("Could not setup %s"), b_head.buf);
 		else if (opt_a)
 			printf("%s/HEAD set to %s\n", argv[0], head_name);
 		free(head_name);
 	}
 
-	strbuf_release(&buf);
-	strbuf_release(&buf2);
+	strbuf_release(&b_head);
+	strbuf_release(&b_remote_head);
 	return result;
 }
 
-- 
2.47.0.94.g1247fb88fd.dirty
Junio C Hamano· Nov 15, 2024, 05:50 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v12 3/8] remote set-head: refactor for readability

Bence Ferdinandy <bence@ferdinandy.com> writes:
Show 6 quoted lines
> Rename buf and buf2 to something more explanatory.
>
> Instead of calling get_main_ref_store(the_repository) multiple times,
> call it once and store in a new refs variable. Although this change
> probably offers some performance benefits, the main purpose is to
> shorten the line lengths of function calls using this variable.

Also two strbufs are renamed, which is in line with the objective of the other change---to make the resulting code easier to follow.

Looking good.
>     v9: - further improve readability by renaming buf, and buf2 consistently
>           with how patch 6 was already done
Bence Ferdinandy· Oct 23, 2024, 15:36 UTC · re: Bence Ferdinandy · lore

[PATCH v12 4/8] remote set-head: better output for --auto

Currently, set-head --auto will print a message saying "remote/HEAD set to branch", which implies something was changed.

Change the output of --auto, so the output actually reflects what was done: a) set a previously unset HEAD, b) change HEAD because remote changed or c) no updates. As a fourth output, if HEAD is changed from a previous value that was not a remote branch, explicitly call attention to this fact.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v1-v2:
    
        was RFC in https://lore.kernel.org/git/20240910203835.2288291-1-bence@ferdinandy.com/
    
    v3:
    
        This patch was originally sent along when I thought set-head was
        going to be invoked by fetch, but the discussion on the RFC
        concluded that it should be not. This opened the possibility to make
        it more explicit.
    
        Note: although I feel both things the patch does are really just
        cosmetic, an argument could be made for breaking it into two, one
        for the no-op part and one for the --auto print update.
    
        Was sent in:
        https://lore.kernel.org/git/20240915221055.904107-1-bence@ferdinandy.com/
    
    v4:
        - changes are now handled atomically via the ref update transaction
        - outputs have changed along the lines of Junio's suggestion
        - minor refactor to set_head for improved legibility
    
    v5: - the minor refactor has been split out into its own patch
    
    v6: - fixed uninitialized prev_head
        - fixed case of unusual previous target
        - fixed a test that would have been actually broken at this patch
          (the output was only correct with the later update to fetch)
        - added 4 tests for the 4 output cases
    
    v7: - change commit prefix to be more in line with project standards
        - fixed tests to also work with the reftable backend
        - renamed report function, fixed style issue with checking buf len
        - fixed not releasing an strbuf
    
    v8: no change
    
    v9: - mark output strings in report_set_head_auto as translatable
        - rename buf_prev to b_local_head for consistency
        - use ${SQ} in tests instead of '\''
    
    v10: no change
    
    v11: no change
    
    v12: no change
 builtin/remote.c  | 33 +++++++++++++++++++++++++++---
 t/t5505-remote.sh | 51 ++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 80 insertions(+), 4 deletions(-)
Show changes to 2 files +80 −4

builtin/remote.c, t/t5505-remote.sh

diff --git a/builtin/remote.c b/builtin/remote.c
index 1d68c5b2ba..108f1271d3 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1399,10 +1399,35 @@ static int show(int argc, const char **argv, const char *prefix)
 	return result;
 }
 
+static void report_set_head_auto(const char *remote, const char *head_name,
+			struct strbuf *b_local_head) {
+	struct strbuf buf_prefix = STRBUF_INIT;
+	const char *prev_head = NULL;
+
+	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
+	skip_prefix(b_local_head->buf, buf_prefix.buf, &prev_head);
+
+	if (prev_head && !strcmp(prev_head, head_name))
+		printf(_("'%s/HEAD' is unchanged and points to '%s'\n"),
+			remote, head_name);
+	else if (prev_head)
+		printf(_("'%s/HEAD' has changed from '%s' and now points to '%s'\n"),
+			remote, prev_head, head_name);
+	else if (!b_local_head->len)
+		printf(_("'%s/HEAD' is now created and points to '%s'\n"),
+			remote, head_name);
+	else
+		printf(_("'%s/HEAD' used to point to '%s' "
+			"(which is not a remote branch), but now points to '%s'\n"),
+			remote, b_local_head->buf, head_name);
+	strbuf_release(&buf_prefix);
+}
+
 static int set_head(int argc, const char **argv, const char *prefix)
 {
 	int i, opt_a = 0, opt_d = 0, result = 0;
-	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
+		b_local_head = STRBUF_INIT;
 	char *head_name = NULL;
 	struct ref_store *refs = get_main_ref_store(the_repository);
 
@@ -1445,15 +1470,17 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		/* make sure it's valid */
 		if (!refs_ref_exists(refs, b_remote_head.buf))
 			result |= error(_("Not a valid ref: %s"), b_remote_head.buf);
-		else if (refs_update_symref(refs, b_head.buf, b_remote_head.buf, "remote set-head"))
+		else if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
+					"remote set-head", &b_local_head))
 			result |= error(_("Could not setup %s"), b_head.buf);
 		else if (opt_a)
-			printf("%s/HEAD set to %s\n", argv[0], head_name);
+			report_set_head_auto(argv[0], head_name, &b_local_head);
 		free(head_name);
 	}
 
 	strbuf_release(&b_head);
 	strbuf_release(&b_remote_head);
+	strbuf_release(&b_local_head);
 	return result;
 }
 
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 9b50276646..0ea86d51a4 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -432,12 +432,51 @@ test_expect_success 'set-head --auto' '
 	)
 '
 
+test_expect_success 'set-head --auto detects creation' '
+	(
+		cd test &&
+		git symbolic-ref -d refs/remotes/origin/HEAD &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} is now created and points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects no change' '
+	(
+		cd test &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} is unchanged and points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects change' '
+	(
+		cd test &&
+		git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/ahead &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} has changed from ${SQ}ahead${SQ} and now points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects strange ref' '
+	(
+		cd test &&
+		git symbolic-ref refs/remotes/origin/HEAD refs/heads/main &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} used to point to ${SQ}refs/heads/main${SQ} (which is not a remote branch), but now points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
 test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
 	(
 		cd test &&
 		git fetch two "refs/heads/*:refs/remotes/two/*" &&
 		git remote set-head --auto two >output 2>&1 &&
-		echo "two/HEAD set to main" >expect &&
+		echo "${SQ}two/HEAD${SQ} is now created and points to ${SQ}main${SQ}" >expect &&
 		test_cmp expect output
 	)
 '
@@ -456,6 +495,16 @@ test_expect_success 'set-head explicit' '
 	)
 '
 
+test_expect_success 'set-head --auto reports change' '
+	(
+		cd test &&
+		git remote set-head origin side2 &&
+		git remote set-head --auto origin >output 2>&1 &&
+		echo "${SQ}origin/HEAD${SQ} has changed from ${SQ}side2${SQ} and now points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
 cat >test/expect <<EOF
 Pruning origin
 URL: $(pwd)/one
-- 
2.47.0.94.g1247fb88fd.dirty
Junio C Hamano· Nov 15, 2024, 05:50 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v12 4/8] remote set-head: better output for --auto

Bence Ferdinandy <bence@ferdinandy.com> writes:
Show 8 quoted lines
> Currently, set-head --auto will print a message saying "remote/HEAD set
> to branch", which implies something was changed.
>
> Change the output of --auto, so the output actually reflects what was
> done: a) set a previously unset HEAD, b) change HEAD because remote
> changed or c) no updates. As a fourth output, if HEAD is changed from
> a previous value that was not a remote branch, explicitly call attention
> to this fact.
OK.  That's sensible.

There is a slight variant of the fourth case. HEAD may have been a symbolic ref that pointed at an unexpected place (which you addressed), or HEAD may have been a non-symbolic ref (which the new code would mistakenly say "HEAD is now created", if I am reading the patch correctly).

Show 12 quoted lines
> diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
> index 9b50276646..0ea86d51a4 100755
> --- a/t/t5505-remote.sh
> +++ b/t/t5505-remote.sh
> @@ -432,12 +432,51 @@ test_expect_success 'set-head --auto' '
>  	)
>  '
>  
> +test_expect_success 'set-head --auto detects creation' '
> +	(
> +		cd test &&
> +		git symbolic-ref -d refs/remotes/origin/HEAD &&

Are we sure refs/remotes/origin/HEAD exists at this point in the test, regardless of which earlier tests were skipped or failed? If not, perhaps

		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
is a better alternative.
Show 5 quoted lines
> +		git remote set-head --auto origin >output &&
> +		echo "${SQ}origin/HEAD${SQ} is now created and points to ${SQ}main${SQ}" >expect &&
> +		test_cmp expect output
> +	)
> +'
Here, we could insert another one:
test_expect_success 'set-head --auto to update a non symbolic ref' '
	(
		cd test &&
		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
		git update-ref refs/remotes/origin/HEAD HEAD &&
		git remote set-head --auto origin >output &&

I'd imagine "output" should at least say that we are setting up a symref origin/HEAD to point at some ref the --auto option figured out, and if we wanted to report its previous state, it was a non symbolic ref that pointed at some commit. In any case,

		echo "${SQ}origin/HEAD${SQ} is now created and points to ${SQ}main${SQ}" >expect &&
is not what we want to see here, I suspect.

Can we detect the case where we overwrite a non symref with a symref without going back to step 2/8 and doing a major surgery?

		test_cmp expect output
	)
'
Bence Ferdinandy· Nov 15, 2024, 22:49 UTC · re: Junio C Hamano · lore

Re: [PATCH v12 4/8] remote set-head: better output for --auto

On Fri Nov 15, 2024 at 06:50, Junio C Hamano <gitster@pobox.com> wrote:
Show 18 quoted lines
> Bence Ferdinandy <bence@ferdinandy.com> writes:
>
>> Currently, set-head --auto will print a message saying "remote/HEAD set
>> to branch", which implies something was changed.
>>
>> Change the output of --auto, so the output actually reflects what was
>> done: a) set a previously unset HEAD, b) change HEAD because remote
>> changed or c) no updates. As a fourth output, if HEAD is changed from
>> a previous value that was not a remote branch, explicitly call attention
>> to this fact.
>
> OK.  That's sensible.
>
> There is a slight variant of the fourth case.  HEAD may have been a
> symbolic ref that pointed at an unexpected place (which you
> addressed), or HEAD may have been a non-symbolic ref (which the new
> code would mistakenly say "HEAD is now created", if I am reading the
> patch correctly).

Good point, and yes, that is what happens. (Although I'm not quite sure how valid that state is where a remote's HEAD is not a branch).

Show 21 quoted lines
>
>> diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
>> index 9b50276646..0ea86d51a4 100755
>> --- a/t/t5505-remote.sh
>> +++ b/t/t5505-remote.sh
>> @@ -432,12 +432,51 @@ test_expect_success 'set-head --auto' '
>>  	)
>>  '
>>  
>> +test_expect_success 'set-head --auto detects creation' '
>> +	(
>> +		cd test &&
>> +		git symbolic-ref -d refs/remotes/origin/HEAD &&
>
> Are we sure refs/remotes/origin/HEAD exists at this point in the
> test, regardless of which earlier tests were skipped or failed?  If
> not, perhaps
>
> 		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
>
> is a better alternative.
Ack.
Show 31 quoted lines
>
>> +		git remote set-head --auto origin >output &&
>> +		echo "${SQ}origin/HEAD${SQ} is now created and points to ${SQ}main${SQ}" >expect &&
>> +		test_cmp expect output
>> +	)
>> +'
>
> Here, we could insert another one:
>
> test_expect_success 'set-head --auto to update a non symbolic ref' '
> 	(
> 		cd test &&
> 		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
> 		git update-ref refs/remotes/origin/HEAD HEAD &&
> 		git remote set-head --auto origin >output &&
>
> I'd imagine "output" should at least say that we are setting up a
> symref origin/HEAD to point at some ref the --auto option figured
> out, and if we wanted to report its previous state, it was a non
> symbolic ref that pointed at some commit.  In any case,
>
> 		echo "${SQ}origin/HEAD${SQ} is now created and points to ${SQ}main${SQ}" >expect &&
>
> is not what we want to see here, I suspect.
>
> Can we detect the case where we overwrite a non symref with a symref
> without going back to step 2/8 and doing a major surgery?
>
> 		test_cmp expect output
> 	)
> '

I agree, adding this makes sense. And this also takes us back to the question of what we should do in 2/8 when refs_read_symbolic_ref exits with 1. I now tested the behaviour and if origin/HEAD is gibberish, git already dies before with

error: cannot lock ref 'refs/remotes/origin/HEAD': unable to resolve reference 'refs/remotes/origin/HEAD': reference broken

so refs_read_symbolic_ref -> 1 only happens if there's a valid non-symbolic ref in origin/HEAD. So maybe if we put "Not a symbolic reference." in the referent (which should be an invalid symref), the caller could check for that and then should be able to distinguish this special case?

Thanks, Bence

-- 
bence.ferdinandy.com
Bence Ferdinandy· Nov 15, 2024, 23:13 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v12 4/8] remote set-head: better output for --auto

On Fri Nov 15, 2024 at 23:49, Bence Ferdinandy <bence@ferdinandy.com> wrote:
Show 91 quoted lines
>
> On Fri Nov 15, 2024 at 06:50, Junio C Hamano <gitster@pobox.com> wrote:
>> Bence Ferdinandy <bence@ferdinandy.com> writes:
>>
>>> Currently, set-head --auto will print a message saying "remote/HEAD set
>>> to branch", which implies something was changed.
>>>
>>> Change the output of --auto, so the output actually reflects what was
>>> done: a) set a previously unset HEAD, b) change HEAD because remote
>>> changed or c) no updates. As a fourth output, if HEAD is changed from
>>> a previous value that was not a remote branch, explicitly call attention
>>> to this fact.
>>
>> OK.  That's sensible.
>>
>> There is a slight variant of the fourth case.  HEAD may have been a
>> symbolic ref that pointed at an unexpected place (which you
>> addressed), or HEAD may have been a non-symbolic ref (which the new
>> code would mistakenly say "HEAD is now created", if I am reading the
>> patch correctly).
>
> Good point, and yes, that is what happens. (Although I'm not quite sure how
> valid that state is where a remote's HEAD is not a branch).
>
>>
>>> diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
>>> index 9b50276646..0ea86d51a4 100755
>>> --- a/t/t5505-remote.sh
>>> +++ b/t/t5505-remote.sh
>>> @@ -432,12 +432,51 @@ test_expect_success 'set-head --auto' '
>>>  	)
>>>  '
>>>  
>>> +test_expect_success 'set-head --auto detects creation' '
>>> +	(
>>> +		cd test &&
>>> +		git symbolic-ref -d refs/remotes/origin/HEAD &&
>>
>> Are we sure refs/remotes/origin/HEAD exists at this point in the
>> test, regardless of which earlier tests were skipped or failed?  If
>> not, perhaps
>>
>> 		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
>>
>> is a better alternative.
>
> Ack.
>
>>
>>> +		git remote set-head --auto origin >output &&
>>> +		echo "${SQ}origin/HEAD${SQ} is now created and points to ${SQ}main${SQ}" >expect &&
>>> +		test_cmp expect output
>>> +	)
>>> +'
>>
>> Here, we could insert another one:
>>
>> test_expect_success 'set-head --auto to update a non symbolic ref' '
>> 	(
>> 		cd test &&
>> 		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
>> 		git update-ref refs/remotes/origin/HEAD HEAD &&
>> 		git remote set-head --auto origin >output &&
>>
>> I'd imagine "output" should at least say that we are setting up a
>> symref origin/HEAD to point at some ref the --auto option figured
>> out, and if we wanted to report its previous state, it was a non
>> symbolic ref that pointed at some commit.  In any case,
>>
>> 		echo "${SQ}origin/HEAD${SQ} is now created and points to ${SQ}main${SQ}" >expect &&
>>
>> is not what we want to see here, I suspect.
>>
>> Can we detect the case where we overwrite a non symref with a symref
>> without going back to step 2/8 and doing a major surgery?
>>
>> 		test_cmp expect output
>> 	)
>> '
>
> I agree, adding this makes sense. And this also takes us back to the question
> of what we should do in 2/8 when refs_read_symbolic_ref exits with 1. I now
> tested the behaviour and if origin/HEAD is gibberish, git already dies before
> with 
>
> error: cannot lock ref 'refs/remotes/origin/HEAD': unable to resolve reference 'refs/remotes/origin/HEAD': reference broken
>
> so refs_read_symbolic_ref -> 1 only happens if there's a valid non-symbolic ref
> in origin/HEAD. So maybe if we put "Not a symbolic reference." in the referent
> (which should be an invalid symref), the caller could check for that and then
> should be able to distinguish this special case?

On second thought, it would maybe make even more sense to get the reference hash and put that into referent. In that case the output could still be

"'%s/HEAD' has changed from '%s' and now points to '%s'\n"
but with a non-symref after from.
Junio C Hamano· Nov 16, 2024, 00:22 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v12 4/8] remote set-head: better output for --auto

"Bence Ferdinandy" <bence@ferdinandy.com> writes:
Show 11 quoted lines
>> so refs_read_symbolic_ref -> 1 only happens if there's a valid non-symbolic ref
>> in origin/HEAD. So maybe if we put "Not a symbolic reference." in the referent
>> (which should be an invalid symref), the caller could check for that and then
>> should be able to distinguish this special case?
>
> On second thought, it would maybe make even more sense to get the reference
> hash and put that into referent. In that case the output could still be
>
> "'%s/HEAD' has changed from '%s' and now points to '%s'\n"
>
> but with a non-symref after from.

The output may look like the above, but people would certainly wish to clarify and/or translate "from '%s'" part. And to allow that, we may need to signal a bit more explicitly that we saw a non-symref HEAD there so that we can tell between a detached HEAD and a confusingly named branch whose name is 40-hex.

Otherwise, one who tries to update that "has changed from '%s'" message to clarify between "pointed at branch '%s'" and "pointed at commit '%s'" (or translate into a language where the distinction matters, perhaps because branch and commit have different gender there that affects conjugation for "has changed from" part, or something hand-wavy) have insufficient information to work with.

Thanks.
Junio C Hamano· Nov 16, 2024, 00:15 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v12 4/8] remote set-head: better output for --auto

"Bence Ferdinandy" <bence@ferdinandy.com> writes:
> Good point, and yes, that is what happens. (Although I'm not quite sure how
> valid that state is where a remote's HEAD is not a branch).

Not often when you cloned from a publishing repository, but if you cloned from a repository for your own work with an worktree, the HEAD at such an origin repository may have been detached, so it would be sensible (I think "git clone" historically guesses over eagerly to hide the detached HEAD state of the other side, though) if the clone's remote-tracking HEAD reflects it, I would think.

> ... So maybe if we put "Not a symbolic reference." in the referent
> (which should be an invalid symref), the caller could check for that and then
> should be able to distinguish this special case?
Yuck.

Are we limited by the narrow interface that only passes "referent", or are you in the position that allows you to extend the interface to "do it right"?

Bence Ferdinandy· Nov 16, 2024, 14:43 UTC · re: Junio C Hamano · lore

Re: [PATCH v12 4/8] remote set-head: better output for --auto

On Sat Nov 16, 2024 at 01:15, Junio C Hamano <gitster@pobox.com> wrote:
Show 21 quoted lines
> "Bence Ferdinandy" <bence@ferdinandy.com> writes:
>
>> Good point, and yes, that is what happens. (Although I'm not quite sure how
>> valid that state is where a remote's HEAD is not a branch).
>
> Not often when you cloned from a publishing repository, but if you
> cloned from a repository for your own work with an worktree, the
> HEAD at such an origin repository may have been detached, so it
> would be sensible (I think "git clone" historically guesses over
> eagerly to hide the detached HEAD state of the other side, though)
> if the clone's remote-tracking HEAD reflects it, I would think.
>
>> ... So maybe if we put "Not a symbolic reference." in the referent
>> (which should be an invalid symref), the caller could check for that and then
>> should be able to distinguish this special case?
>
> Yuck.
>
> Are we limited by the narrow interface that only passes "referent",
> or are you in the position that allows you to extend the interface
> to "do it right"?
How about we do the following if we can't read a symref:
1) remember to return a new exit code for refs_update_symref_extended (I guess 2),
   but continue on with updating HEAD
2) read the non-symref ref into "referent" (I think this should not be able to
   fail since, otherwise locking HEAD will already fail earlier)

this way the caller can check the exit code, see that it's 2, so not entirely a success, but HEAD has been updated and can report things accordingly.

Does that sounds more reasonable?

Thanks, Bence

Bence Ferdinandy· Oct 23, 2024, 15:36 UTC · re: Bence Ferdinandy · lore

[PATCH v12 5/8] refs: add TRANSACTION_CREATE_EXISTS error

Currently there is only one special error for transaction, for when there is a naming conflict, all other errors are dumped under a generic error. Add a new special error case for when the caller requests the reference to be updated only when it does not yet exist and the reference actually does exist.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    v5: no change
    v6: no change
    
    v7: - change commit prefix to be more in line with project standards
        - changed error checking to Karthik's suggestion
    
    v8: no change
    
    v9: - no change
    
    v10: no change
    
    v11: no change
    
    v12: no change
 refs.h                  |  4 +++-
 refs/files-backend.c    | 24 ++++++++++++++++--------
 refs/reftable-backend.c |  6 ++++--
 3 files changed, 23 insertions(+), 11 deletions(-)
Show changes to 3 files +23 −11

refs.h, refs/files-backend.c, refs/reftable-backend.c

diff --git a/refs.h b/refs.h
index 259191a485..a5bc25442b 100644
--- a/refs.h
+++ b/refs.h
@@ -762,8 +762,10 @@ int ref_transaction_verify(struct ref_transaction *transaction,
 
 /* Naming conflict (for example, the ref names A and A/B conflict). */
 #define TRANSACTION_NAME_CONFLICT -1
+/* When only creation was requested, but the ref already exists. */
+#define TRANSACTION_CREATE_EXISTS -2
 /* All other errors. */
-#define TRANSACTION_GENERIC_ERROR -2
+#define TRANSACTION_GENERIC_ERROR -3
 
 /*
  * Perform the preparatory stages of committing `transaction`. Acquire
diff --git a/refs/files-backend.c b/refs/files-backend.c
index 0824c0b8a9..e743ec44b5 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -2502,14 +2502,18 @@ static int split_symref_update(struct ref_update *update,
 static int check_old_oid(struct ref_update *update, struct object_id *oid,
 			 struct strbuf *err)
 {
+	int ret = TRANSACTION_GENERIC_ERROR;
+
 	if (!(update->flags & REF_HAVE_OLD) ||
 		   oideq(oid, &update->old_oid))
 		return 0;
 
-	if (is_null_oid(&update->old_oid))
+	if (is_null_oid(&update->old_oid)) {
 		strbuf_addf(err, "cannot lock ref '%s': "
 			    "reference already exists",
 			    ref_update_original_update_refname(update));
+		ret = TRANSACTION_CREATE_EXISTS;
+	}
 	else if (is_null_oid(oid))
 		strbuf_addf(err, "cannot lock ref '%s': "
 			    "reference is missing but expected %s",
@@ -2522,7 +2526,7 @@ static int check_old_oid(struct ref_update *update, struct object_id *oid,
 			    oid_to_hex(oid),
 			    oid_to_hex(&update->old_oid));
 
-	return -1;
+	return ret;
 }
 
 /*
@@ -2602,9 +2606,11 @@ static int lock_ref_for_update(struct files_ref_store *refs,
 					ret = TRANSACTION_GENERIC_ERROR;
 					goto out;
 				}
-			} else if  (check_old_oid(update, &lock->old_oid, err)) {
-				ret = TRANSACTION_GENERIC_ERROR;
-				goto out;
+			} else {
+				ret = check_old_oid(update, &lock->old_oid, err);
+				if  (ret) {
+					goto out;
+				}
 			}
 		} else {
 			/*
@@ -2635,9 +2641,11 @@ static int lock_ref_for_update(struct files_ref_store *refs,
 				    update->old_target);
 			ret = TRANSACTION_GENERIC_ERROR;
 			goto out;
-		} else if  (check_old_oid(update, &lock->old_oid, err)) {
-			ret = TRANSACTION_GENERIC_ERROR;
-			goto out;
+		} else {
+			ret = check_old_oid(update, &lock->old_oid, err);
+			if  (ret) {
+				goto out;
+			}
 		}
 
 		/*
diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
index 3c6107c7ce..b3b5ce77dd 100644
--- a/refs/reftable-backend.c
+++ b/refs/reftable-backend.c
@@ -1206,10 +1206,13 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 				goto done;
 			}
 		} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {
-			if (is_null_oid(&u->old_oid))
+			ret = TRANSACTION_NAME_CONFLICT;
+			if (is_null_oid(&u->old_oid)) {
 				strbuf_addf(err, _("cannot lock ref '%s': "
 						   "reference already exists"),
 					    ref_update_original_update_refname(u));
+				ret = TRANSACTION_CREATE_EXISTS;
+			}
 			else if (is_null_oid(&current_oid))
 				strbuf_addf(err, _("cannot lock ref '%s': "
 						   "reference is missing but expected %s"),
@@ -1221,7 +1224,6 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 					    ref_update_original_update_refname(u),
 					    oid_to_hex(&current_oid),
 					    oid_to_hex(&u->old_oid));
-			ret = -1;
 			goto done;
 		}
 
-- 
2.47.0.94.g1247fb88fd.dirty
Bence Ferdinandy· Oct 23, 2024, 15:36 UTC · re: Bence Ferdinandy · lore

[PATCH v12 6/8] refs: add create_only option to refs_update_symref_extended

Allow the caller to specify that it only wants to update the symref if it does not already exist. Silently ignore the error from the transaction API if the symref already exists.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    v5: no change
    
    v6: - switched from bool to int for create_only
        - refactored logic in refs_update_symref with goto as layed out by
          Junio
    
    v7: - change commit prefix to be more in line with project standards
        - refactored code to accommodate changes in the first patch, but
          otherwise no change
    
    v8: no change
    
    v9: - no change (except for following through the
          refs_update_symref_extended refactoring)
    
    v10: no change
    
    v11: no change
    
    v12: no change
 builtin/remote.c |  2 +-
 refs.c           | 32 +++++++++++++++++++++++---------
 refs.h           |  2 +-
 3 files changed, 25 insertions(+), 11 deletions(-)
Show changes to 3 files +25 −11

builtin/remote.c, refs.c, refs.h

diff --git a/builtin/remote.c b/builtin/remote.c
index 108f1271d3..b1eba75a2b 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1471,7 +1471,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		if (!refs_ref_exists(refs, b_remote_head.buf))
 			result |= error(_("Not a valid ref: %s"), b_remote_head.buf);
 		else if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
-					"remote set-head", &b_local_head))
+					"remote set-head", &b_local_head, 0))
 			result |= error(_("Could not setup %s"), b_head.buf);
 		else if (opt_a)
 			report_set_head_auto(argv[0], head_name, &b_local_head);
diff --git a/refs.c b/refs.c
index 24a4172cd2..093ee11ab0 100644
--- a/refs.c
+++ b/refs.c
@@ -2116,31 +2116,45 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
 int refs_update_symref(struct ref_store *refs, const char *ref,
 		       const char *target, const char *logmsg)
 {
-	return refs_update_symref_extended(refs, ref, target, logmsg, NULL);
+	return refs_update_symref_extended(refs, ref, target, logmsg, NULL, 0);
 }
 
 int refs_update_symref_extended(struct ref_store *refs, const char *ref,
 		       const char *target, const char *logmsg,
-		       struct strbuf *referent)
+		       struct strbuf *referent, int create_only)
 {
 	struct ref_transaction *transaction;
 	struct strbuf err = STRBUF_INIT;
-	int ret = 0;
+	int ret = 0, prepret = 0;
 
 	transaction = ref_store_transaction_begin(refs, &err);
-	if (!transaction ||
-		ref_transaction_update(transaction, ref, NULL, NULL,
-				   target, NULL, REF_NO_DEREF,
-				   logmsg, &err) ||
-		ref_transaction_prepare(transaction, &err)) {
+	if (!transaction) {
+	error_return:
 		ret = error("%s", err.buf);
 		goto cleanup;
 	}
+	if (create_only) {
+		if (ref_transaction_create(transaction, ref, NULL, target,
+					   REF_NO_DEREF, logmsg, &err))
+			goto error_return;
+		prepret = ref_transaction_prepare(transaction, &err);
+		if (prepret && prepret != TRANSACTION_CREATE_EXISTS)
+			goto error_return;
+	} else {
+		if (ref_transaction_update(transaction, ref, NULL, NULL,
+					   target, NULL, REF_NO_DEREF,
+					   logmsg, &err) ||
+			ref_transaction_prepare(transaction, &err))
+			goto error_return;
+	}
+
 	if (referent)
 		refs_read_symbolic_ref(refs, ref, referent);
 
+	if (prepret == TRANSACTION_CREATE_EXISTS)
+		goto cleanup;
 	if (ref_transaction_commit(transaction, &err))
-		ret = error("%s", err.buf);
+		goto error_return;
 
 cleanup:
 	strbuf_release(&err);
diff --git a/refs.h b/refs.h
index a5bc25442b..458582ebcf 100644
--- a/refs.h
+++ b/refs.h
@@ -575,7 +575,7 @@ int refs_update_symref(struct ref_store *refs, const char *refname,
 
 int refs_update_symref_extended(struct ref_store *refs, const char *refname,
 		       const char *target, const char *logmsg,
-		       struct strbuf *referent);
+		       struct strbuf *referent, int create_only);
 
 enum action_on_err {
 	UPDATE_REFS_MSG_ON_ERR,
-- 
2.47.0.94.g1247fb88fd.dirty
Bence Ferdinandy· Oct 23, 2024, 15:36 UTC · re: Bence Ferdinandy · lore

[PATCH v12 8/8] fetch set_head: handle mirrored bare repositories

When adding a remote to bare repository with "git remote add --mirror",
running fetch will fail to update HEAD to the remote's HEAD, since it
does not know how to handle bare repositories. On the other hand HEAD
already has content, since "git init --bare" has already set HEAD to
whatever is the default branch set for the user. Unless this - by chance
- is the same as the remote's HEAD, HEAD will be pointing to a bad
symref. Teach set_head to handle bare repositories, by overwriting HEAD
so it mirrors the remote's HEAD.

Note, that in this case overriding the local HEAD reference is necessary, since HEAD will exist before fetch can be run, but this should not be an issue, since the whole purpose of --mirror is to be an exact mirror of the remote, so following any changes to HEAD makes sense.

Also note, that although "git remote set-head" also fails when trying to update the remote's locally tracked HEAD in a mirrored bare repository, the usage of the command does not make much sense after this patch: fetch will update the remote HEAD correctly, and setting it manually to something else is antithetical to the concept of mirroring.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v10: - new patch
         - handles the issue discovered in
           https://lore.kernel.org/git/D4ZAELFWJMKN.S88LJ6YK31LZ@ferdinandy.com/T/
    
    v11: no change
    
    v12: no change
 builtin/fetch.c   | 16 ++++++++++++----
 t/t5505-remote.sh | 10 ++++++++++
 2 files changed, 22 insertions(+), 4 deletions(-)
Show changes to 2 files +22 −4

builtin/fetch.c, t/t5505-remote.sh

diff --git a/builtin/fetch.c b/builtin/fetch.c
index cadfd2407a..8af3efd6ac 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1633,14 +1633,22 @@ static int set_head(const struct ref *remote_refs)
 	else
 		head_name = xstrdup(heads.items[0].string);
 	if (head_name) {
-		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
-		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote, head_name);
+		int is_bare = is_bare_repository();
+		if (is_bare) {
+			strbuf_addstr(&b_head, "HEAD");
+			strbuf_addf(&b_remote_head, "refs/heads/%s", head_name);
+		} else {
+			strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
+			strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote, head_name);
+		}
 		/* make sure it's valid */
-		if (!refs_ref_exists(refs, b_remote_head.buf))
+		if (!is_bare && !refs_ref_exists(refs, b_remote_head.buf)) {
 			result = 1;
+		}
 		else if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
-					"fetch", &b_local_head, 1))
+					"fetch", &b_local_head, !is_bare)) {
 			result = 1;
+		}
 		else
 			report_set_head(remote, head_name, &b_local_head);
 
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 4990d00209..dfa78f3e8d 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -545,6 +545,16 @@ test_expect_success 'add --mirror && prune' '
 	)
 '
 
+test_expect_success 'add --mirror setting HEAD' '
+	mkdir headmirror &&
+	(
+		cd headmirror &&
+		git init --bare -b notmain &&
+		git remote add --mirror -f origin ../one &&
+		test "$(git symbolic-ref HEAD)" = "refs/heads/main"
+	)
+'
+
 test_expect_success 'add --mirror=fetch' '
 	mkdir mirror-fetch &&
 	git init -b main mirror-fetch/parent &&
-- 
2.47.0.94.g1247fb88fd.dirty
Bence Ferdinandy· Oct 23, 2024, 15:36 UTC · re: Bence Ferdinandy · lore

[PATCH v12 7/8] fetch: set remote/HEAD if it does not exist

If the user has remote/HEAD set already and it looks like it has changed on the server, then print a message, otherwise set it if we can. Silently pass if the user already has the same remote/HEAD set as reported by the server or if we encounter any errors along the way.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v3: - does not rely on remote set-head anymore so it only authenticates
        once
        - uses the new REF_CREATE_ONLY to atomically check if the ref exists
          and only write it if it doesn't
        - in all other cases the maximum it does is print a warning
    
    v4: - instead of the discarded REF_CREATE_ONLY, it uses the existing,
          but updated transaction api to request a silent create only
        - it now uses the atomic before_target to determine reporting
        - refactored for legibility
    
    v5: - instead of printing a not too useful message, it now fails
          silently, this in line with the objective to only set up
          remote/HEAD automatically if the right thing is trivial, for
          everything else there is remote set-head
        - fixed all failing tests
        - added two new tests, one for checking if remote/HEAD is set to the
          correct one, and one to test that we do not override remote/HEAD
          if it has changed on the server from what we have locally
    
    v6: - fixed style issues and unintended extra empty line
        - updated function call with bool to int from previous patch's
          change
        - removed calls to error(...) inherited from builtin/remote.c so we
          actually fail silently
        - set the test for remote set-head --auto to the correct value here,
          which was previously erronously set in the remote set-head patch
    
    v7: - no change
    
    v8: - changed logmsg in call to refs_update_symref from "remote
          set-head" to "fetch"
    
    v9: - follow through with refs_update_symref_extended
        - fix test errors uncovered by the new patch
    
    v10: no change
    
    v11: fixed some memory leaks
    
    v12: no change
 builtin/fetch.c                  |  86 ++++++++++++
 t/t4207-log-decoration-colors.sh |   3 +-
 t/t5505-remote.sh                |  21 ++-
 t/t5510-fetch.sh                 | 229 ++++++++++++++++---------------
 t/t5512-ls-remote.sh             |   2 +
 t/t5514-fetch-multiple.sh        |  17 ++-
 t/t5516-fetch-push.sh            |   3 +-
 t/t5527-fetch-odd-refs.sh        |   3 +-
 t/t7900-maintenance.sh           |   3 +-
 t/t9210-scalar.sh                |   5 +-
 t/t9211-scalar-clone.sh          |   6 +-
 t/t9902-completion.sh            |  65 +++++++++
 12 files changed, 319 insertions(+), 124 deletions(-)
Show changes to 12 files +319 −124

builtin/fetch.c, t/t4207-log-decoration-colors.sh, t/t5505-remote.sh, t/t5510-fetch.sh, t/t5512-ls-remote.sh, t/t5514-fetch-multiple.sh, t/t5516-fetch-push.sh, t/t5527-fetch-odd-refs.sh, t/t7900-maintenance.sh, t/t9210-scalar.sh, t/t9211-scalar-clone.sh, t/t9902-completion.sh

diff --git a/builtin/fetch.c b/builtin/fetch.c
index d9027e4dc9..cadfd2407a 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1578,6 +1578,84 @@ static int backfill_tags(struct display_state *display_state,
 	return retcode;
 }
 
+static void report_set_head(const char *remote, const char *head_name,
+			struct strbuf *buf_prev) {
+	struct strbuf buf_prefix = STRBUF_INIT;
+	const char *prev_head = NULL;
+
+	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
+	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
+
+	if (prev_head && strcmp(prev_head, head_name)) {
+		printf("'HEAD' at '%s' has changed from '%s' to '%s'\n",
+			remote, prev_head, head_name);
+		printf("Run 'git remote set-head %s %s' to follow the change.\n",
+			remote, head_name);
+	}
+	strbuf_release(&buf_prefix);
+}
+
+static const char *strip_refshead(const char *name){
+	skip_prefix(name, "refs/heads/", &name);
+	return name;
+}
+
+static int set_head(const struct ref *remote_refs)
+{
+	int result = 0;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
+		b_local_head = STRBUF_INIT;
+	const char *remote = gtransport->remote->name;
+	char *head_name = NULL;
+	struct ref *ref, *matches;
+	struct ref *fetch_map = NULL, **fetch_map_tail = &fetch_map;
+	struct refspec_item refspec = {
+		.force = 0,
+		.pattern = 1,
+		.src = (char *) "refs/heads/*",
+		.dst = (char *) "refs/heads/*",
+	};
+	struct string_list heads = STRING_LIST_INIT_DUP;
+	struct ref_store *refs = get_main_ref_store(the_repository);
+
+	get_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);
+	matches = guess_remote_head(find_ref_by_name(remote_refs, "HEAD"),
+				    fetch_map, 1);
+	for (ref = matches; ref; ref = ref->next) {
+		string_list_append(&heads, strip_refshead(ref->name));
+	}
+
+
+	if (!heads.nr)
+		result = 1;
+	else if (heads.nr > 1)
+		result = 1;
+	else
+		head_name = xstrdup(heads.items[0].string);
+	if (head_name) {
+		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
+		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote, head_name);
+		/* make sure it's valid */
+		if (!refs_ref_exists(refs, b_remote_head.buf))
+			result = 1;
+		else if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
+					"fetch", &b_local_head, 1))
+			result = 1;
+		else
+			report_set_head(remote, head_name, &b_local_head);
+
+		free(head_name);
+	}
+
+	free_refs(fetch_map);
+	free_refs(matches);
+	string_list_clear(&heads, 0);
+	strbuf_release(&b_head);
+	strbuf_release(&b_local_head);
+	strbuf_release(&b_remote_head);
+	return result;
+}
+
 static int do_fetch(struct transport *transport,
 		    struct refspec *rs,
 		    const struct fetch_config *config)
@@ -1647,6 +1725,8 @@ static int do_fetch(struct transport *transport,
 				    "refs/tags/");
 	}
 
+	strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
+
 	if (must_list_refs) {
 		trace2_region_enter("fetch", "remote_refs", the_repository);
 		remote_refs = transport_get_remote_refs(transport,
@@ -1791,6 +1871,12 @@ static int do_fetch(struct transport *transport,
 				  "you need to specify exactly one branch with the --set-upstream option"));
 		}
 	}
+	if (set_head(remote_refs))
+		;
+		/*
+		 * Way too many cases where this can go wrong
+		 * so let's just fail silently for now.
+		 */
 
 cleanup:
 	if (retcode) {
diff --git a/t/t4207-log-decoration-colors.sh b/t/t4207-log-decoration-colors.sh
index 73ea9e5155..d55d22cb2f 100755
--- a/t/t4207-log-decoration-colors.sh
+++ b/t/t4207-log-decoration-colors.sh
@@ -59,7 +59,8 @@ ${c_reset}${c_tag}tag: ${c_reset}${c_tag}v1.0${c_reset}${c_commit}, \
 ${c_reset}${c_tag}tag: ${c_reset}${c_tag}B${c_reset}${c_commit})${c_reset} B
 ${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
 ${c_tag}tag: ${c_reset}${c_tag}A1${c_reset}${c_commit}, \
-${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit})${c_reset} A1
+${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit}, \
+${c_reset}${c_remoteBranch}other/HEAD${c_reset}${c_commit})${c_reset} A1
 	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
 ${c_stash}refs/stash${c_reset}${c_commit})${c_reset} On main: Changes to A.t
 	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 0ea86d51a4..4990d00209 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -74,7 +74,7 @@ test_expect_success 'add another remote' '
 		cd test &&
 		git remote add -f second ../two &&
 		tokens_match "origin second" "$(git remote)" &&
-		check_tracking_branch second main side another &&
+		check_tracking_branch second main side another HEAD &&
 		git for-each-ref "--format=%(refname)" refs/remotes |
 		sed -e "/^refs\/remotes\/origin\//d" \
 		    -e "/^refs\/remotes\/second\//d" >actual &&
@@ -476,7 +476,7 @@ test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
 		cd test &&
 		git fetch two "refs/heads/*:refs/remotes/two/*" &&
 		git remote set-head --auto two >output 2>&1 &&
-		echo "${SQ}two/HEAD${SQ} is now created and points to ${SQ}main${SQ}" >expect &&
+		echo "${SQ}two/HEAD${SQ} is unchanged and points to ${SQ}main${SQ}" >expect &&
 		test_cmp expect output
 	)
 '
@@ -764,8 +764,10 @@ test_expect_success 'reject --no-no-tags' '
 '
 
 cat >one/expect <<\EOF
+  apis/HEAD -> apis/main
   apis/main
   apis/side
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
@@ -783,11 +785,14 @@ test_expect_success 'update' '
 '
 
 cat >one/expect <<\EOF
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
+  manduca/HEAD -> manduca/main
   manduca/main
   manduca/side
+  megaloprepus/HEAD -> megaloprepus/main
   megaloprepus/main
   megaloprepus/side
 EOF
@@ -795,7 +800,7 @@ EOF
 test_expect_success 'update with arguments' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -827,10 +832,13 @@ test_expect_success 'update --prune' '
 '
 
 cat >one/expect <<-\EOF
+  apis/HEAD -> apis/main
   apis/main
   apis/side
+  manduca/HEAD -> manduca/main
   manduca/main
   manduca/side
+  megaloprepus/HEAD -> megaloprepus/main
   megaloprepus/main
   megaloprepus/side
 EOF
@@ -838,7 +846,7 @@ EOF
 test_expect_success 'update default' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -850,6 +858,7 @@ test_expect_success 'update default' '
 '
 
 cat >one/expect <<\EOF
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
@@ -858,7 +867,7 @@ EOF
 test_expect_success 'update default (overridden, with funny whitespace)' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -872,7 +881,7 @@ test_expect_success 'update default (overridden, with funny whitespace)' '
 test_expect_success 'update (with remotes.default defined)' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index 0890b9f61c..dfc8d748ba 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -75,6 +75,30 @@ test_expect_success "fetch test for-merge" '
 	cut -f -2 .git/FETCH_HEAD >actual &&
 	test_cmp expected actual'
 
+test_expect_success "fetch test remote HEAD" '
+	cd "$D" &&
+	cd two &&
+	git fetch &&
+	git rev-parse --verify refs/remotes/origin/HEAD &&
+	git rev-parse --verify refs/remotes/origin/main &&
+	head=$(git rev-parse refs/remotes/origin/HEAD) &&
+	branch=$(git rev-parse refs/remotes/origin/main) &&
+	test "z$head" = "z$branch"'
+
+test_expect_success "fetch test remote HEAD change" '
+	cd "$D" &&
+	cd two &&
+	git switch -c other &&
+	git push -u origin other &&
+	git rev-parse --verify refs/remotes/origin/HEAD &&
+	git rev-parse --verify refs/remotes/origin/main &&
+	git rev-parse --verify refs/remotes/origin/other &&
+	git remote set-head origin other &&
+	git fetch &&
+	head=$(git rev-parse refs/remotes/origin/HEAD) &&
+	branch=$(git rev-parse refs/remotes/origin/other) &&
+	test "z$head" = "z$branch"'
+
 test_expect_success 'fetch --prune on its own works as expected' '
 	cd "$D" &&
 	git clone . prune &&
@@ -478,7 +502,6 @@ test_expect_success 'unbundle 1' '
 	test_must_fail git fetch "$D/bundle1" main:main
 '
 
-
 test_expect_success 'bundle 1 has only 3 files ' '
 	cd "$D" &&
 	test_bundle_object_count bundle1 3
@@ -819,7 +842,7 @@ test_expect_success 'fetch from multiple configured URLs in single remote' '
 
 # configured prune tests
 
-set_config_tristate () {
+set_config_tristate() {
 	# var=$1 val=$2
 	case "$2" in
 	unset)
@@ -833,12 +856,12 @@ set_config_tristate () {
 	esac
 }
 
-test_configured_prune () {
+test_configured_prune() {
 	test_configured_prune_type "$@" "name"
 	test_configured_prune_type "$@" "link"
 }
 
-test_configured_prune_type () {
+test_configured_prune_type() {
 	fetch_prune=$1
 	remote_origin_prune=$2
 	fetch_prune_tags=$3
@@ -848,8 +871,7 @@ test_configured_prune_type () {
 	cmdline=$7
 	mode=$8
 
-	if test -z "$cmdline_setup"
-	then
+	if test -z "$cmdline_setup"; then
 		test_expect_success 'setup cmdline_setup variable for subsequent test' '
 			remote_url="file://$(git -C one config remote.origin.url)" &&
 			remote_fetch="$(git -C one config remote.origin.fetch)" &&
@@ -857,12 +879,10 @@ test_configured_prune_type () {
 		'
 	fi
 
-	if test "$mode" = 'link'
-	then
+	if test "$mode" = 'link'; then
 		new_cmdline=""
 
-		if test "$cmdline" = ""
-		then
+		if test "$cmdline" = ""; then
 			new_cmdline=$cmdline_setup
 		else
 			new_cmdline=$(perl -e '
@@ -873,10 +893,8 @@ test_configured_prune_type () {
 		fi
 
 		if test "$fetch_prune_tags" = 'true' ||
-		   test "$remote_origin_prune_tags" = 'true'
-		then
-			if ! printf '%s' "$cmdline\n" | grep -q refs/remotes/origin/
-			then
+			test "$remote_origin_prune_tags" = 'true'; then
+			if ! printf '%s' "$cmdline\n" | grep -q refs/remotes/origin/; then
 				new_cmdline="$new_cmdline refs/tags/*:refs/tags/*"
 			fi
 		fi
@@ -946,100 +964,100 @@ test_configured_prune_type () {
 # $7 git-fetch $cmdline:
 #
 #                     $1    $2    $3    $4    $5     $6     $7
-test_configured_prune unset unset unset unset kept   kept   ""
-test_configured_prune unset unset unset unset kept   kept   "--no-prune"
-test_configured_prune unset unset unset unset pruned kept   "--prune"
-test_configured_prune unset unset unset unset kept   pruned \
+test_configured_prune unset unset unset unset kept kept ""
+test_configured_prune unset unset unset unset kept kept "--no-prune"
+test_configured_prune unset unset unset unset pruned kept "--prune"
+test_configured_prune unset unset unset unset kept pruned \
 	"--prune origin refs/tags/*:refs/tags/*"
 test_configured_prune unset unset unset unset pruned pruned \
 	"--prune origin refs/tags/*:refs/tags/* +refs/heads/*:refs/remotes/origin/*"
 
-test_configured_prune false unset unset unset kept   kept   ""
-test_configured_prune false unset unset unset kept   kept   "--no-prune"
-test_configured_prune false unset unset unset pruned kept   "--prune"
+test_configured_prune false unset unset unset kept kept ""
+test_configured_prune false unset unset unset kept kept "--no-prune"
+test_configured_prune false unset unset unset pruned kept "--prune"
 
-test_configured_prune true  unset unset unset pruned kept   ""
-test_configured_prune true  unset unset unset pruned kept   "--prune"
-test_configured_prune true  unset unset unset kept   kept   "--no-prune"
+test_configured_prune true unset unset unset pruned kept ""
+test_configured_prune true unset unset unset pruned kept "--prune"
+test_configured_prune true unset unset unset kept kept "--no-prune"
 
-test_configured_prune unset false unset unset kept   kept   ""
-test_configured_prune unset false unset unset kept   kept   "--no-prune"
-test_configured_prune unset false unset unset pruned kept   "--prune"
+test_configured_prune unset false unset unset kept kept ""
+test_configured_prune unset false unset unset kept kept "--no-prune"
+test_configured_prune unset false unset unset pruned kept "--prune"
 
-test_configured_prune false false unset unset kept   kept   ""
-test_configured_prune false false unset unset kept   kept   "--no-prune"
-test_configured_prune false false unset unset pruned kept   "--prune"
-test_configured_prune false false unset unset kept   pruned \
+test_configured_prune false false unset unset kept kept ""
+test_configured_prune false false unset unset kept kept "--no-prune"
+test_configured_prune false false unset unset pruned kept "--prune"
+test_configured_prune false false unset unset kept pruned \
 	"--prune origin refs/tags/*:refs/tags/*"
 test_configured_prune false false unset unset pruned pruned \
 	"--prune origin refs/tags/*:refs/tags/* +refs/heads/*:refs/remotes/origin/*"
 
-test_configured_prune true  false unset unset kept   kept   ""
-test_configured_prune true  false unset unset pruned kept   "--prune"
-test_configured_prune true  false unset unset kept   kept   "--no-prune"
+test_configured_prune true false unset unset kept kept ""
+test_configured_prune true false unset unset pruned kept "--prune"
+test_configured_prune true false unset unset kept kept "--no-prune"
 
-test_configured_prune unset true  unset unset pruned kept   ""
-test_configured_prune unset true  unset unset kept   kept   "--no-prune"
-test_configured_prune unset true  unset unset pruned kept   "--prune"
+test_configured_prune unset true unset unset pruned kept ""
+test_configured_prune unset true unset unset kept kept "--no-prune"
+test_configured_prune unset true unset unset pruned kept "--prune"
 
-test_configured_prune false true  unset unset pruned kept   ""
-test_configured_prune false true  unset unset kept   kept   "--no-prune"
-test_configured_prune false true  unset unset pruned kept   "--prune"
+test_configured_prune false true unset unset pruned kept ""
+test_configured_prune false true unset unset kept kept "--no-prune"
+test_configured_prune false true unset unset pruned kept "--prune"
 
-test_configured_prune true  true  unset unset pruned kept   ""
-test_configured_prune true  true  unset unset pruned kept   "--prune"
-test_configured_prune true  true  unset unset kept   kept   "--no-prune"
-test_configured_prune true  true  unset unset kept   pruned \
+test_configured_prune true true unset unset pruned kept ""
+test_configured_prune true true unset unset pruned kept "--prune"
+test_configured_prune true true unset unset kept kept "--no-prune"
+test_configured_prune true true unset unset kept pruned \
 	"--prune origin refs/tags/*:refs/tags/*"
-test_configured_prune true  true  unset unset pruned pruned \
+test_configured_prune true true unset unset pruned pruned \
 	"--prune origin refs/tags/*:refs/tags/* +refs/heads/*:refs/remotes/origin/*"
 
 # --prune-tags on its own does nothing, needs --prune as well, same
 # for fetch.pruneTags without fetch.prune
-test_configured_prune unset unset unset unset kept kept     "--prune-tags"
-test_configured_prune unset unset true unset  kept kept     ""
-test_configured_prune unset unset unset true  kept kept     ""
+test_configured_prune unset unset unset unset kept kept "--prune-tags"
+test_configured_prune unset unset true unset kept kept ""
+test_configured_prune unset unset unset true kept kept ""
 
 # These will prune the tags
 test_configured_prune unset unset unset unset pruned pruned "--prune --prune-tags"
-test_configured_prune true  unset true  unset pruned pruned ""
-test_configured_prune unset true  unset true  pruned pruned ""
+test_configured_prune true unset true unset pruned pruned ""
+test_configured_prune unset true unset true pruned pruned ""
 
 # remote.<name>.pruneTags overrides fetch.pruneTags, just like
 # remote.<name>.prune overrides fetch.prune if set.
-test_configured_prune true  unset true unset pruned pruned  ""
-test_configured_prune false true  false true  pruned pruned ""
-test_configured_prune true  false true  false kept   kept   ""
+test_configured_prune true unset true unset pruned pruned ""
+test_configured_prune false true false true pruned pruned ""
+test_configured_prune true false true false kept kept ""
 
 # When --prune-tags is supplied it's ignored if an explicit refspec is
 # given, same for the configuration options.
 test_configured_prune unset unset unset unset pruned kept \
 	"--prune --prune-tags origin +refs/heads/*:refs/remotes/origin/*"
-test_configured_prune unset unset true  unset pruned kept \
+test_configured_prune unset unset true unset pruned kept \
 	"--prune origin +refs/heads/*:refs/remotes/origin/*"
-test_configured_prune unset unset unset true pruned  kept \
+test_configured_prune unset unset unset true pruned kept \
 	"--prune origin +refs/heads/*:refs/remotes/origin/*"
 
 # Pruning that also takes place if a file:// url replaces a named
 # remote. However, because there's no implicit
 # +refs/heads/*:refs/remotes/origin/* refspec and supplying it on the
 # command-line negates --prune-tags, the branches will not be pruned.
-test_configured_prune_type unset unset unset unset kept   kept   "origin --prune-tags" "name"
-test_configured_prune_type unset unset unset unset kept   kept   "origin --prune-tags" "link"
+test_configured_prune_type unset unset unset unset kept kept "origin --prune-tags" "name"
+test_configured_prune_type unset unset unset unset kept kept "origin --prune-tags" "link"
 test_configured_prune_type unset unset unset unset pruned pruned "origin --prune --prune-tags" "name"
-test_configured_prune_type unset unset unset unset kept   pruned "origin --prune --prune-tags" "link"
+test_configured_prune_type unset unset unset unset kept pruned "origin --prune --prune-tags" "link"
 test_configured_prune_type unset unset unset unset pruned pruned "--prune --prune-tags origin" "name"
-test_configured_prune_type unset unset unset unset kept   pruned "--prune --prune-tags origin" "link"
-test_configured_prune_type unset unset true  unset pruned pruned "--prune origin" "name"
-test_configured_prune_type unset unset true  unset kept   pruned "--prune origin" "link"
-test_configured_prune_type unset unset unset true  pruned pruned "--prune origin" "name"
-test_configured_prune_type unset unset unset true  kept   pruned "--prune origin" "link"
-test_configured_prune_type true  unset true  unset pruned pruned "origin" "name"
-test_configured_prune_type true  unset true  unset kept   pruned "origin" "link"
-test_configured_prune_type unset  true true  unset pruned pruned "origin" "name"
-test_configured_prune_type unset  true true  unset kept   pruned "origin" "link"
-test_configured_prune_type unset  true unset true  pruned pruned "origin" "name"
-test_configured_prune_type unset  true unset true  kept   pruned "origin" "link"
+test_configured_prune_type unset unset unset unset kept pruned "--prune --prune-tags origin" "link"
+test_configured_prune_type unset unset true unset pruned pruned "--prune origin" "name"
+test_configured_prune_type unset unset true unset kept pruned "--prune origin" "link"
+test_configured_prune_type unset unset unset true pruned pruned "--prune origin" "name"
+test_configured_prune_type unset unset unset true kept pruned "--prune origin" "link"
+test_configured_prune_type true unset true unset pruned pruned "origin" "name"
+test_configured_prune_type true unset true unset kept pruned "origin" "link"
+test_configured_prune_type unset true true unset pruned pruned "origin" "name"
+test_configured_prune_type unset true true unset kept pruned "origin" "link"
+test_configured_prune_type unset true unset true pruned pruned "origin" "name"
+test_configured_prune_type unset true unset true kept pruned "origin" "link"
 
 # When all remote.origin.fetch settings are deleted a --prune
 # --prune-tags still implicitly supplies refs/tags/*:refs/tags/* so
@@ -1137,8 +1155,7 @@ test_expect_success 'fetching with auto-gc does not lock up' '
 	)
 '
 
-for section in fetch transfer
-do
+for section in fetch transfer; do
 	test_expect_success "$section.hideRefs affects connectivity check" '
 		GIT_TRACE="$PWD"/trace git -c $section.hideRefs=refs -c \
 			$section.hideRefs="!refs/tags/" fetch &&
@@ -1157,21 +1174,23 @@ test_expect_success 'prepare source branch' '
 	test 3 -le $(wc -l <actual)
 '
 
-validate_store_type () {
+validate_store_type() {
 	git -C dest count-objects -v >actual &&
-	case "$store_type" in
-	packed)
-		grep "^count: 0$" actual ;;
-	loose)
-		grep "^packs: 0$" actual ;;
-	esac || {
+		case "$store_type" in
+		packed)
+			grep "^count: 0$" actual
+			;;
+		loose)
+			grep "^packs: 0$" actual
+			;;
+		esac || {
 		echo "store_type is $store_type"
 		cat actual
 		false
 	}
 }
 
-test_unpack_limit () {
+test_unpack_limit() {
 	store_type=$1
 
 	case "$store_type" in
@@ -1192,43 +1211,39 @@ test_unpack_limit () {
 test_unpack_limit packed
 test_unpack_limit loose
 
-setup_negotiation_tip () {
+setup_negotiation_tip() {
 	SERVER="$1"
 	URL="$2"
 	USE_PROTOCOL_V2="$3"
 
 	rm -rf "$SERVER" client trace &&
-	git init -b main "$SERVER" &&
-	test_commit -C "$SERVER" alpha_1 &&
-	test_commit -C "$SERVER" alpha_2 &&
-	git -C "$SERVER" checkout --orphan beta &&
-	test_commit -C "$SERVER" beta_1 &&
-	test_commit -C "$SERVER" beta_2 &&
-
-	git clone "$URL" client &&
-
-	if test "$USE_PROTOCOL_V2" -eq 1
-	then
-		git -C "$SERVER" config protocol.version 2 &&
-		git -C client config protocol.version 2
-	fi &&
-
-	test_commit -C "$SERVER" beta_s &&
-	git -C "$SERVER" checkout main &&
-	test_commit -C "$SERVER" alpha_s &&
-	git -C "$SERVER" tag -d alpha_1 alpha_2 beta_1 beta_2
+		git init -b main "$SERVER" &&
+		test_commit -C "$SERVER" alpha_1 &&
+		test_commit -C "$SERVER" alpha_2 &&
+		git -C "$SERVER" checkout --orphan beta &&
+		test_commit -C "$SERVER" beta_1 &&
+		test_commit -C "$SERVER" beta_2 &&
+		git clone "$URL" client &&
+		if test "$USE_PROTOCOL_V2" -eq 1; then
+			git -C "$SERVER" config protocol.version 2 &&
+				git -C client config protocol.version 2
+		fi &&
+		test_commit -C "$SERVER" beta_s &&
+		git -C "$SERVER" checkout main &&
+		test_commit -C "$SERVER" alpha_s &&
+		git -C "$SERVER" tag -d alpha_1 alpha_2 beta_1 beta_2
 }
 
-check_negotiation_tip () {
+check_negotiation_tip() {
 	# Ensure that {alpha,beta}_1 are sent as "have", but not {alpha_beta}_2
 	ALPHA_1=$(git -C client rev-parse alpha_1) &&
-	grep "fetch> have $ALPHA_1" trace &&
-	BETA_1=$(git -C client rev-parse beta_1) &&
-	grep "fetch> have $BETA_1" trace &&
-	ALPHA_2=$(git -C client rev-parse alpha_2) &&
-	! grep "fetch> have $ALPHA_2" trace &&
-	BETA_2=$(git -C client rev-parse beta_2) &&
-	! grep "fetch> have $BETA_2" trace
+		grep "fetch> have $ALPHA_1" trace &&
+		BETA_1=$(git -C client rev-parse beta_1) &&
+		grep "fetch> have $BETA_1" trace &&
+		ALPHA_2=$(git -C client rev-parse alpha_2) &&
+		! grep "fetch> have $ALPHA_2" trace &&
+		BETA_2=$(git -C client rev-parse beta_2) &&
+		! grep "fetch> have $BETA_2" trace
 }
 
 test_expect_success '--negotiation-tip limits "have" lines sent' '
diff --git a/t/t5512-ls-remote.sh b/t/t5512-ls-remote.sh
index 64b3491e4e..1b3865e154 100755
--- a/t/t5512-ls-remote.sh
+++ b/t/t5512-ls-remote.sh
@@ -293,6 +293,8 @@ test_expect_success 'ls-remote with filtered symref (refname)' '
 	cat >expect <<-EOF &&
 	ref: refs/heads/main	HEAD
 	$rev	HEAD
+	ref: refs/remotes/origin/main	refs/remotes/origin/HEAD
+	$rev	refs/remotes/origin/HEAD
 	EOF
 	git ls-remote --symref . HEAD >actual &&
 	test_cmp expect actual
diff --git a/t/t5514-fetch-multiple.sh b/t/t5514-fetch-multiple.sh
index 579872c258..e3482b27b2 100755
--- a/t/t5514-fetch-multiple.sh
+++ b/t/t5514-fetch-multiple.sh
@@ -45,14 +45,17 @@ test_expect_success setup '
 '
 
 cat > test/expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
   origin/HEAD -> origin/main
   origin/main
   origin/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -97,6 +100,7 @@ cat > expect << EOF
   origin/HEAD -> origin/main
   origin/main
   origin/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
@@ -112,8 +116,10 @@ test_expect_success 'git fetch --multiple (but only one remote)' '
 '
 
 cat > expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -141,7 +147,7 @@ test_expect_success 'git fetch --multiple (bad remote names)' '
 
 test_expect_success 'git fetch --all (skipFetchAll)' '
 	(cd test4 &&
-	 for b in $(git branch -r)
+	 for b in $(git branch -r | grep -v HEAD)
 	 do
 		git branch -r -d $b || exit 1
 	 done &&
@@ -153,11 +159,14 @@ test_expect_success 'git fetch --all (skipFetchAll)' '
 '
 
 cat > expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -165,7 +174,7 @@ EOF
 
 test_expect_success 'git fetch --multiple (ignoring skipFetchAll)' '
 	(cd test4 &&
-	 for b in $(git branch -r)
+	 for b in $(git branch -r | grep -v HEAD)
 	 do
 		git branch -r -d $b || exit 1
 	 done &&
@@ -221,14 +230,17 @@ test_expect_success 'git fetch --multiple --jobs=0 picks a default' '
 
 create_fetch_all_expect () {
 	cat >expect <<-\EOF
+	  one/HEAD -> one/main
 	  one/main
 	  one/side
 	  origin/HEAD -> origin/main
 	  origin/main
 	  origin/side
+	  three/HEAD -> three/main
 	  three/another
 	  three/main
 	  three/side
+	  two/HEAD -> two/main
 	  two/another
 	  two/main
 	  two/side
@@ -265,6 +277,7 @@ test_expect_success 'git fetch (fetch all remotes with fetch.all = true)' '
 
 create_fetch_one_expect () {
 	cat >expect <<-\EOF
+	  one/HEAD -> one/main
 	  one/main
 	  one/side
 	  origin/HEAD -> origin/main
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 331778bd42..5a051aa0c7 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1395,7 +1395,8 @@ test_expect_success 'fetch follows tags by default' '
 		git tag -m "annotated" tag &&
 		git for-each-ref >tmp1 &&
 		sed -n "p; s|refs/heads/main$|refs/remotes/origin/main|p" tmp1 |
-		sort -k 3 >../expect
+		sed -n "p; s|refs/heads/main$|refs/remotes/origin/HEAD|p"  |
+		sort -k 4 >../expect
 	) &&
 	test_when_finished "rm -rf dst" &&
 	git init dst &&
diff --git a/t/t5527-fetch-odd-refs.sh b/t/t5527-fetch-odd-refs.sh
index 98ece27c6a..d3996af6ee 100755
--- a/t/t5527-fetch-odd-refs.sh
+++ b/t/t5527-fetch-odd-refs.sh
@@ -52,7 +52,8 @@ test_expect_success LONG_REF 'fetch handles extremely long refname' '
 	long
 	main
 	EOF
-	git for-each-ref --format="%(subject)" refs/remotes/long >actual &&
+	git for-each-ref --format="%(subject)" refs/remotes/long \
+		--exclude=refs/remotes/long/HEAD >actual &&
 	test_cmp expect actual
 '
 
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index a66d0e089d..88331d1ceb 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -329,7 +329,8 @@ test_expect_success 'incremental-repack task' '
 
 	# Delete refs that have not been repacked in these packs.
 	git for-each-ref --format="delete %(refname)" \
-		refs/prefetch refs/tags refs/remotes >refs &&
+		refs/prefetch refs/tags refs/remotes \
+		--exclude=refs/remotes/*/HEAD >refs &&
 	git update-ref --stdin <refs &&
 
 	# Replace the object directory with this pack layout.
diff --git a/t/t9210-scalar.sh b/t/t9210-scalar.sh
index a30b2c9f70..2237844550 100755
--- a/t/t9210-scalar.sh
+++ b/t/t9210-scalar.sh
@@ -151,7 +151,8 @@ test_expect_success 'scalar clone' '
 			"$(pwd)" &&
 
 		git for-each-ref --format="%(refname)" refs/remotes/origin/ >actual &&
-		echo "refs/remotes/origin/parallel" >expect &&
+		echo "refs/remotes/origin/HEAD" >>expect &&
+		echo "refs/remotes/origin/parallel" >>expect &&
 		test_cmp expect actual &&
 
 		test_path_is_missing 1/2 &&
@@ -220,7 +221,7 @@ test_expect_success 'scalar reconfigure --all with includeIf.onbranch' '
 	done
 '
 
- test_expect_success 'scalar reconfigure --all with detached HEADs' '
+test_expect_success 'scalar reconfigure --all with detached HEADs' '
 	repos="two three four" &&
 	for num in $repos
 	do
diff --git a/t/t9211-scalar-clone.sh b/t/t9211-scalar-clone.sh
index c16ea67c1d..d9cb6b9a3e 100755
--- a/t/t9211-scalar-clone.sh
+++ b/t/t9211-scalar-clone.sh
@@ -32,7 +32,7 @@ test_expect_success 'set up repository to clone' '
 	)
 '
 
-cleanup_clone () {
+cleanup_clone() {
 	rm -rf "$1"
 }
 
@@ -128,7 +128,7 @@ test_expect_success '--single-branch clones HEAD only' '
 	(
 		cd $enlistment/src &&
 		git for-each-ref refs/remotes/origin >out &&
-		test_line_count = 1 out &&
+		test_line_count = 2 out &&
 		grep "refs/remotes/origin/base" out
 	) &&
 
@@ -142,7 +142,7 @@ test_expect_success '--no-single-branch clones all branches' '
 	(
 		cd $enlistment/src &&
 		git for-each-ref refs/remotes/origin >out &&
-		test_line_count = 2 out &&
+		test_line_count = 3 out &&
 		grep "refs/remotes/origin/base" out &&
 		grep "refs/remotes/origin/parallel" out
 	) &&
diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh
index cc6aa9f0cd..b663c4609e 100755
--- a/t/t9902-completion.sh
+++ b/t/t9902-completion.sh
@@ -658,6 +658,7 @@ test_expect_success '__git_refs - simple' '
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	matching-tag
@@ -673,6 +674,7 @@ test_expect_success '__git_refs - full refs' '
 	cat >expected <<-EOF &&
 	refs/heads/main
 	refs/heads/matching-branch
+	refs/remotes/other/HEAD
 	refs/remotes/other/branch-in-other
 	refs/remotes/other/main-in-other
 	refs/tags/matching-tag
@@ -729,6 +731,7 @@ test_expect_success '__git_refs - remote on local file system - full refs' '
 test_expect_success '__git_refs - configured remote' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -756,6 +759,7 @@ test_expect_success '__git_refs - configured remote - full refs' '
 test_expect_success '__git_refs - configured remote - repo given on the command line' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -787,6 +791,7 @@ test_expect_success '__git_refs - configured remote - full refs - repo given on
 test_expect_success '__git_refs - configured remote - remote name matches a directory' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -875,12 +880,14 @@ test_expect_success '__git_refs - unique remote branches for git checkout DWIMer
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/ambiguous
 	other/branch-in-other
 	other/main-in-other
 	remote/ambiguous
 	remote/branch-in-remote
 	matching-tag
+	HEAD
 	branch-in-other
 	branch-in-remote
 	main-in-other
@@ -904,6 +911,7 @@ test_expect_success '__git_refs - after --opt=' '
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	matching-tag
@@ -919,6 +927,7 @@ test_expect_success '__git_refs - after --opt= - full refs' '
 	cat >expected <<-EOF &&
 	refs/heads/main
 	refs/heads/matching-branch
+	refs/remotes/other/HEAD
 	refs/remotes/other/branch-in-other
 	refs/remotes/other/main-in-other
 	refs/tags/matching-tag
@@ -935,6 +944,7 @@ test_expect_success '__git refs - excluding refs' '
 	^HEAD
 	^main
 	^matching-branch
+	^other/HEAD
 	^other/branch-in-other
 	^other/main-in-other
 	^matching-tag
@@ -950,6 +960,7 @@ test_expect_success '__git refs - excluding full refs' '
 	cat >expected <<-EOF &&
 	^refs/heads/main
 	^refs/heads/matching-branch
+	^refs/remotes/other/HEAD
 	^refs/remotes/other/branch-in-other
 	^refs/remotes/other/main-in-other
 	^refs/tags/matching-tag
@@ -975,6 +986,7 @@ test_expect_success '__git_refs - do not filter refs unless told so' '
 	main
 	matching-branch
 	matching/branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	other/matching/branch-in-other
@@ -1095,6 +1107,7 @@ test_expect_success '__git_complete_refs - simple' '
 	HEAD Z
 	main Z
 	matching-branch Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	matching-tag Z
@@ -1123,6 +1136,7 @@ test_expect_success '__git_complete_refs - matching' '
 test_expect_success '__git_complete_refs - remote' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main-in-other Z
 	EOF
@@ -1139,9 +1153,11 @@ test_expect_success '__git_complete_refs - track' '
 	HEAD Z
 	main Z
 	matching-branch Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	matching-tag Z
+	HEAD Z
 	branch-in-other Z
 	main-in-other Z
 	EOF
@@ -1184,6 +1200,7 @@ test_expect_success '__git_complete_refs - suffix' '
 	HEAD.
 	main.
 	matching-branch.
+	other/HEAD.
 	other/branch-in-other.
 	other/main-in-other.
 	matching-tag.
@@ -1199,6 +1216,7 @@ test_expect_success '__git_complete_refs - suffix' '
 test_expect_success '__git_complete_fetch_refspecs - simple' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	HEAD:HEAD Z
+	HEAD:HEAD Z
 	branch-in-other:branch-in-other Z
 	main-in-other:main-in-other Z
 	EOF
@@ -1225,6 +1243,7 @@ test_expect_success '__git_complete_fetch_refspecs - matching' '
 test_expect_success '__git_complete_fetch_refspecs - prefix' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	+HEAD:HEAD Z
+	+HEAD:HEAD Z
 	+branch-in-other:branch-in-other Z
 	+main-in-other:main-in-other Z
 	EOF
@@ -1289,6 +1308,7 @@ test_expect_success '__git_complete_worktree_paths with -C' '
 
 test_expect_success 'git switch - with no options, complete local branches and unique remote branch names for DWIM logic' '
 	test_completion "git switch " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1435,11 +1455,13 @@ test_expect_success 'git-bisect - existing view subcommand is recognized and ena
 test_expect_success 'git checkout - completes refs and unique remote branches for DWIM' '
 	test_completion "git checkout " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1461,6 +1483,7 @@ test_expect_success 'git switch - with GIT_COMPLETION_CHECKOUT_NO_GUESS=1, compl
 
 test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_GUESS=1, complete local branches and unique remote names for DWIM logic' '
 	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git switch --guess " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1470,6 +1493,7 @@ test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_G
 
 test_expect_success 'git switch - a later --guess overrides previous --no-guess, complete local and remote unique branches for DWIM' '
 	test_completion "git switch --no-guess --guess " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1490,6 +1514,7 @@ test_expect_success 'git checkout - with GIT_COMPLETION_NO_GUESS=1 only complete
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1498,11 +1523,13 @@ test_expect_success 'git checkout - with GIT_COMPLETION_NO_GUESS=1 only complete
 test_expect_success 'git checkout - --guess overrides GIT_COMPLETION_NO_GUESS=1, complete refs and unique remote branches for DWIM' '
 	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git checkout --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1514,6 +1541,7 @@ test_expect_success 'git checkout - with --no-guess, only completes refs' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1522,11 +1550,13 @@ test_expect_success 'git checkout - with --no-guess, only completes refs' '
 test_expect_success 'git checkout - a later --guess overrides previous --no-guess, complete refs and unique remote branches for DWIM' '
 	test_completion "git checkout --no-guess --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1538,6 +1568,7 @@ test_expect_success 'git checkout - a later --no-guess overrides previous --gues
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1550,6 +1581,7 @@ test_expect_success 'git checkout - with checkout.guess = false, only completes
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1559,11 +1591,13 @@ test_expect_success 'git checkout - with checkout.guess = true, completes refs a
 	test_config checkout.guess true &&
 	test_completion "git checkout " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1573,11 +1607,13 @@ test_expect_success 'git checkout - a later --guess overrides previous checkout.
 	test_config checkout.guess false &&
 	test_completion "git checkout --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1590,6 +1626,7 @@ test_expect_success 'git checkout - a later --no-guess overrides previous checko
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1601,6 +1638,7 @@ test_expect_success 'git switch - with --detach, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1612,6 +1650,7 @@ test_expect_success 'git checkout - with --detach, complete only references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1783,6 +1822,7 @@ test_expect_success 'git switch - with -d, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1794,6 +1834,7 @@ test_expect_success 'git checkout - with -d, complete only references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1801,10 +1842,12 @@ test_expect_success 'git checkout - with -d, complete only references' '
 
 test_expect_success 'git switch - with --track, complete only remote branches' '
 	test_completion "git switch --track " <<-\EOF &&
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
 	test_completion "git switch -t " <<-\EOF
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1812,10 +1855,12 @@ test_expect_success 'git switch - with --track, complete only remote branches' '
 
 test_expect_success 'git checkout - with --track, complete only remote branches' '
 	test_completion "git checkout --track " <<-\EOF &&
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
 	test_completion "git checkout -t " <<-\EOF
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1834,6 +1879,7 @@ test_expect_success 'git checkout - with --no-track, complete only local referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1845,6 +1891,7 @@ test_expect_success 'git switch - with -c, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1856,6 +1903,7 @@ test_expect_success 'git switch - with -C, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1867,6 +1915,7 @@ test_expect_success 'git switch - with -c and --track, complete all references'
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1878,6 +1927,7 @@ test_expect_success 'git switch - with -C and --track, complete all references'
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1889,6 +1939,7 @@ test_expect_success 'git switch - with -c and --no-track, complete all reference
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1900,6 +1951,7 @@ test_expect_success 'git switch - with -C and --no-track, complete all reference
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1911,6 +1963,7 @@ test_expect_success 'git checkout - with -b, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1922,6 +1975,7 @@ test_expect_success 'git checkout - with -B, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1933,6 +1987,7 @@ test_expect_success 'git checkout - with -b and --track, complete all references
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1944,6 +1999,7 @@ test_expect_success 'git checkout - with -B and --track, complete all references
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1955,6 +2011,7 @@ test_expect_success 'git checkout - with -b and --no-track, complete all referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1966,6 +2023,7 @@ test_expect_success 'git checkout - with -B and --no-track, complete all referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1973,6 +2031,7 @@ test_expect_success 'git checkout - with -B and --no-track, complete all referen
 
 test_expect_success 'git switch - for -c, complete local branches and unique remote branches' '
 	test_completion "git switch -c " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1982,6 +2041,7 @@ test_expect_success 'git switch - for -c, complete local branches and unique rem
 
 test_expect_success 'git switch - for -C, complete local branches and unique remote branches' '
 	test_completion "git switch -C " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2019,6 +2079,7 @@ test_expect_success 'git switch - for -C with --no-track, complete local branche
 
 test_expect_success 'git checkout - for -b, complete local branches and unique remote branches' '
 	test_completion "git checkout -b " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2028,6 +2089,7 @@ test_expect_success 'git checkout - for -b, complete local branches and unique r
 
 test_expect_success 'git checkout - for -B, complete local branches and unique remote branches' '
 	test_completion "git checkout -B " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2065,6 +2127,7 @@ test_expect_success 'git checkout - for -B with --no-track, complete local branc
 
 test_expect_success 'git switch - with --orphan completes local branch names and unique remote branch names' '
 	test_completion "git switch --orphan " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2080,6 +2143,7 @@ test_expect_success 'git switch - --orphan with branch already provided complete
 
 test_expect_success 'git checkout - with --orphan completes local branch names and unique remote branch names' '
 	test_completion "git checkout --orphan " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2093,6 +2157,7 @@ test_expect_success 'git checkout - --orphan with branch already provided comple
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
-- 
2.47.0.94.g1247fb88fd.dirty
Kristoffer Haugsbakk· Oct 23, 2024, 16:50 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v12 7/8] fetch: set remote/HEAD if it does not exist

On Wed, Oct 23, 2024, at 17:36, Bence Ferdinandy wrote:
Show 50 quoted lines
> If the user has remote/HEAD set already and it looks like it has changed
> on the server, then print a message, otherwise set it if we can.
> Silently pass if the user already has the same remote/HEAD set as
> reported by the server or if we encounter any errors along the way.
>
> Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
> ---
>
> Notes:
>     v3: - does not rely on remote set-head anymore so it only authenticates
>         once
>         - uses the new REF_CREATE_ONLY to atomically check if the ref exists
>           and only write it if it doesn't
>         - in all other cases the maximum it does is print a warning
>
>     v4: - instead of the discarded REF_CREATE_ONLY, it uses the existing,
>           but updated transaction api to request a silent create only
>         - it now uses the atomic before_target to determine reporting
>         - refactored for legibility
>
>     v5: - instead of printing a not too useful message, it now fails
>           silently, this in line with the objective to only set up
>           remote/HEAD automatically if the right thing is trivial, for
>           everything else there is remote set-head
>         - fixed all failing tests
>         - added two new tests, one for checking if remote/HEAD is set to the
>           correct one, and one to test that we do not override remote/HEAD
>           if it has changed on the server from what we have locally
>
>     v6: - fixed style issues and unintended extra empty line
>         - updated function call with bool to int from previous patch's
>           change
>         - removed calls to error(...) inherited from builtin/remote.c so we
>           actually fail silently
>         - set the test for remote set-head --auto to the correct value here,
>           which was previously erronously set in the remote set-head patch
>
>     v7: - no change
>
>     v8: - changed logmsg in call to refs_update_symref from "remote
>           set-head" to "fetch"
>
>     v9: - follow through with refs_update_symref_extended
>         - fix test errors uncovered by the new patch
>
>     v10: no change
>
>     v11: fixed some memory leaks
>
>     v12: no change

I think it would be better to reverse-order these patch changelog comments so that the newest is on top/first. (for next time)

Thanks for the careful versioning here.
-- 
Kristoffer Haugsbakk
Bence Ferdinandy· Oct 23, 2024, 17:07 UTC · re: Kristoffer Haugsbakk · lore

Re: [PATCH v12 7/8] fetch: set remote/HEAD if it does not exist

On Wed Oct 23, 2024 at 18:50, Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com> wrote:
Show 56 quoted lines
> On Wed, Oct 23, 2024, at 17:36, Bence Ferdinandy wrote:
>> If the user has remote/HEAD set already and it looks like it has changed
>> on the server, then print a message, otherwise set it if we can.
>> Silently pass if the user already has the same remote/HEAD set as
>> reported by the server or if we encounter any errors along the way.
>>
>> Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
>> ---
>>
>> Notes:
>>     v3: - does not rely on remote set-head anymore so it only authenticates
>>         once
>>         - uses the new REF_CREATE_ONLY to atomically check if the ref exists
>>           and only write it if it doesn't
>>         - in all other cases the maximum it does is print a warning
>>
>>     v4: - instead of the discarded REF_CREATE_ONLY, it uses the existing,
>>           but updated transaction api to request a silent create only
>>         - it now uses the atomic before_target to determine reporting
>>         - refactored for legibility
>>
>>     v5: - instead of printing a not too useful message, it now fails
>>           silently, this in line with the objective to only set up
>>           remote/HEAD automatically if the right thing is trivial, for
>>           everything else there is remote set-head
>>         - fixed all failing tests
>>         - added two new tests, one for checking if remote/HEAD is set to the
>>           correct one, and one to test that we do not override remote/HEAD
>>           if it has changed on the server from what we have locally
>>
>>     v6: - fixed style issues and unintended extra empty line
>>         - updated function call with bool to int from previous patch's
>>           change
>>         - removed calls to error(...) inherited from builtin/remote.c so we
>>           actually fail silently
>>         - set the test for remote set-head --auto to the correct value here,
>>           which was previously erronously set in the remote set-head patch
>>
>>     v7: - no change
>>
>>     v8: - changed logmsg in call to refs_update_symref from "remote
>>           set-head" to "fetch"
>>
>>     v9: - follow through with refs_update_symref_extended
>>         - fix test errors uncovered by the new patch
>>
>>     v10: no change
>>
>>     v11: fixed some memory leaks
>>
>>     v12: no change
>
> I think it would be better to reverse-order these patch changelog
> comments so that the newest is on top/first.  (for next time)
>
> Thanks for the careful versioning here.

Yeah, this works fine when you only go up to v3 and it all fits on a screen :D This is definitely the longest patch series I've made thus far, both in number of commits and versions. I also noticed this today when I realized that I did the cover letter in reverse and how much better readable that was than some of the patches ...

One thing that would not be easy is that with so many patches, how I start out the notes is

	git rev-list HEAD~8..HEAD | xargs -i git notes append {} -m "v12: no change"

and there's no "git notes prepend". That could be another patch for another time :) It also bothers me, that the branch description that can be used to save cover letters is not a note, just a local configuration, but that's a third issue.

Anyhow, thanks for calling it out, if we do come to a v13 I might just spend some time on reversing (or be lazy and just continue at the top ...).

Best, Bence

-- 
bence.ferdinandy.com
Bence Ferdinandy· Nov 14, 2024, 20:23 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v12 0/8] set-head/fetch remote/HEAD updates

Hi all,
I just wanted to ping this thread in hopes of another round of reviews.
Aside from some smaller fixes, the main things that have not yet been reviewed are:
- the new patches 1/8 and 8/8
  (https://lore.kernel.org/git/Zw8IKyPkG0Hr6%2F5t@nand.local/, but see
  https://lore.kernel.org/git/D4ZAELFWJMKN.S88LJ6YK31LZ@ferdinandy.com/)
- changes to 2/8 (refs_update_symref -> refs_update_symref_extended change
  reflecting on Phillip's comments see
  https://lore.kernel.org/git/a7cb48e5-d8ba-44c1-9dbe-d1e8f8a63e3c@gmail.com/)

Regarding the latter name: reading another thread about code style here I was wondering if refs_update_symref_1 would be a better choice.

I'm hoping that otherwise the series is mature now.

Thanks, Bence

Bence Ferdinandy· Nov 18, 2024, 15:09 UTC · re: Bence Ferdinandy · lore

[PATCH v13 0/9] set-head/fetch remote/HEAD updates

This iteration handles the special case when the previous HEAD was detached. This case is now specifically printed by both remote set-head -a and fetch. There were also some small cleanups in commit messages and unintended formattings.

Although the patches themselves are versioned:
- 2/9 is completely new (the above uncovered a discrepancy between backends)
- 3/9 had smaller changes to accomodate this
- 5/9 and 8/9 needed more substantial changes
Bence Ferdinandy (9):
  t/t5505-remote: set default branch to main
  refs: standardize output of refs_read_symbolic_ref
  refs: atomically record overwritten ref in update_symref
  remote set-head: refactor for readability
  remote set-head: better output for --auto
  refs: add TRANSACTION_CREATE_EXISTS error
  refs: add create_only option to refs_update_symref_extended
  fetch: set remote/HEAD if it does not exist
  fetch set_head: handle mirrored bare repositories
 builtin/fetch.c                  | 105 +++++++++++++++++++++++++++++++
 builtin/remote.c                 |  72 +++++++++++++++------
 refs.c                           |  49 ++++++++++++---
 refs.h                           |  14 ++++-
 refs/files-backend.c             |  31 +++++----
 refs/refs-internal.h             |   6 ++
 refs/reftable-backend.c          |  10 ++-
 t/t4207-log-decoration-colors.sh |   3 +-
 t/t5505-remote.sh                |  95 ++++++++++++++++++++++++++--
 t/t5510-fetch.sh                 |  24 +++++++
 t/t5512-ls-remote.sh             |   2 +
 t/t5514-fetch-multiple.sh        |  17 ++++-
 t/t5516-fetch-push.sh            |   3 +-
 t/t5527-fetch-odd-refs.sh        |   3 +-
 t/t7900-maintenance.sh           |   3 +-
 t/t9210-scalar.sh                |   5 +-
 t/t9211-scalar-clone.sh          |   6 +-
 t/t9902-completion.sh            |  65 +++++++++++++++++++
 18 files changed, 455 insertions(+), 58 deletions(-)
-- 
2.47.0.296.gda1ecfef29.dirty
Bence Ferdinandy· Nov 18, 2024, 15:09 UTC · re: Bence Ferdinandy · lore

[PATCH v13 1/9] t/t5505-remote: set default branch to main

Consider the bare repository called "mirror" in the test. Running `git remote add --mirror -f origin ../one` will not change HEAD, consequently if init.defaultBranch is not the same as what HEAD in the remote ("one"), HEAD in "mirror" will be pointing to a non-existent reference. Hence if "mirror" is used as a remote by yet another repository, ls-remote will not show HEAD. On the other hand, if init.defaultBranch happens to match HEAD in "one", then ls-remote will show HEAD.

Since the "ci/run-build-and-tests.sh" script globally exports GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main, there may be a drift in some tests between how the test repositories are set up in the CI and during local testing, if the test itself uses "master" as default instead of "main". In particular, this happens in t5505-remote.sh. This issue does not manifest currently, as the test does not do any remote HEAD manipulation where this would come up, but should such things be added, a locally passing test would break the CI and vice-versa.

Set GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main in t5505-remote to be consistent with the CI.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v9: - new patch
        - a bandaid for the CI issue noticed by Taylor (cf:
          https://lore.kernel.org/git/Zw8IKyPkG0Hr6%2F5t@nand.local/), but
          see
          https://lore.kernel.org/git/D4ZAELFWJMKN.S88LJ6YK31LZ@ferdinandy.com/ for the root cause in detail
    
    v10: no change
    
    v11: no change
    
    v12: added forgotten sign-off
    
    v13: commit message udpated to be more precise
 t/t5505-remote.sh | 3 +++
 1 file changed, 3 insertions(+)
Show changes to t/t5505-remote.sh +3 −0
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 532035933f..9b50276646 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -2,6 +2,9 @@
 
 test_description='git remote porcelain-ish'
 
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
 TEST_PASSES_SANITIZE_LEAK=true
 . ./test-lib.sh
 
-- 
2.47.0.296.gda1ecfef29.dirty
Junio C Hamano· Nov 20, 2024, 03:46 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v13 1/9] t/t5505-remote: set default branch to main

Bence Ferdinandy <bence@ferdinandy.com> writes:
Show 10 quoted lines
> Consider the bare repository called "mirror" in the test. Running `git
> remote add --mirror -f origin ../one` will not change HEAD, consequently
> if init.defaultBranch is not the same as what HEAD in the remote
> ("one"), HEAD in "mirror" will be pointing to a non-existent reference.
> Hence if "mirror" is used as a remote by yet another repository,
> ls-remote will not show HEAD. On the other hand, if init.defaultBranch
> happens to match HEAD in "one", then ls-remote will show HEAD.
>
> Since the "ci/run-build-and-tests.sh" script globally exports
> GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main, there may be a drift in some

"main," -> "main for some (but not all) jobs," or something. IOW, the point of that forced export is to test both.

> tests between how the test repositories are set up in the CI and during
> local testing, if the test itself uses "master" as default instead of
> "main".
Bence Ferdinandy· Nov 18, 2024, 15:09 UTC · re: Bence Ferdinandy · lore

[PATCH v13 3/9] refs: atomically record overwritten ref in update_symref

When updating a symref with update_symref it's currently not possible to know for sure what was the previous value that was overwritten. Extend refs_update_symref under a new function name, to record the value after the ref has been locked if the caller of refs_update_symref_extended requests it via a new variable in the function call. Keep the original refs_update_symref function with the same signature, but now as a wrapper around refs_update_symref_extended.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    
    v5: - added before_target to reftables backend
        - added an extra safety check for transaction's existence in refs.c
    
    v6: - no change
    
    v7: - remove the whole before_target concept from the backends and
          handle checking it in refs.c instead (thanks Karthik)
        - rename the before_target to referent which is how the same concept
          is called in the backends
        - change commit prefix to be more in line with project standards
    
    v8: no change
    
    v9: - instead of adding parameters to refs_update_symref, rename what
          was in v8 as refs_update_symref_extended and make refs_update_symref
          a wrapper for that. This significantly reduces the number of files
          that need to be touched, and avoids adding a lot of dummy NULL-s
          in unrelated places.
    
    v10: no change
    
    v11: no change
    
    v12: no change
    
    v13: if referent is a non-symbolic ref, record the hash in referent and
        signal this with a return value of -1
 refs.c | 24 ++++++++++++++++++++++--
 refs.h |  4 ++++
 2 files changed, 26 insertions(+), 2 deletions(-)
Show changes to 2 files +26 −2

refs.c, refs.h

diff --git a/refs.c b/refs.c
index 5f729ed412..c4500a7582 100644
--- a/refs.c
+++ b/refs.c
@@ -2115,6 +2115,13 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
 
 int refs_update_symref(struct ref_store *refs, const char *ref,
 		       const char *target, const char *logmsg)
+{
+	return refs_update_symref_extended(refs, ref, target, logmsg, NULL);
+}
+
+int refs_update_symref_extended(struct ref_store *refs, const char *ref,
+		       const char *target, const char *logmsg,
+		       struct strbuf *referent)
 {
 	struct ref_transaction *transaction;
 	struct strbuf err = STRBUF_INIT;
@@ -2125,10 +2132,24 @@ int refs_update_symref(struct ref_store *refs, const char *ref,
 	    ref_transaction_update(transaction, ref, NULL, NULL,
 				   target, NULL, REF_NO_DEREF,
 				   logmsg, &err) ||
-	    ref_transaction_commit(transaction, &err)) {
+	    ref_transaction_prepare(transaction, &err)) {
 		ret = error("%s", err.buf);
+		goto cleanup;
+	}
+	if (referent && refs_read_symbolic_ref(refs, ref, referent) == -2) {
+		struct object_id oid;
+		if (!refs_read_ref(refs, ref, &oid)) {
+			strbuf_addstr(referent, oid_to_hex(&oid));
+			ret = -1;
+		} else {
+			ret = 1;
+		}
 	}
 
+	if (ref_transaction_commit(transaction, &err))
+		ret = error("%s", err.buf);
+
+cleanup:
 	strbuf_release(&err);
 	if (transaction)
 		ref_transaction_free(transaction);
@@ -2948,4 +2969,3 @@ int ref_update_expects_existing_old_ref(struct ref_update *update)
 	return (update->flags & REF_HAVE_OLD) &&
 		(!is_null_oid(&update->old_oid) || update->old_target);
 }
-
diff --git a/refs.h b/refs.h
index f8b714ca1d..92622e807d 100644
--- a/refs.h
+++ b/refs.h
@@ -579,6 +579,10 @@ int refs_copy_existing_ref(struct ref_store *refs, const char *oldref,
 int refs_update_symref(struct ref_store *refs, const char *refname,
 		       const char *target, const char *logmsg);
 
+int refs_update_symref_extended(struct ref_store *refs, const char *refname,
+		       const char *target, const char *logmsg,
+		       struct strbuf *referent);
+
 enum action_on_err {
 	UPDATE_REFS_MSG_ON_ERR,
 	UPDATE_REFS_DIE_ON_ERR,
-- 
2.47.0.296.gda1ecfef29.dirty
Bence Ferdinandy· Nov 18, 2024, 15:09 UTC · re: Bence Ferdinandy · lore

[PATCH v13 2/9] refs: standardize output of refs_read_symbolic_ref

When the symbolic reference we want to read with refs_read_symbolic_ref is actually not a symbolic reference, the files and the reftable backends return different values (1 and -1 respectively). Standardize the returned values so that 0 is success, -1 is a generic error and -2 is that the reference was actually non-symbolic.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v13: new patch
 refs.h                  | 6 ++++++
 refs/files-backend.c    | 7 +++----
 refs/refs-internal.h    | 6 ++++++
 refs/reftable-backend.c | 4 +++-
 4 files changed, 18 insertions(+), 5 deletions(-)
Show changes to 4 files +18 −5

refs.h, refs/files-backend.c, refs/refs-internal.h, refs/reftable-backend.c

diff --git a/refs.h b/refs.h
index 108dfc93b3..f8b714ca1d 100644
--- a/refs.h
+++ b/refs.h
@@ -83,6 +83,12 @@ int refs_read_ref_full(struct ref_store *refs, const char *refname,
 
 int refs_read_ref(struct ref_store *refs, const char *refname, struct object_id *oid);
 
+/*
+ * Return 0 if the symbolic reference could be read without error.
+ * Return -1 for generic errors.
+ * Return -2 if the reference was actually non-symbolic.
+ */
+
 int refs_read_symbolic_ref(struct ref_store *ref_store, const char *refname,
 			   struct strbuf *referent);
 
diff --git a/refs/files-backend.c b/refs/files-backend.c
index 0824c0b8a9..81e650ec48 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -596,10 +596,9 @@ static int files_read_symbolic_ref(struct ref_store *ref_store, const char *refn
 	unsigned int type;
 
 	ret = read_ref_internal(ref_store, refname, &oid, referent, &type, &failure_errno, 1);
-	if (ret)
-		return ret;
-
-	return !(type & REF_ISSYMREF);
+	if (!ret && !(type & REF_ISSYMREF))
+		return -2;
+	return ret;
 }
 
 int parse_loose_ref_contents(const struct git_hash_algo *algop,
diff --git a/refs/refs-internal.h b/refs/refs-internal.h
index 2313c830d8..f0ef354bce 100644
--- a/refs/refs-internal.h
+++ b/refs/refs-internal.h
@@ -673,6 +673,12 @@ struct ref_storage_be {
 
 	ref_iterator_begin_fn *iterator_begin;
 	read_raw_ref_fn *read_raw_ref;
+
+	/*
+	 * Return 0 if the symbolic reference could be read without error.
+	 * Return -1 for generic errors.
+	 * Return -2 if the reference was actually non-symbolic.
+	 */
 	read_symbolic_ref_fn *read_symbolic_ref;
 
 	reflog_iterator_begin_fn *reflog_iterator_begin;
diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
index 38eb14d591..60cb83f23a 100644
--- a/refs/reftable-backend.c
+++ b/refs/reftable-backend.c
@@ -830,7 +830,9 @@ static int reftable_be_read_symbolic_ref(struct ref_store *ref_store,
 		return ret;
 
 	ret = reftable_stack_read_ref(stack, refname, &ref);
-	if (ret == 0 && ref.value_type == REFTABLE_REF_SYMREF)
+	if (!ret && (ref.value_type != REFTABLE_REF_SYMREF))
+		ret = -2;
+	else if (!ret && (ref.value_type == REFTABLE_REF_SYMREF))
 		strbuf_addstr(referent, ref.value.symref);
 	else
 		ret = -1;
-- 
2.47.0.296.gda1ecfef29.dirty
Junio C Hamano· Nov 19, 2024, 01:22 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v13 2/9] refs: standardize output of refs_read_symbolic_ref

Bence Ferdinandy <bence@ferdinandy.com> writes:
> Subject: Re: [PATCH v13 2/9] refs: standardize output of refs_read_symbolic_ref
"output" -> "return values", or something.
Show 5 quoted lines
> When the symbolic reference we want to read with refs_read_symbolic_ref
> is actually not a symbolic reference, the files and the reftable
> backends return different values (1 and -1 respectively). Standardize
> the returned values so that 0 is success, -1 is a generic error and -2
> is that the reference was actually non-symbolic.
Are all the existing callers OK with this switch from 1 to -2?

IOW, if a caller using the ref-files backend start behaving differently with this change upon seeing a return value of -2 (where it previously was expecting to see 1), that would not be nice.

Because "reftable was already broken" is not a good excuse to introduce a separate regression to ref-files users, we'd want to be careful if we want to do this kind of "while at it" change.

Show 5 quoted lines
> +/*
> + * Return 0 if the symbolic reference could be read without error.
> + * Return -1 for generic errors.
> + * Return -2 if the reference was actually non-symbolic.
> + */

As this is an implementation of ref_stroage_be.read_symbolic_ref, the above comment must stay in sync with the comment over there (and a comment before the corresponding function in the other backend).

I personally would not add the above comment for that reason, but as long as we are consistent, I am OK either way. So if we add this, we should do the same to the reftable side as well.

By the way, it is arguable that it is an error when a caller asks "here is a ref, please read it as a symbolic ref" and the ref turns out to be not a symbolic ref. I'd say that it is a valid view that the caller asked the question to find out if the ref was a symbolic before making the call, and "that ref is not symbolic" is one of the valid and expected results. So if you wanted to change the value from 1 to -2 only because "you called read_symbolic_ref() without checking if it is a symbolic to begin with, which is your fault and you deserve to get an error", I am not sure if I agree with that view and reasoning.

In any case, this "not a symbolic ref" may want to have a symbolic constant. With NOT_A_SYMREF being a non-error, you could structure the caller like so:

	status = read_symref()
	if (status < 0)
		... we got an error so we stop here ...
		... we do not behave differently on error type ...
	switch (status) {
	case 0:
		... everything is peachy ...
		break ;;
	case NOT_A_SYMREF:
		... we want to handle non-symref here ...
		break ;;
	default:
		BUG("unhandled case");
	}

Also, even if we decide that we want to treat this as an error, lumping everything else as "generic error" might make it awkward to evolve the API, I suspect.

Show 15 quoted lines
> diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
> index 38eb14d591..60cb83f23a 100644
> --- a/refs/reftable-backend.c
> +++ b/refs/reftable-backend.c
> @@ -830,7 +830,9 @@ static int reftable_be_read_symbolic_ref(struct ref_store *ref_store,
>  		return ret;
>  
>  	ret = reftable_stack_read_ref(stack, refname, &ref);
> -	if (ret == 0 && ref.value_type == REFTABLE_REF_SYMREF)
> +	if (!ret && (ref.value_type != REFTABLE_REF_SYMREF))
> +		ret = -2;
> +	else if (!ret && (ref.value_type == REFTABLE_REF_SYMREF))
>  		strbuf_addstr(referent, ref.value.symref);
>  	else
>  		ret = -1;
Patrick Steinhardt· Nov 19, 2024, 06:44 UTC · re: Junio C Hamano · lore

Re: [PATCH v13 2/9] refs: standardize output of refs_read_symbolic_ref

On Tue, Nov 19, 2024 at 10:22:31AM +0900, Junio C Hamano wrote:
Show 21 quoted lines
> Bence Ferdinandy <bence@ferdinandy.com> writes:
> 
> > Subject: Re: [PATCH v13 2/9] refs: standardize output of refs_read_symbolic_ref
> 
> "output" -> "return values", or something.
> 
> > When the symbolic reference we want to read with refs_read_symbolic_ref
> > is actually not a symbolic reference, the files and the reftable
> > backends return different values (1 and -1 respectively). Standardize
> > the returned values so that 0 is success, -1 is a generic error and -2
> > is that the reference was actually non-symbolic.
> 
> Are all the existing callers OK with this switch from 1 to -2?
> 
> IOW, if a caller using the ref-files backend start behaving
> differently with this change upon seeing a return value of -2 (where
> it previously was expecting to see 1), that would not be nice.
> 
> Because "reftable was already broken" is not a good excuse to
> introduce a separate regression to ref-files users, we'd want to be
> careful if we want to do this kind of "while at it" change.
There are only three callers:
  - "remote.c:ignore_symref_update()" only cares whether the return
    value is 0 or not.
  - "builtin/remote.c:mv()" is the same.
  - "refs.c:migrate_one_ref()" assumes the behaviour of the reftable
    backend and only checks for negative error codes.

So you could argue that it's the "files" backend that is broken, not the "reftable" backend. Doesn't matter ultimately though, the real problem is that this wasn't ever documented anywhere.

I agree that this should be part of the commit message.
Show 13 quoted lines
> > +/*
> > + * Return 0 if the symbolic reference could be read without error.
> > + * Return -1 for generic errors.
> > + * Return -2 if the reference was actually non-symbolic.
> > + */
> 
> As this is an implementation of ref_stroage_be.read_symbolic_ref,
> the above comment must stay in sync with the comment over there (and
> a comment before the corresponding function in the other backend).
> 
> I personally would not add the above comment for that reason, but as
> long as we are consistent, I am OK either way.  So if we add this,
> we should do the same to the reftable side as well.

Another solution could be to have the comment in "refs.h" for the caller-facing function, while the backend pointer simply says "Please refer to the documentation of `refs_read_symbolic_ref()`."

Show 10 quoted lines
> By the way, it is arguable that it is an error when a caller asks
> "here is a ref, please read it as a symbolic ref" and the ref turns
> out to be not a symbolic ref.  I'd say that it is a valid view that
> the caller asked the question to find out if the ref was a symbolic
> before making the call, and "that ref is not symbolic" is one of the
> valid and expected results.  So if you wanted to change the value
> from 1 to -2 only because "you called read_symbolic_ref() without
> checking if it is a symbolic to begin with, which is your fault and
> you deserve to get an error", I am not sure if I agree with that
> view and reasoning.

The reason why I've been proposing to return negative is because we have the idiom of checking `err < 0` in many places, so a function that returns a positive value in the case where it didn't return the expected result can easily lead to bugs.

> In any case, this "not a symbolic ref" may want to have a symbolic
> constant.  With NOT_A_SYMREF being a non-error, you could structure
> the caller like so:
That'd be a good idea.
Show 19 quoted lines
> 	status = read_symref()
> 	if (status < 0)
> 		... we got an error so we stop here ...
> 		... we do not behave differently on error type ...
> 
> 	switch (status) {
> 	case 0:
> 		... everything is peachy ...
> 		break ;;
> 	case NOT_A_SYMREF:
> 		... we want to handle non-symref here ...
> 		break ;;
> 	default:
> 		BUG("unhandled case");
> 	}
> 
> Also, even if we decide that we want to treat this as an error,
> lumping everything else as "generic error" might make it awkward to
> evolve the API, I suspect.

I guess most callers don't care about the exact error code, they only care about whether or not they have been able to read the symref. I think this idiom would easily be extensible in the future if all callers know to check for `err < 0` by default, because introducing a new error code would continue to work for them. And if they want to handle that new error code they can adapt as you propose above with the switch.

Patrick
Junio C Hamano· Nov 19, 2024, 06:54 UTC · re: Patrick Steinhardt · lore

Re: [PATCH v13 2/9] refs: standardize output of refs_read_symbolic_ref

Patrick Steinhardt <ps@pks.im> writes:
Show 13 quoted lines
> There are only three callers:
>
>   - "remote.c:ignore_symref_update()" only cares whether the return
>     value is 0 or not.
>
>   - "builtin/remote.c:mv()" is the same.
>
>   - "refs.c:migrate_one_ref()" assumes the behaviour of the reftable
>     backend and only checks for negative error codes.
>
> So you could argue that it's the "files" backend that is broken, not the
> "reftable" backend. Doesn't matter ultimately though, the real problem
> is that this wasn't ever documented anywhere.

You're correct that it does not matter ultimately. But as a general rule, which also applies here, anything newly introduced one does differently without having a good reason is a bug in the new thing, I would say.

> Another solution could be to have the comment in "refs.h" for the
> caller-facing function, while the backend pointer simply says "Please
> refer to the documentation of `refs_read_symbolic_ref()`."
Yup, avoiding unnecessary duplication is a good thing.
> The reason why I've been proposing to return negative is because we have
> the idiom of checking `err < 0` in many places, so a function that
> returns a positive value in the case where it didn't return the expected
> result can easily lead to bugs.

I agree with the general reasoning. I am saying this may or may not be an error, and if it turns out that it is not an error but is just one of the generally expected outcome, treating it as an error and having "if (status < 0)" to lump the case together with other error cases may not be nice to the callers.

Patrick Steinhardt· Nov 19, 2024, 07:26 UTC · re: Junio C Hamano · lore

Re: [PATCH v13 2/9] refs: standardize output of refs_read_symbolic_ref

On Tue, Nov 19, 2024 at 03:54:05PM +0900, Junio C Hamano wrote:
Show 11 quoted lines
> Patrick Steinhardt <ps@pks.im> writes:
> > The reason why I've been proposing to return negative is because we have
> > the idiom of checking `err < 0` in many places, so a function that
> > returns a positive value in the case where it didn't return the expected
> > result can easily lead to bugs.
> 
> I agree with the general reasoning.  I am saying this may or may not
> be an error, and if it turns out that it is not an error but is just
> one of the generally expected outcome, treating it as an error and
> having "if (status < 0)" to lump the case together with other error
> cases may not be nice to the callers.

The question to me is whether the function returns something sensible in all non-error cases that a caller can use properly without having to explicitly check for the value. And I'd say that this is not the case with `refs_read_symbolic_ref()`, which wouldn't end up setting the value of `referent`.

So regardless of whether we define this as error or non-error, the caller would have to exlicitly handle the case where it's not a symref in order to make sense of it because the result is not well-defined.

Patrick
Bence Ferdinandy· Nov 19, 2024, 10:10 UTC · re: Patrick Steinhardt · lore

Re: [PATCH v13 2/9] refs: standardize output of refs_read_symbolic_ref

On Tue Nov 19, 2024 at 08:26, Patrick Steinhardt <ps@pks.im> wrote:
Show 22 quoted lines
> On Tue, Nov 19, 2024 at 03:54:05PM +0900, Junio C Hamano wrote:
>> Patrick Steinhardt <ps@pks.im> writes:
>> > The reason why I've been proposing to return negative is because we have
>> > the idiom of checking `err < 0` in many places, so a function that
>> > returns a positive value in the case where it didn't return the expected
>> > result can easily lead to bugs.
>> 
>> I agree with the general reasoning.  I am saying this may or may not
>> be an error, and if it turns out that it is not an error but is just
>> one of the generally expected outcome, treating it as an error and
>> having "if (status < 0)" to lump the case together with other error
>> cases may not be nice to the callers.
>
> The question to me is whether the function returns something sensible in
> all non-error cases that a caller can use properly without having to
> explicitly check for the value. And I'd say that this is not the case
> with `refs_read_symbolic_ref()`, which wouldn't end up setting the value
> of `referent`.
>
> So regardless of whether we define this as error or non-error, the
> caller would have to exlicitly handle the case where it's not a symref
> in order to make sense of it because the result is not well-defined.

I agree with Patrick re the -1, -2 return values. The non-error behavior should be when referent is set, anything else is something the caller would need to consider if they want to do something with it or not.

Junio C Hamano· Nov 19, 2024, 05:10 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v13 2/9] refs: standardize output of refs_read_symbolic_ref

Bence Ferdinandy <bence@ferdinandy.com> writes:
Show 15 quoted lines
> diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
> index 38eb14d591..60cb83f23a 100644
> --- a/refs/reftable-backend.c
> +++ b/refs/reftable-backend.c
> @@ -830,7 +830,9 @@ static int reftable_be_read_symbolic_ref(struct ref_store *ref_store,
>  		return ret;
>  
>  	ret = reftable_stack_read_ref(stack, refname, &ref);
> -	if (ret == 0 && ref.value_type == REFTABLE_REF_SYMREF)
> +	if (!ret && (ref.value_type != REFTABLE_REF_SYMREF))
> +		ret = -2;
> +	else if (!ret && (ref.value_type == REFTABLE_REF_SYMREF))
>  		strbuf_addstr(referent, ref.value.symref);
>  	else
>  		ret = -1;

The ref.value_type can be either equal to REFTABLE_REF_SYMREF or not equal to it, and there is no other choice.

Wouldn't it be easier to reason about if the above code were written more like this:

        if (ret)
		ret = -1;
	else if (ref.value_type == REFTABLE_REF_SYMREF)
		strbuf_addstr(...);
	else
		ret = -2;

I found it curious when I read it again while attempting to resolve conflicts with 5413d69f (refs/reftable: refactor reading symbolic refs to use reftable backend, 2024-11-05). The resolution has to update this part of code to use the new implementation that asks reftable_backend_read_ref() and becomes a lot simpler, so the way it is written in your topic does not make much difference in the longer term when both topics graduate.

IOW, if we were rebuilding your topic on top of Patrick's topoic that includes 5413d69f, this part would read like so, I think.

Show changes to diff +3 −1
diff --git c/refs/reftable-backend.c w/refs/reftable-backend.c
index 6298991da7..b6bc3039a5 100644
--- c/refs/reftable-backend.c
+++ w/refs/reftable-backend.c
@@ -920,8 +920,10 @@ static int reftable_be_read_symbolic_ref(struct ref_store *ref_store,
 		return ret;
 
 	ret = reftable_backend_read_ref(be, refname, &oid, referent, &type);
-	if (type != REF_ISSYMREF)
+	if (ret)
 		ret = -1;
+	else if (type != REF_ISSYMREF)
+		ret = -2;
 	return ret;
 }
 
Bence Ferdinandy· Nov 19, 2024, 10:04 UTC · re: Junio C Hamano · lore

Re: [PATCH v13 2/9] refs: standardize output of refs_read_symbolic_ref

On Tue Nov 19, 2024 at 06:10, Junio C Hamano <gitster@pobox.com> wrote:
Show 20 quoted lines
> Bence Ferdinandy <bence@ferdinandy.com> writes:
>
>> diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
>> index 38eb14d591..60cb83f23a 100644
>> --- a/refs/reftable-backend.c
>> +++ b/refs/reftable-backend.c
>> @@ -830,7 +830,9 @@ static int reftable_be_read_symbolic_ref(struct ref_store *ref_store,
>>  		return ret;
>>  
>>  	ret = reftable_stack_read_ref(stack, refname, &ref);
>> -	if (ret == 0 && ref.value_type == REFTABLE_REF_SYMREF)
>> +	if (!ret && (ref.value_type != REFTABLE_REF_SYMREF))
>> +		ret = -2;
>> +	else if (!ret && (ref.value_type == REFTABLE_REF_SYMREF))
>>  		strbuf_addstr(referent, ref.value.symref);
>>  	else
>>  		ret = -1;
>
> The ref.value_type can be either equal to REFTABLE_REF_SYMREF or not
> equal to it, and there is no other choice.
Ah, ok, I didn't realize this.
Show 18 quoted lines
>
> Wouldn't it be easier to reason about if the above code were written
> more like this:
>
>         if (ret)
> 		ret = -1;
> 	else if (ref.value_type == REFTABLE_REF_SYMREF)
> 		strbuf_addstr(...);
> 	else
> 		ret = -2;
>
> I found it curious when I read it again while attempting to resolve
> conflicts with 5413d69f (refs/reftable: refactor reading symbolic
> refs to use reftable backend, 2024-11-05).  The resolution has to
> update this part of code to use the new implementation that asks
> reftable_backend_read_ref() and becomes a lot simpler, so the way it
> is written in your topic does not make much difference in the longer
> term when both topics graduate.
I'll update the patch with the above, 
Show 20 quoted lines
>
> IOW, if we were rebuilding your topic on top of Patrick's topoic
> that includes 5413d69f, this part would read like so, I think.
>
> diff --git c/refs/reftable-backend.c w/refs/reftable-backend.c
> index 6298991da7..b6bc3039a5 100644
> --- c/refs/reftable-backend.c
> +++ w/refs/reftable-backend.c
> @@ -920,8 +920,10 @@ static int reftable_be_read_symbolic_ref(struct ref_store *ref_store,
>  		return ret;
>  
>  	ret = reftable_backend_read_ref(be, refname, &oid, referent, &type);
> -	if (type != REF_ISSYMREF)
> +	if (ret)
>  		ret = -1;
> +	else if (type != REF_ISSYMREF)
> +		ret = -2;
>  	return ret;
>  }
>  

but I'll save this as well, I would not be completely surprised if Patrick's topic makes it in sooner :)

Thanks, Bence

Patrick Steinhardt· Nov 19, 2024, 06:48 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v13 2/9] refs: standardize output of refs_read_symbolic_ref

On Mon, Nov 18, 2024 at 04:09:21PM +0100, Bence Ferdinandy wrote:
Show 14 quoted lines
> diff --git a/refs.h b/refs.h
> index 108dfc93b3..f8b714ca1d 100644
> --- a/refs.h
> +++ b/refs.h
> @@ -83,6 +83,12 @@ int refs_read_ref_full(struct ref_store *refs, const char *refname,
>  
>  int refs_read_ref(struct ref_store *refs, const char *refname, struct object_id *oid);
>  
> +/*
> + * Return 0 if the symbolic reference could be read without error.
> + * Return -1 for generic errors.
> + * Return -2 if the reference was actually non-symbolic.
> + */
> +
Extraneous empty newline.
Also, how about the following:
    /*
     * Read the symbolic ref named "refname" and write its immediate
     * referent into the provided buffer. This does not resolve the
     * symbolic ref recursively in case the target is a symbolic ref, as
     * well.
     *
     * Returns 0 on success, -2 if the "refname" is not a symbolic ref,
     * -1 otherwise.
     */
Show 15 quoted lines
> diff --git a/refs/refs-internal.h b/refs/refs-internal.h
> index 2313c830d8..f0ef354bce 100644
> --- a/refs/refs-internal.h
> +++ b/refs/refs-internal.h
> @@ -673,6 +673,12 @@ struct ref_storage_be {
>  
>  	ref_iterator_begin_fn *iterator_begin;
>  	read_raw_ref_fn *read_raw_ref;
> +
> +	/*
> +	 * Return 0 if the symbolic reference could be read without error.
> +	 * Return -1 for generic errors.
> +	 * Return -2 if the reference was actually non-symbolic.
> +	 */
>  	read_symbolic_ref_fn *read_symbolic_ref;
As proposed in the other thread, this could instead be:
    /*
     * Please refer to `refs_read_symbolic_ref()` for the expected
     * behaviour.
     /
Patrick
Bence Ferdinandy· Nov 19, 2024, 10:17 UTC · re: Patrick Steinhardt · lore

Re: [PATCH v13 2/9] refs: standardize output of refs_read_symbolic_ref

On Tue Nov 19, 2024 at 07:48, Patrick Steinhardt <ps@pks.im> wrote:
Show 52 quoted lines
> On Mon, Nov 18, 2024 at 04:09:21PM +0100, Bence Ferdinandy wrote:
>> diff --git a/refs.h b/refs.h
>> index 108dfc93b3..f8b714ca1d 100644
>> --- a/refs.h
>> +++ b/refs.h
>> @@ -83,6 +83,12 @@ int refs_read_ref_full(struct ref_store *refs, const char *refname,
>>  
>>  int refs_read_ref(struct ref_store *refs, const char *refname, struct object_id *oid);
>>  
>> +/*
>> + * Return 0 if the symbolic reference could be read without error.
>> + * Return -1 for generic errors.
>> + * Return -2 if the reference was actually non-symbolic.
>> + */
>> +
>
> Extraneous empty newline.
>
> Also, how about the following:
>
>     /*
>      * Read the symbolic ref named "refname" and write its immediate
>      * referent into the provided buffer. This does not resolve the
>      * symbolic ref recursively in case the target is a symbolic ref, as
>      * well.
>      *
>      * Returns 0 on success, -2 if the "refname" is not a symbolic ref,
>      * -1 otherwise.
>      */
>
>> diff --git a/refs/refs-internal.h b/refs/refs-internal.h
>> index 2313c830d8..f0ef354bce 100644
>> --- a/refs/refs-internal.h
>> +++ b/refs/refs-internal.h
>> @@ -673,6 +673,12 @@ struct ref_storage_be {
>>  
>>  	ref_iterator_begin_fn *iterator_begin;
>>  	read_raw_ref_fn *read_raw_ref;
>> +
>> +	/*
>> +	 * Return 0 if the symbolic reference could be read without error.
>> +	 * Return -1 for generic errors.
>> +	 * Return -2 if the reference was actually non-symbolic.
>> +	 */
>>  	read_symbolic_ref_fn *read_symbolic_ref;
>
> As proposed in the other thread, this could instead be:
>
>     /*
>      * Please refer to `refs_read_symbolic_ref()` for the expected
>      * behaviour.
>      /

Thanks, that makes sense. So as a summary, I'll update the comments and also the implementation detail Junio pointed out.

Bence Ferdinandy· Nov 18, 2024, 15:09 UTC · re: Bence Ferdinandy · lore

[PATCH v13 5/9] remote set-head: better output for --auto

Currently, set-head --auto will print a message saying "remote/HEAD set to branch", which implies something was changed.

Change the output of --auto, so the output actually reflects what was done: a) set a previously unset HEAD, b) change HEAD because remote changed or c) no updates. As edge cases, if HEAD is changed from a previous symbolic reference that was not a remote branch, explicitly call attention to this fact, and also notify the user if the previous reference was not a symbolic reference.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v1-v2:
    
        was RFC in https://lore.kernel.org/git/20240910203835.2288291-1-bence@ferdinandy.com/
    
    v3:
    
        This patch was originally sent along when I thought set-head was
        going to be invoked by fetch, but the discussion on the RFC
        concluded that it should be not. This opened the possibility to make
        it more explicit.
    
        Note: although I feel both things the patch does are really just
        cosmetic, an argument could be made for breaking it into two, one
        for the no-op part and one for the --auto print update.
    
        Was sent in:
        https://lore.kernel.org/git/20240915221055.904107-1-bence@ferdinandy.com/
    
    v4:
        - changes are now handled atomically via the ref update transaction
        - outputs have changed along the lines of Junio's suggestion
        - minor refactor to set_head for improved legibility
    
    v5: - the minor refactor has been split out into its own patch
    
    v6: - fixed uninitialized prev_head
        - fixed case of unusual previous target
        - fixed a test that would have been actually broken at this patch
          (the output was only correct with the later update to fetch)
        - added 4 tests for the 4 output cases
    
    v7: - change commit prefix to be more in line with project standards
        - fixed tests to also work with the reftable backend
        - renamed report function, fixed style issue with checking buf len
        - fixed not releasing an strbuf
    
    v8: no change
    
    v9: - mark output strings in report_set_head_auto as translatable
        - rename buf_prev to b_local_head for consistency
        - use ${SQ} in tests instead of '\''
    
    v10: no change
    
    v11: no change
    
    v12: no change
    
    v13: added handling the edge case of previous remote/HEAD being
    a detached HEAD
 builtin/remote.c  | 59 +++++++++++++++++++++++++++++++++++---------
 t/t5505-remote.sh | 63 ++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 109 insertions(+), 13 deletions(-)
Show changes to 2 files +109 −13

builtin/remote.c, t/t5505-remote.sh

diff --git a/builtin/remote.c b/builtin/remote.c
index 1d68c5b2ba..a682ef5df2 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1399,10 +1399,38 @@ static int show(int argc, const char **argv, const char *prefix)
 	return result;
 }
 
+static void report_set_head_auto(const char *remote, const char *head_name,
+			struct strbuf *b_local_head, int updateres) {
+	struct strbuf buf_prefix = STRBUF_INIT;
+	const char *prev_head = NULL;
+
+	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
+	skip_prefix(b_local_head->buf, buf_prefix.buf, &prev_head);
+
+	if (prev_head && !strcmp(prev_head, head_name))
+		printf(_("'%s/HEAD' is unchanged and points to '%s'\n"),
+			remote, head_name);
+	else if (prev_head)
+		printf(_("'%s/HEAD' has changed from '%s' and now points to '%s'\n"),
+			remote, prev_head, head_name);
+	else if (!b_local_head->len)
+		printf(_("'%s/HEAD' is now created and points to '%s'\n"),
+			remote, head_name);
+	else if (!!updateres && b_local_head->len)
+		printf(_("'%s/HEAD' was detached at '%s' and now points to '%s'\n"),
+			remote, b_local_head->buf, head_name);
+	else
+		printf(_("'%s/HEAD' used to point to '%s' "
+			"(which is not a remote branch), but now points to '%s'\n"),
+			remote, b_local_head->buf, head_name);
+	strbuf_release(&buf_prefix);
+}
+
 static int set_head(int argc, const char **argv, const char *prefix)
 {
-	int i, opt_a = 0, opt_d = 0, result = 0;
-	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT;
+	int i, opt_a = 0, opt_d = 0, result = 0, updateres;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
+		b_local_head = STRBUF_INIT;
 	char *head_name = NULL;
 	struct ref_store *refs = get_main_ref_store(the_repository);
 
@@ -1440,20 +1468,27 @@ static int set_head(int argc, const char **argv, const char *prefix)
 	} else
 		usage_with_options(builtin_remote_sethead_usage, options);
 
-	if (head_name) {
-		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", argv[0], head_name);
-		/* make sure it's valid */
-		if (!refs_ref_exists(refs, b_remote_head.buf))
-			result |= error(_("Not a valid ref: %s"), b_remote_head.buf);
-		else if (refs_update_symref(refs, b_head.buf, b_remote_head.buf, "remote set-head"))
-			result |= error(_("Could not setup %s"), b_head.buf);
-		else if (opt_a)
-			printf("%s/HEAD set to %s\n", argv[0], head_name);
-		free(head_name);
+	if (!head_name)
+		goto cleanup;
+	strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", argv[0], head_name);
+	if (!refs_ref_exists(refs, b_remote_head.buf)) {
+		result |= error(_("Not a valid ref: %s"), b_remote_head.buf);
+		goto cleanup;
+	}
+	updateres = refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
+			"remote set-head", &b_local_head);
+	if (updateres == -2) {
+		result |= error(_("Could not setup %s"), b_head.buf);
+		goto cleanup;
 	}
+	if (opt_a)
+		report_set_head_auto(argv[0], head_name, &b_local_head, updateres);
 
+cleanup:
+	free(head_name);
 	strbuf_release(&b_head);
 	strbuf_release(&b_remote_head);
+	strbuf_release(&b_local_head);
 	return result;
 }
 
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 9b50276646..807df00ba7 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -432,12 +432,63 @@ test_expect_success 'set-head --auto' '
 	)
 '
 
+test_expect_success 'set-head --auto detects creation' '
+	(
+		cd test &&
+		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} is now created and points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto to update a non symbolic ref' '
+	(
+		cd test &&
+		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
+		git update-ref refs/remotes/origin/HEAD HEAD &&
+		HEAD=$(git log --pretty="%H") &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} was detached at ${SQ}${HEAD}${SQ} and now points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects no change' '
+	(
+		cd test &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} is unchanged and points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects change' '
+	(
+		cd test &&
+		git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/ahead &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} has changed from ${SQ}ahead${SQ} and now points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects strange ref' '
+	(
+		cd test &&
+		git symbolic-ref refs/remotes/origin/HEAD refs/heads/main &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} used to point to ${SQ}refs/heads/main${SQ} (which is not a remote branch), but now points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
 test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
 	(
 		cd test &&
 		git fetch two "refs/heads/*:refs/remotes/two/*" &&
 		git remote set-head --auto two >output 2>&1 &&
-		echo "two/HEAD set to main" >expect &&
+		echo "${SQ}two/HEAD${SQ} is now created and points to ${SQ}main${SQ}" >expect &&
 		test_cmp expect output
 	)
 '
@@ -456,6 +507,16 @@ test_expect_success 'set-head explicit' '
 	)
 '
 
+test_expect_success 'set-head --auto reports change' '
+	(
+		cd test &&
+		git remote set-head origin side2 &&
+		git remote set-head --auto origin >output 2>&1 &&
+		echo "${SQ}origin/HEAD${SQ} has changed from ${SQ}side2${SQ} and now points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
 cat >test/expect <<EOF
 Pruning origin
 URL: $(pwd)/one
-- 
2.47.0.296.gda1ecfef29.dirty
Junio C Hamano· Nov 19, 2024, 02:27 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v13 5/9] remote set-head: better output for --auto

Bence Ferdinandy <bence@ferdinandy.com> writes:
> +static void report_set_head_auto(const char *remote, const char *head_name,
> +			struct strbuf *b_local_head, int updateres) {

"updateres" was too mysterious a name. "res" stands for what, "resource"?

Looking at the way the parameter is used by the code, it seems to indicate that the remote HEAD originally was in a detached state, so "was_detached" may be a better name, perhaps?

> +	else if (!!updateres && b_local_head->len)
> +		printf(_("'%s/HEAD' was detached at '%s' and now points to '%s'\n"),
> +			remote, b_local_head->buf, head_name);

There is no need for !!; any non-zero integer is true. !! is useful only in a context that takes only 0 and 1 (like when you are making an assignment to a variable or a structure member that takes only 0 or 1).

Show 7 quoted lines
>  static int set_head(int argc, const char **argv, const char *prefix)
>  {
> -	int i, opt_a = 0, opt_d = 0, result = 0;
> -	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT;
> +	int i, opt_a = 0, opt_d = 0, result = 0, updateres;
> +	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
> +		b_local_head = STRBUF_INIT;
Show 21 quoted lines
> @@ -1440,20 +1468,27 @@ static int set_head(int argc, const char **argv, const char *prefix)
>  	} else
>  		usage_with_options(builtin_remote_sethead_usage, options);
>  
> -	if (head_name) {
> -		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", argv[0], head_name);
> -		/* make sure it's valid */
> -		if (!refs_ref_exists(refs, b_remote_head.buf))
> -			result |= error(_("Not a valid ref: %s"), b_remote_head.buf);
> -		else if (refs_update_symref(refs, b_head.buf, b_remote_head.buf, "remote set-head"))
> -			result |= error(_("Could not setup %s"), b_head.buf);
> -		else if (opt_a)
> -			printf("%s/HEAD set to %s\n", argv[0], head_name);
> -		free(head_name);
> +	if (!head_name)
> +		goto cleanup;
> +	strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", argv[0], head_name);
> +	if (!refs_ref_exists(refs, b_remote_head.buf)) {
> +		result |= error(_("Not a valid ref: %s"), b_remote_head.buf);
> +		goto cleanup;
> +	}

OK, we refuse to allow a manual "remote set-head" to create a dangling symref, which is a faithful rewrite from the original.

> +	updateres = refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
> +			"remote set-head", &b_local_head);
> +	if (updateres == -2) {

Where does this -2 come from? It is not the "you asked to read it as a symref but it wasn't a symref" thing, which was mapped to -1 with [PATCH 3/9].

It is an unusual way to construct an extensible API function to say "all different kinds of errors we happen to know when this particular caller was written return -2, but some special cases are not -2".

Rather, "all negatives, other than these selected few values we special-case and handle, are errors" is more natural, isn't it?

Maybe I am misreading the code and missing where the -2 comes from or the significance of the value? I dunno.

Bence Ferdinandy· Nov 19, 2024, 10:29 UTC · re: Junio C Hamano · lore

Re: [PATCH v13 5/9] remote set-head: better output for --auto

On Tue Nov 19, 2024 at 03:27, Junio C Hamano <gitster@pobox.com> wrote:
Show 11 quoted lines
> Bence Ferdinandy <bence@ferdinandy.com> writes:
>
>> +static void report_set_head_auto(const char *remote, const char *head_name,
>> +			struct strbuf *b_local_head, int updateres) {
>
> "updateres" was too mysterious a name.  "res" stands for what,
> "resource"?
>
> Looking at the way the parameter is used by the code, it seems to
> indicate that the remote HEAD originally was in a detached state, so
> "was_detached" may be a better name, perhaps?
"res" wanted to be short for result, but "was_detached" is definitely more readable.
Show 51 quoted lines
>
>> +	else if (!!updateres && b_local_head->len)
>> +		printf(_("'%s/HEAD' was detached at '%s' and now points to '%s'\n"),
>> +			remote, b_local_head->buf, head_name);
>
> There is no need for !!; any non-zero integer is true.  !! is useful
> only in a context that takes only 0 and 1 (like when you are making
> an assignment to a variable or a structure member that takes only 0
> or 1).
>
>>  static int set_head(int argc, const char **argv, const char *prefix)
>>  {
>> -	int i, opt_a = 0, opt_d = 0, result = 0;
>> -	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT;
>> +	int i, opt_a = 0, opt_d = 0, result = 0, updateres;
>> +	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
>> +		b_local_head = STRBUF_INIT;
>
>> @@ -1440,20 +1468,27 @@ static int set_head(int argc, const char **argv, const char *prefix)
>>  	} else
>>  		usage_with_options(builtin_remote_sethead_usage, options);
>>  
>> -	if (head_name) {
>> -		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", argv[0], head_name);
>> -		/* make sure it's valid */
>> -		if (!refs_ref_exists(refs, b_remote_head.buf))
>> -			result |= error(_("Not a valid ref: %s"), b_remote_head.buf);
>> -		else if (refs_update_symref(refs, b_head.buf, b_remote_head.buf, "remote set-head"))
>> -			result |= error(_("Could not setup %s"), b_head.buf);
>> -		else if (opt_a)
>> -			printf("%s/HEAD set to %s\n", argv[0], head_name);
>> -		free(head_name);
>> +	if (!head_name)
>> +		goto cleanup;
>> +	strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", argv[0], head_name);
>> +	if (!refs_ref_exists(refs, b_remote_head.buf)) {
>> +		result |= error(_("Not a valid ref: %s"), b_remote_head.buf);
>> +		goto cleanup;
>> +	}
>
> OK, we refuse to allow a manual "remote set-head" to create a
> dangling symref, which is a faithful rewrite from the original.
>
>> +	updateres = refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
>> +			"remote set-head", &b_local_head);
>
>> +	if (updateres == -2) {
>
> Where does this -2 come from?  It is not the "you asked to read it
> as a symref but it wasn't a symref" thing, which was mapped to -1
> with [PATCH 3/9].

No, it is not, but it's also a mistake. It should be `updateres == 1`. refs_update_symref_extended outputs -1 for "not a symref" and 1 for any other error currently. Before I touched the code it was 1 for any error, so I left that as is. So we want to error out on set_head if we get a 1 and continue if we get 0 or -1 (and handle the difference in the report_set_head_auto).

Thanks for noticing, I'll get that fixed in v14.
Junio C Hamano· Nov 19, 2024, 10:54 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v13 5/9] remote set-head: better output for --auto

"Bence Ferdinandy" <bence@ferdinandy.com> writes:
Show 7 quoted lines
> No, it is not, but it's also a mistake. It should be `updateres == 1`.
> refs_update_symref_extended outputs -1 for "not a symref" and 1 for any other
> error currently. Before I touched the code it was 1 for any error, so I left
> that as is. So we want to error out on set_head if we get a 1 and continue if
> we get 0 or -1 (and handle the difference in the report_set_head_auto).
>
> Thanks for noticing, I'll get that fixed in v14.

It is good that somebody noticed it (and it may have happened to be me), but if it is a "mistake" as you said, I wonder why none of your tests caught it. Do we have a gap in test coverage?

Thanks.
Bence Ferdinandy· Nov 19, 2024, 11:33 UTC · re: Junio C Hamano · lore

Re: [PATCH v13 5/9] remote set-head: better output for --auto

On Tue Nov 19, 2024 at 11:54, Junio C Hamano <gitster@pobox.com> wrote:
Show 13 quoted lines
> "Bence Ferdinandy" <bence@ferdinandy.com> writes:
>
>> No, it is not, but it's also a mistake. It should be `updateres == 1`.
>> refs_update_symref_extended outputs -1 for "not a symref" and 1 for any other
>> error currently. Before I touched the code it was 1 for any error, so I left
>> that as is. So we want to error out on set_head if we get a 1 and continue if
>> we get 0 or -1 (and handle the difference in the report_set_head_auto).
>>
>> Thanks for noticing, I'll get that fixed in v14.
>
> It is good that somebody noticed it (and it may have happened to be
> me), but if it is a "mistake" as you said, I wonder why none of your
> tests caught it.  Do we have a gap in test coverage?
I think there is no test that is testing this branch:
	updateres = refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
			"remote set-head", &b_local_head, 0);
	if (updateres == 1) {
		result |= error(_("Could not setup %s"), b_head.buf);
		goto cleanup;
Running this in t/ 
	grep -r "Could not setup"

also yield nothing, so that's probably true. I'm wondering what would be the best way to trigger this error, refs_update_symref needs to fail for this.

Bence Ferdinandy· Nov 20, 2024, 12:49 UTC · re: Junio C Hamano · lore

Re: [PATCH v13 5/9] remote set-head: better output for --auto

On Tue Nov 19, 2024 at 11:54, Junio C Hamano <gitster@pobox.com> wrote:
Show 15 quoted lines
> "Bence Ferdinandy" <bence@ferdinandy.com> writes:
>
>> No, it is not, but it's also a mistake. It should be `updateres == 1`.
>> refs_update_symref_extended outputs -1 for "not a symref" and 1 for any other
>> error currently. Before I touched the code it was 1 for any error, so I left
>> that as is. So we want to error out on set_head if we get a 1 and continue if
>> we get 0 or -1 (and handle the difference in the report_set_head_auto).
>>
>> Thanks for noticing, I'll get that fixed in v14.
>
> It is good that somebody noticed it (and it may have happened to be
> me), but if it is a "mistake" as you said, I wonder why none of your
> tests caught it.  Do we have a gap in test coverage?
>
> Thanks.

I've been looking into how I can force an error on remote set-head to test the error branch. For the files backend it seems to be pretty easy

touch .git/refs/remotes/origin/HEAD.lock git remote set-head [something] rm .git/refs/remotes/origin/HEAD.lock

My problem is that in this case, since I want to force an error on a user command, I don't see a way that is possible with git commands and I'm not sure how I could manipulate the reftable file in a way that can be reversed and only errors out the particular thing I need.

I've been looking through the functions called here and it seems other errors all depend passing wrong parameters but that is not possible (and should not be possible) to trigger with remote set-head, so I think something "manual" needs to be done, like the above.

Since this particular test just wants to test what happens if `refs_update_symref_extended` returns with 1 and not testing correct behaviour of backends and such, would it be acceptable if this particular test case would check for the backend and if it is reftables it will just pass without actually checking and do the manually locking thing above for the files backend?

Thanks, Bence

Junio C Hamano· Nov 20, 2024, 23:56 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v13 5/9] remote set-head: better output for --auto

"Bence Ferdinandy" <bence@ferdinandy.com> writes:
Show 5 quoted lines
> Since this particular test just wants to test what happens if
> `refs_update_symref_extended` returns with 1 and not testing correct behaviour
> of backends and such, would it be acceptable if this particular test case would
> check for the backend and if it is reftables it will just pass without actually
> checking and do the manually locking thing above for the files backend?

I think we have some pre-made test prerequisite to skip tests unless run with a particular ref backend, exactly for that. Perhaps

    test_expect_success REFFILES 'block update to check error' '
	... manually block update and see how the operation
	... errors out
    '
or something?
Bence Ferdinandy· Nov 18, 2024, 15:09 UTC · re: Bence Ferdinandy · lore

[PATCH v13 4/9] remote set-head: refactor for readability

Make two different readability refactors:
Rename strbufs "buf" and "buf2" to something more explanatory.

Instead of calling get_main_ref_store(the_repository) multiple times, call it once and store the result in a new refs variable. Although this change probably offers some performance benefits, the main purpose is to shorten the line lengths of function calls using this variable.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v5: new patch (split from the next patch as a preparatory step)
    
    v6: no change
    
    v7: - change commit prefix to be more in line with project standards
    
    v8: no change
    
    v9: - further improve readability by renaming buf, and buf2 consistently
          with how patch 6 was already done
    
    v10: no change
    
    v11: no change
    
    v12: no change
    
    v13: more precise wording for commit message
 builtin/remote.c | 23 ++++++++++++-----------
 1 file changed, 12 insertions(+), 11 deletions(-)
Show changes to builtin/remote.c +12 −11
diff --git a/builtin/remote.c b/builtin/remote.c
index 76670ddd8b..1d68c5b2ba 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1402,8 +1402,9 @@ static int show(int argc, const char **argv, const char *prefix)
 static int set_head(int argc, const char **argv, const char *prefix)
 {
 	int i, opt_a = 0, opt_d = 0, result = 0;
-	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT;
 	char *head_name = NULL;
+	struct ref_store *refs = get_main_ref_store(the_repository);
 
 	struct option options[] = {
 		OPT_BOOL('a', "auto", &opt_a,
@@ -1415,7 +1416,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 	argc = parse_options(argc, argv, prefix, options,
 			     builtin_remote_sethead_usage, 0);
 	if (argc)
-		strbuf_addf(&buf, "refs/remotes/%s/HEAD", argv[0]);
+		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", argv[0]);
 
 	if (!opt_a && !opt_d && argc == 2) {
 		head_name = xstrdup(argv[1]);
@@ -1434,25 +1435,25 @@ static int set_head(int argc, const char **argv, const char *prefix)
 			head_name = xstrdup(states.heads.items[0].string);
 		free_remote_ref_states(&states);
 	} else if (opt_d && !opt_a && argc == 1) {
-		if (refs_delete_ref(get_main_ref_store(the_repository), NULL, buf.buf, NULL, REF_NO_DEREF))
-			result |= error(_("Could not delete %s"), buf.buf);
+		if (refs_delete_ref(refs, NULL, b_head.buf, NULL, REF_NO_DEREF))
+			result |= error(_("Could not delete %s"), b_head.buf);
 	} else
 		usage_with_options(builtin_remote_sethead_usage, options);
 
 	if (head_name) {
-		strbuf_addf(&buf2, "refs/remotes/%s/%s", argv[0], head_name);
+		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", argv[0], head_name);
 		/* make sure it's valid */
-		if (!refs_ref_exists(get_main_ref_store(the_repository), buf2.buf))
-			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head"))
-			result |= error(_("Could not setup %s"), buf.buf);
+		if (!refs_ref_exists(refs, b_remote_head.buf))
+			result |= error(_("Not a valid ref: %s"), b_remote_head.buf);
+		else if (refs_update_symref(refs, b_head.buf, b_remote_head.buf, "remote set-head"))
+			result |= error(_("Could not setup %s"), b_head.buf);
 		else if (opt_a)
 			printf("%s/HEAD set to %s\n", argv[0], head_name);
 		free(head_name);
 	}
 
-	strbuf_release(&buf);
-	strbuf_release(&buf2);
+	strbuf_release(&b_head);
+	strbuf_release(&b_remote_head);
 	return result;
 }
 
-- 
2.47.0.296.gda1ecfef29.dirty
Bence Ferdinandy· Nov 18, 2024, 15:09 UTC · re: Bence Ferdinandy · lore

[PATCH v13 6/9] refs: add TRANSACTION_CREATE_EXISTS error

Currently there is only one special error for transaction, for when there is a naming conflict, all other errors are dumped under a generic error. Add a new special error case for when the caller requests the reference to be updated only when it does not yet exist and the reference actually does exist.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    v5: no change
    v6: no change
    
    v7: - change commit prefix to be more in line with project standards
        - changed error checking to Karthik's suggestion
    
    v8: no change
    
    v9: - no change
    
    v10: no change
    
    v11: no change
    
    v12: no change
    
    v13: no change
 refs.h                  |  4 +++-
 refs/files-backend.c    | 24 ++++++++++++++++--------
 refs/reftable-backend.c |  6 ++++--
 3 files changed, 23 insertions(+), 11 deletions(-)
Show changes to 3 files +23 −11

refs.h, refs/files-backend.c, refs/reftable-backend.c

diff --git a/refs.h b/refs.h
index 92622e807d..6a32f82f8c 100644
--- a/refs.h
+++ b/refs.h
@@ -768,8 +768,10 @@ int ref_transaction_verify(struct ref_transaction *transaction,
 
 /* Naming conflict (for example, the ref names A and A/B conflict). */
 #define TRANSACTION_NAME_CONFLICT -1
+/* When only creation was requested, but the ref already exists. */
+#define TRANSACTION_CREATE_EXISTS -2
 /* All other errors. */
-#define TRANSACTION_GENERIC_ERROR -2
+#define TRANSACTION_GENERIC_ERROR -3
 
 /*
  * Perform the preparatory stages of committing `transaction`. Acquire
diff --git a/refs/files-backend.c b/refs/files-backend.c
index 81e650ec48..86674705a7 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -2501,14 +2501,18 @@ static int split_symref_update(struct ref_update *update,
 static int check_old_oid(struct ref_update *update, struct object_id *oid,
 			 struct strbuf *err)
 {
+	int ret = TRANSACTION_GENERIC_ERROR;
+
 	if (!(update->flags & REF_HAVE_OLD) ||
 		   oideq(oid, &update->old_oid))
 		return 0;
 
-	if (is_null_oid(&update->old_oid))
+	if (is_null_oid(&update->old_oid)) {
 		strbuf_addf(err, "cannot lock ref '%s': "
 			    "reference already exists",
 			    ref_update_original_update_refname(update));
+		ret = TRANSACTION_CREATE_EXISTS;
+	}
 	else if (is_null_oid(oid))
 		strbuf_addf(err, "cannot lock ref '%s': "
 			    "reference is missing but expected %s",
@@ -2521,7 +2525,7 @@ static int check_old_oid(struct ref_update *update, struct object_id *oid,
 			    oid_to_hex(oid),
 			    oid_to_hex(&update->old_oid));
 
-	return -1;
+	return ret;
 }
 
 /*
@@ -2601,9 +2605,11 @@ static int lock_ref_for_update(struct files_ref_store *refs,
 					ret = TRANSACTION_GENERIC_ERROR;
 					goto out;
 				}
-			} else if  (check_old_oid(update, &lock->old_oid, err)) {
-				ret = TRANSACTION_GENERIC_ERROR;
-				goto out;
+			} else {
+				ret = check_old_oid(update, &lock->old_oid, err);
+				if  (ret) {
+					goto out;
+				}
 			}
 		} else {
 			/*
@@ -2634,9 +2640,11 @@ static int lock_ref_for_update(struct files_ref_store *refs,
 				    update->old_target);
 			ret = TRANSACTION_GENERIC_ERROR;
 			goto out;
-		} else if  (check_old_oid(update, &lock->old_oid, err)) {
-			ret = TRANSACTION_GENERIC_ERROR;
-			goto out;
+		} else {
+			ret = check_old_oid(update, &lock->old_oid, err);
+			if  (ret) {
+				goto out;
+			}
 		}
 
 		/*
diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
index 60cb83f23a..4f2e615eae 100644
--- a/refs/reftable-backend.c
+++ b/refs/reftable-backend.c
@@ -1208,10 +1208,13 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 				goto done;
 			}
 		} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {
-			if (is_null_oid(&u->old_oid))
+			ret = TRANSACTION_NAME_CONFLICT;
+			if (is_null_oid(&u->old_oid)) {
 				strbuf_addf(err, _("cannot lock ref '%s': "
 						   "reference already exists"),
 					    ref_update_original_update_refname(u));
+				ret = TRANSACTION_CREATE_EXISTS;
+			}
 			else if (is_null_oid(&current_oid))
 				strbuf_addf(err, _("cannot lock ref '%s': "
 						   "reference is missing but expected %s"),
@@ -1223,7 +1226,6 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 					    ref_update_original_update_refname(u),
 					    oid_to_hex(&current_oid),
 					    oid_to_hex(&u->old_oid));
-			ret = -1;
 			goto done;
 		}
 
-- 
2.47.0.296.gda1ecfef29.dirty
Bence Ferdinandy· Nov 18, 2024, 15:09 UTC · re: Bence Ferdinandy · lore

[PATCH v13 7/9] refs: add create_only option to refs_update_symref_extended

Allow the caller to specify that it only wants to update the symref if it does not already exist. Silently ignore the error from the transaction API if the symref already exists.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    v5: no change
    
    v6: - switched from bool to int for create_only
        - refactored logic in refs_update_symref with goto as layed out by
          Junio
    
    v7: - change commit prefix to be more in line with project standards
        - refactored code to accommodate changes in the first patch, but
          otherwise no change
    
    v8: no change
    
    v9: - no change (except for following through the
          refs_update_symref_extended refactoring)
    
    v10: no change
    
    v11: no change
    
    v12: no change
    
    v13: changes only due to changes in previous patch
 builtin/remote.c |  2 +-
 refs.c           | 33 ++++++++++++++++++++++++---------
 refs.h           |  2 +-
 3 files changed, 26 insertions(+), 11 deletions(-)
Show changes to 3 files +26 −11

builtin/remote.c, refs.c, refs.h

diff --git a/builtin/remote.c b/builtin/remote.c
index a682ef5df2..7b4eb0e30b 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1476,7 +1476,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		goto cleanup;
 	}
 	updateres = refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
-			"remote set-head", &b_local_head);
+			"remote set-head", &b_local_head, 0);
 	if (updateres == -2) {
 		result |= error(_("Could not setup %s"), b_head.buf);
 		goto cleanup;
diff --git a/refs.c b/refs.c
index c4500a7582..f4d8a8dcb1 100644
--- a/refs.c
+++ b/refs.c
@@ -2116,26 +2116,38 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
 int refs_update_symref(struct ref_store *refs, const char *ref,
 		       const char *target, const char *logmsg)
 {
-	return refs_update_symref_extended(refs, ref, target, logmsg, NULL);
+	return refs_update_symref_extended(refs, ref, target, logmsg, NULL, 0);
 }
 
 int refs_update_symref_extended(struct ref_store *refs, const char *ref,
 		       const char *target, const char *logmsg,
-		       struct strbuf *referent)
+		       struct strbuf *referent, int create_only)
 {
 	struct ref_transaction *transaction;
 	struct strbuf err = STRBUF_INIT;
-	int ret = 0;
+	int ret = 0, prepret = 0;
 
 	transaction = ref_store_transaction_begin(refs, &err);
-	if (!transaction ||
-	    ref_transaction_update(transaction, ref, NULL, NULL,
-				   target, NULL, REF_NO_DEREF,
-				   logmsg, &err) ||
-	    ref_transaction_prepare(transaction, &err)) {
+	if (!transaction) {
+	error_return:
 		ret = error("%s", err.buf);
 		goto cleanup;
 	}
+	if (create_only) {
+		if (ref_transaction_create(transaction, ref, NULL, target,
+					   REF_NO_DEREF, logmsg, &err))
+			goto error_return;
+		prepret = ref_transaction_prepare(transaction, &err);
+		if (prepret && prepret != TRANSACTION_CREATE_EXISTS)
+			goto error_return;
+	} else {
+		if (ref_transaction_update(transaction, ref, NULL, NULL,
+					   target, NULL, REF_NO_DEREF,
+					   logmsg, &err) ||
+			ref_transaction_prepare(transaction, &err))
+			goto error_return;
+	}
+
 	if (referent && refs_read_symbolic_ref(refs, ref, referent) == -2) {
 		struct object_id oid;
 		if (!refs_read_ref(refs, ref, &oid)) {
@@ -2146,8 +2158,11 @@ int refs_update_symref_extended(struct ref_store *refs, const char *ref,
 		}
 	}
 
+	if (prepret == TRANSACTION_CREATE_EXISTS)
+		goto cleanup;
+
 	if (ref_transaction_commit(transaction, &err))
-		ret = error("%s", err.buf);
+		goto error_return;
 
 cleanup:
 	strbuf_release(&err);
diff --git a/refs.h b/refs.h
index 6a32f82f8c..f131e89fe6 100644
--- a/refs.h
+++ b/refs.h
@@ -581,7 +581,7 @@ int refs_update_symref(struct ref_store *refs, const char *refname,
 
 int refs_update_symref_extended(struct ref_store *refs, const char *refname,
 		       const char *target, const char *logmsg,
-		       struct strbuf *referent);
+		       struct strbuf *referent, int create_only);
 
 enum action_on_err {
 	UPDATE_REFS_MSG_ON_ERR,
-- 
2.47.0.296.gda1ecfef29.dirty
Junio C Hamano· Nov 19, 2024, 02:54 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v13 7/9] refs: add create_only option to refs_update_symref_extended

Bence Ferdinandy <bence@ferdinandy.com> writes:
> Allow the caller to specify that it only wants to update the symref if
> it does not already exist. Silently ignore the error from the
> transaction API if the symref already exists.

Because it is not even an error, from the point of view of "create only if it does not exist", checking the current condition and finding that there already is one, this makes perfect sense.

Bence Ferdinandy· Nov 18, 2024, 15:09 UTC · re: Bence Ferdinandy · lore

[PATCH v13 9/9] fetch set_head: handle mirrored bare repositories

When adding a remote to bare repository with "git remote add --mirror",
running fetch will fail to update HEAD to the remote's HEAD, since it
does not know how to handle bare repositories. On the other hand HEAD
already has content, since "git init --bare" has already set HEAD to
whatever is the default branch set for the user. Unless this - by chance
- is the same as the remote's HEAD, HEAD will be pointing to a bad
symref. Teach set_head to handle bare repositories, by overwriting HEAD
so it mirrors the remote's HEAD.

Note, that in this case overriding the local HEAD reference is necessary, since HEAD will exist before fetch can be run, but this should not be an issue, since the whole purpose of --mirror is to be an exact mirror of the remote, so following any changes to HEAD makes sense.

Also note, that although "git remote set-head" also fails when trying to update the remote's locally tracked HEAD in a mirrored bare repository, the usage of the command does not make much sense after this patch: fetch will update the remote HEAD correctly, and setting it manually to something else is antithetical to the concept of mirroring.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v10: - new patch
         - handles the issue discovered in
           https://lore.kernel.org/git/D4ZAELFWJMKN.S88LJ6YK31LZ@ferdinandy.com/T/
    
    v11: no change
    
    v12: no change
    
    v13: properly print the previous head if it was detached
 builtin/fetch.c   | 16 +++++++++++-----
 t/t5505-remote.sh | 10 ++++++++++
 2 files changed, 21 insertions(+), 5 deletions(-)
Show changes to 2 files +21 −5

builtin/fetch.c, t/t5505-remote.sh

diff --git a/builtin/fetch.c b/builtin/fetch.c
index 3d70cd1add..5e37b9c00f 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1608,7 +1608,7 @@ static const char *strip_refshead(const char *name){
 
 static int set_head(const struct ref *remote_refs)
 {
-	int result = 0, updateres;
+	int result = 0, updateres, is_bare;
 	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
 		b_local_head = STRBUF_INIT;
 	const char *remote = gtransport->remote->name;
@@ -1641,15 +1641,21 @@ static int set_head(const struct ref *remote_refs)
 
 	if (!head_name)
 		goto cleanup;
-	strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
-	strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote, head_name);
+	is_bare = is_bare_repository();
+	if (is_bare) {
+		strbuf_addstr(&b_head, "HEAD");
+		strbuf_addf(&b_remote_head, "refs/heads/%s", head_name);
+	} else {
+		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
+		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote, head_name);
+	}
 		/* make sure it's valid */
-	if (!refs_ref_exists(refs, b_remote_head.buf)) {
+	if (!is_bare && !refs_ref_exists(refs, b_remote_head.buf)) {
 		result = 1;
 		goto cleanup;
 	}
 	updateres = refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
-					"fetch", &b_local_head, 1);
+					"fetch", &b_local_head, !is_bare);
 	if (updateres == -1) {
 		result = 1;
 		goto cleanup;
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 660310239c..4cc5df2e61 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -557,6 +557,16 @@ test_expect_success 'add --mirror && prune' '
 	)
 '
 
+test_expect_success 'add --mirror setting HEAD' '
+	mkdir headmirror &&
+	(
+		cd headmirror &&
+		git init --bare -b notmain &&
+		git remote add --mirror -f origin ../one &&
+		test "$(git symbolic-ref HEAD)" = "refs/heads/main"
+	)
+'
+
 test_expect_success 'add --mirror=fetch' '
 	mkdir mirror-fetch &&
 	git init -b main mirror-fetch/parent &&
-- 
2.47.0.296.gda1ecfef29.dirty
Bence Ferdinandy· Nov 18, 2024, 15:09 UTC · re: Bence Ferdinandy · lore

[PATCH v13 8/9] fetch: set remote/HEAD if it does not exist

If the user has remote/HEAD set already and it looks like it has changed on the server, then print a message, otherwise set it if we can. Silently pass if the user already has the same remote/HEAD set as reported by the server or if we encounter any errors along the way.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v3: - does not rely on remote set-head anymore so it only authenticates
        once
        - uses the new REF_CREATE_ONLY to atomically check if the ref exists
          and only write it if it doesn't
        - in all other cases the maximum it does is print a warning
    
    v4: - instead of the discarded REF_CREATE_ONLY, it uses the existing,
          but updated transaction api to request a silent create only
        - it now uses the atomic before_target to determine reporting
        - refactored for legibility
    
    v5: - instead of printing a not too useful message, it now fails
          silently, this in line with the objective to only set up
          remote/HEAD automatically if the right thing is trivial, for
          everything else there is remote set-head
        - fixed all failing tests
        - added two new tests, one for checking if remote/HEAD is set to the
          correct one, and one to test that we do not override remote/HEAD
          if it has changed on the server from what we have locally
    
    v6: - fixed style issues and unintended extra empty line
        - updated function call with bool to int from previous patch's
          change
        - removed calls to error(...) inherited from builtin/remote.c so we
          actually fail silently
        - set the test for remote set-head --auto to the correct value here,
          which was previously erronously set in the remote set-head patch
    
    v7: - no change
    
    v8: - changed logmsg in call to refs_update_symref from "remote
          set-head" to "fetch"
    
    v9: - follow through with refs_update_symref_extended
        - fix test errors uncovered by the new patch
    
    v10: no change
    
    v11: fixed some memory leaks
    
    v12: no change
    
    v13: - fix printed information if the local HEAD is detached
         - remove accidental formatting noise in a test
 builtin/fetch.c                  | 99 ++++++++++++++++++++++++++++++++
 t/t4207-log-decoration-colors.sh |  3 +-
 t/t5505-remote.sh                | 21 +++++--
 t/t5510-fetch.sh                 | 24 ++++++++
 t/t5512-ls-remote.sh             |  2 +
 t/t5514-fetch-multiple.sh        | 17 +++++-
 t/t5516-fetch-push.sh            |  3 +-
 t/t5527-fetch-odd-refs.sh        |  3 +-
 t/t7900-maintenance.sh           |  3 +-
 t/t9210-scalar.sh                |  5 +-
 t/t9211-scalar-clone.sh          |  6 +-
 t/t9902-completion.sh            | 65 +++++++++++++++++++++
 12 files changed, 234 insertions(+), 17 deletions(-)
Show changes to 12 files +234 −17

builtin/fetch.c, t/t4207-log-decoration-colors.sh, t/t5505-remote.sh, t/t5510-fetch.sh, t/t5512-ls-remote.sh, t/t5514-fetch-multiple.sh, t/t5516-fetch-push.sh, t/t5527-fetch-odd-refs.sh, t/t7900-maintenance.sh, t/t9210-scalar.sh, t/t9211-scalar-clone.sh, t/t9902-completion.sh

diff --git a/builtin/fetch.c b/builtin/fetch.c
index 18eff4e5fa..3d70cd1add 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1578,6 +1578,97 @@ static int backfill_tags(struct display_state *display_state,
 	return retcode;
 }
 
+static void report_set_head(const char *remote, const char *head_name,
+			struct strbuf *buf_prev, int updateres) {
+	struct strbuf buf_prefix = STRBUF_INIT;
+	const char *prev_head = NULL;
+
+	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
+	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
+
+	if (prev_head && strcmp(prev_head, head_name)) {
+		printf("'HEAD' at '%s' has changed from '%s' to '%s'\n",
+			remote, prev_head, head_name);
+		printf("Run 'git remote set-head %s %s' to follow the change.\n",
+			remote, head_name);
+	}
+	else if (!!updateres && buf_prev->len) {
+		printf("detached 'HEAD' at '%s' has changed from '%s' to '%s'\n",
+			remote, buf_prev->buf, head_name);
+		printf("Run 'git remote set-head %s %s' to follow the change.\n",
+			remote, head_name);
+	}
+	strbuf_release(&buf_prefix);
+}
+
+static const char *strip_refshead(const char *name){
+	skip_prefix(name, "refs/heads/", &name);
+	return name;
+}
+
+static int set_head(const struct ref *remote_refs)
+{
+	int result = 0, updateres;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
+		b_local_head = STRBUF_INIT;
+	const char *remote = gtransport->remote->name;
+	char *head_name = NULL;
+	struct ref *ref, *matches;
+	struct ref *fetch_map = NULL, **fetch_map_tail = &fetch_map;
+	struct refspec_item refspec = {
+		.force = 0,
+		.pattern = 1,
+		.src = (char *) "refs/heads/*",
+		.dst = (char *) "refs/heads/*",
+	};
+	struct string_list heads = STRING_LIST_INIT_DUP;
+	struct ref_store *refs = get_main_ref_store(the_repository);
+
+	get_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);
+	matches = guess_remote_head(find_ref_by_name(remote_refs, "HEAD"),
+				    fetch_map, 1);
+	for (ref = matches; ref; ref = ref->next) {
+		string_list_append(&heads, strip_refshead(ref->name));
+	}
+
+
+	if (!heads.nr)
+		result = 1;
+	else if (heads.nr > 1)
+		result = 1;
+	else
+		head_name = xstrdup(heads.items[0].string);
+
+	if (!head_name)
+		goto cleanup;
+	strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
+	strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote, head_name);
+		/* make sure it's valid */
+	if (!refs_ref_exists(refs, b_remote_head.buf)) {
+		result = 1;
+		goto cleanup;
+	}
+	updateres = refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
+					"fetch", &b_local_head, 1);
+	if (updateres == -1) {
+		result = 1;
+		goto cleanup;
+	}
+	report_set_head(remote, head_name, &b_local_head, updateres);
+
+
+
+cleanup:
+	free(head_name);
+	free_refs(fetch_map);
+	free_refs(matches);
+	string_list_clear(&heads, 0);
+	strbuf_release(&b_head);
+	strbuf_release(&b_local_head);
+	strbuf_release(&b_remote_head);
+	return result;
+}
+
 static int do_fetch(struct transport *transport,
 		    struct refspec *rs,
 		    const struct fetch_config *config)
@@ -1647,6 +1738,8 @@ static int do_fetch(struct transport *transport,
 				    "refs/tags/");
 	}
 
+	strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
+
 	if (must_list_refs) {
 		trace2_region_enter("fetch", "remote_refs", the_repository);
 		remote_refs = transport_get_remote_refs(transport,
@@ -1791,6 +1884,12 @@ static int do_fetch(struct transport *transport,
 				  "you need to specify exactly one branch with the --set-upstream option"));
 		}
 	}
+	if (set_head(remote_refs))
+		;
+		/*
+		 * Way too many cases where this can go wrong
+		 * so let's just fail silently for now.
+		 */
 
 cleanup:
 	if (retcode) {
diff --git a/t/t4207-log-decoration-colors.sh b/t/t4207-log-decoration-colors.sh
index 73ea9e5155..d55d22cb2f 100755
--- a/t/t4207-log-decoration-colors.sh
+++ b/t/t4207-log-decoration-colors.sh
@@ -59,7 +59,8 @@ ${c_reset}${c_tag}tag: ${c_reset}${c_tag}v1.0${c_reset}${c_commit}, \
 ${c_reset}${c_tag}tag: ${c_reset}${c_tag}B${c_reset}${c_commit})${c_reset} B
 ${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
 ${c_tag}tag: ${c_reset}${c_tag}A1${c_reset}${c_commit}, \
-${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit})${c_reset} A1
+${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit}, \
+${c_reset}${c_remoteBranch}other/HEAD${c_reset}${c_commit})${c_reset} A1
 	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
 ${c_stash}refs/stash${c_reset}${c_commit})${c_reset} On main: Changes to A.t
 	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 807df00ba7..660310239c 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -74,7 +74,7 @@ test_expect_success 'add another remote' '
 		cd test &&
 		git remote add -f second ../two &&
 		tokens_match "origin second" "$(git remote)" &&
-		check_tracking_branch second main side another &&
+		check_tracking_branch second main side another HEAD &&
 		git for-each-ref "--format=%(refname)" refs/remotes |
 		sed -e "/^refs\/remotes\/origin\//d" \
 		    -e "/^refs\/remotes\/second\//d" >actual &&
@@ -488,7 +488,7 @@ test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
 		cd test &&
 		git fetch two "refs/heads/*:refs/remotes/two/*" &&
 		git remote set-head --auto two >output 2>&1 &&
-		echo "${SQ}two/HEAD${SQ} is now created and points to ${SQ}main${SQ}" >expect &&
+		echo "${SQ}two/HEAD${SQ} is unchanged and points to ${SQ}main${SQ}" >expect &&
 		test_cmp expect output
 	)
 '
@@ -776,8 +776,10 @@ test_expect_success 'reject --no-no-tags' '
 '
 
 cat >one/expect <<\EOF
+  apis/HEAD -> apis/main
   apis/main
   apis/side
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
@@ -795,11 +797,14 @@ test_expect_success 'update' '
 '
 
 cat >one/expect <<\EOF
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
+  manduca/HEAD -> manduca/main
   manduca/main
   manduca/side
+  megaloprepus/HEAD -> megaloprepus/main
   megaloprepus/main
   megaloprepus/side
 EOF
@@ -807,7 +812,7 @@ EOF
 test_expect_success 'update with arguments' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -839,10 +844,13 @@ test_expect_success 'update --prune' '
 '
 
 cat >one/expect <<-\EOF
+  apis/HEAD -> apis/main
   apis/main
   apis/side
+  manduca/HEAD -> manduca/main
   manduca/main
   manduca/side
+  megaloprepus/HEAD -> megaloprepus/main
   megaloprepus/main
   megaloprepus/side
 EOF
@@ -850,7 +858,7 @@ EOF
 test_expect_success 'update default' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -862,6 +870,7 @@ test_expect_success 'update default' '
 '
 
 cat >one/expect <<\EOF
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
@@ -870,7 +879,7 @@ EOF
 test_expect_success 'update default (overridden, with funny whitespace)' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -884,7 +893,7 @@ test_expect_success 'update default (overridden, with funny whitespace)' '
 test_expect_success 'update (with remotes.default defined)' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index 0890b9f61c..87698341f5 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -75,6 +75,30 @@ test_expect_success "fetch test for-merge" '
 	cut -f -2 .git/FETCH_HEAD >actual &&
 	test_cmp expected actual'
 
+test_expect_success "fetch test remote HEAD" '
+	cd "$D" &&
+	cd two &&
+	git fetch &&
+	git rev-parse --verify refs/remotes/origin/HEAD &&
+	git rev-parse --verify refs/remotes/origin/main &&
+	head=$(git rev-parse refs/remotes/origin/HEAD) &&
+	branch=$(git rev-parse refs/remotes/origin/main) &&
+	test "z$head" = "z$branch"'
+
+test_expect_success "fetch test remote HEAD change" '
+	cd "$D" &&
+	cd two &&
+	git switch -c other &&
+	git push -u origin other &&
+	git rev-parse --verify refs/remotes/origin/HEAD &&
+	git rev-parse --verify refs/remotes/origin/main &&
+	git rev-parse --verify refs/remotes/origin/other &&
+	git remote set-head origin other &&
+	git fetch &&
+	head=$(git rev-parse refs/remotes/origin/HEAD) &&
+	branch=$(git rev-parse refs/remotes/origin/other) &&
+	test "z$head" = "z$branch"'
+
 test_expect_success 'fetch --prune on its own works as expected' '
 	cd "$D" &&
 	git clone . prune &&
diff --git a/t/t5512-ls-remote.sh b/t/t5512-ls-remote.sh
index 64b3491e4e..1b3865e154 100755
--- a/t/t5512-ls-remote.sh
+++ b/t/t5512-ls-remote.sh
@@ -293,6 +293,8 @@ test_expect_success 'ls-remote with filtered symref (refname)' '
 	cat >expect <<-EOF &&
 	ref: refs/heads/main	HEAD
 	$rev	HEAD
+	ref: refs/remotes/origin/main	refs/remotes/origin/HEAD
+	$rev	refs/remotes/origin/HEAD
 	EOF
 	git ls-remote --symref . HEAD >actual &&
 	test_cmp expect actual
diff --git a/t/t5514-fetch-multiple.sh b/t/t5514-fetch-multiple.sh
index 579872c258..e3482b27b2 100755
--- a/t/t5514-fetch-multiple.sh
+++ b/t/t5514-fetch-multiple.sh
@@ -45,14 +45,17 @@ test_expect_success setup '
 '
 
 cat > test/expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
   origin/HEAD -> origin/main
   origin/main
   origin/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -97,6 +100,7 @@ cat > expect << EOF
   origin/HEAD -> origin/main
   origin/main
   origin/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
@@ -112,8 +116,10 @@ test_expect_success 'git fetch --multiple (but only one remote)' '
 '
 
 cat > expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -141,7 +147,7 @@ test_expect_success 'git fetch --multiple (bad remote names)' '
 
 test_expect_success 'git fetch --all (skipFetchAll)' '
 	(cd test4 &&
-	 for b in $(git branch -r)
+	 for b in $(git branch -r | grep -v HEAD)
 	 do
 		git branch -r -d $b || exit 1
 	 done &&
@@ -153,11 +159,14 @@ test_expect_success 'git fetch --all (skipFetchAll)' '
 '
 
 cat > expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -165,7 +174,7 @@ EOF
 
 test_expect_success 'git fetch --multiple (ignoring skipFetchAll)' '
 	(cd test4 &&
-	 for b in $(git branch -r)
+	 for b in $(git branch -r | grep -v HEAD)
 	 do
 		git branch -r -d $b || exit 1
 	 done &&
@@ -221,14 +230,17 @@ test_expect_success 'git fetch --multiple --jobs=0 picks a default' '
 
 create_fetch_all_expect () {
 	cat >expect <<-\EOF
+	  one/HEAD -> one/main
 	  one/main
 	  one/side
 	  origin/HEAD -> origin/main
 	  origin/main
 	  origin/side
+	  three/HEAD -> three/main
 	  three/another
 	  three/main
 	  three/side
+	  two/HEAD -> two/main
 	  two/another
 	  two/main
 	  two/side
@@ -265,6 +277,7 @@ test_expect_success 'git fetch (fetch all remotes with fetch.all = true)' '
 
 create_fetch_one_expect () {
 	cat >expect <<-\EOF
+	  one/HEAD -> one/main
 	  one/main
 	  one/side
 	  origin/HEAD -> origin/main
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 331778bd42..5a051aa0c7 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1395,7 +1395,8 @@ test_expect_success 'fetch follows tags by default' '
 		git tag -m "annotated" tag &&
 		git for-each-ref >tmp1 &&
 		sed -n "p; s|refs/heads/main$|refs/remotes/origin/main|p" tmp1 |
-		sort -k 3 >../expect
+		sed -n "p; s|refs/heads/main$|refs/remotes/origin/HEAD|p"  |
+		sort -k 4 >../expect
 	) &&
 	test_when_finished "rm -rf dst" &&
 	git init dst &&
diff --git a/t/t5527-fetch-odd-refs.sh b/t/t5527-fetch-odd-refs.sh
index 98ece27c6a..d3996af6ee 100755
--- a/t/t5527-fetch-odd-refs.sh
+++ b/t/t5527-fetch-odd-refs.sh
@@ -52,7 +52,8 @@ test_expect_success LONG_REF 'fetch handles extremely long refname' '
 	long
 	main
 	EOF
-	git for-each-ref --format="%(subject)" refs/remotes/long >actual &&
+	git for-each-ref --format="%(subject)" refs/remotes/long \
+		--exclude=refs/remotes/long/HEAD >actual &&
 	test_cmp expect actual
 '
 
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index c224c8450c..edb85b7145 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -329,7 +329,8 @@ test_expect_success 'incremental-repack task' '
 
 	# Delete refs that have not been repacked in these packs.
 	git for-each-ref --format="delete %(refname)" \
-		refs/prefetch refs/tags refs/remotes >refs &&
+		refs/prefetch refs/tags refs/remotes \
+		--exclude=refs/remotes/*/HEAD >refs &&
 	git update-ref --stdin <refs &&
 
 	# Replace the object directory with this pack layout.
diff --git a/t/t9210-scalar.sh b/t/t9210-scalar.sh
index a30b2c9f70..2237844550 100755
--- a/t/t9210-scalar.sh
+++ b/t/t9210-scalar.sh
@@ -151,7 +151,8 @@ test_expect_success 'scalar clone' '
 			"$(pwd)" &&
 
 		git for-each-ref --format="%(refname)" refs/remotes/origin/ >actual &&
-		echo "refs/remotes/origin/parallel" >expect &&
+		echo "refs/remotes/origin/HEAD" >>expect &&
+		echo "refs/remotes/origin/parallel" >>expect &&
 		test_cmp expect actual &&
 
 		test_path_is_missing 1/2 &&
@@ -220,7 +221,7 @@ test_expect_success 'scalar reconfigure --all with includeIf.onbranch' '
 	done
 '
 
- test_expect_success 'scalar reconfigure --all with detached HEADs' '
+test_expect_success 'scalar reconfigure --all with detached HEADs' '
 	repos="two three four" &&
 	for num in $repos
 	do
diff --git a/t/t9211-scalar-clone.sh b/t/t9211-scalar-clone.sh
index c16ea67c1d..d9cb6b9a3e 100755
--- a/t/t9211-scalar-clone.sh
+++ b/t/t9211-scalar-clone.sh
@@ -32,7 +32,7 @@ test_expect_success 'set up repository to clone' '
 	)
 '
 
-cleanup_clone () {
+cleanup_clone() {
 	rm -rf "$1"
 }
 
@@ -128,7 +128,7 @@ test_expect_success '--single-branch clones HEAD only' '
 	(
 		cd $enlistment/src &&
 		git for-each-ref refs/remotes/origin >out &&
-		test_line_count = 1 out &&
+		test_line_count = 2 out &&
 		grep "refs/remotes/origin/base" out
 	) &&
 
@@ -142,7 +142,7 @@ test_expect_success '--no-single-branch clones all branches' '
 	(
 		cd $enlistment/src &&
 		git for-each-ref refs/remotes/origin >out &&
-		test_line_count = 2 out &&
+		test_line_count = 3 out &&
 		grep "refs/remotes/origin/base" out &&
 		grep "refs/remotes/origin/parallel" out
 	) &&
diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh
index cc6aa9f0cd..b663c4609e 100755
--- a/t/t9902-completion.sh
+++ b/t/t9902-completion.sh
@@ -658,6 +658,7 @@ test_expect_success '__git_refs - simple' '
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	matching-tag
@@ -673,6 +674,7 @@ test_expect_success '__git_refs - full refs' '
 	cat >expected <<-EOF &&
 	refs/heads/main
 	refs/heads/matching-branch
+	refs/remotes/other/HEAD
 	refs/remotes/other/branch-in-other
 	refs/remotes/other/main-in-other
 	refs/tags/matching-tag
@@ -729,6 +731,7 @@ test_expect_success '__git_refs - remote on local file system - full refs' '
 test_expect_success '__git_refs - configured remote' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -756,6 +759,7 @@ test_expect_success '__git_refs - configured remote - full refs' '
 test_expect_success '__git_refs - configured remote - repo given on the command line' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -787,6 +791,7 @@ test_expect_success '__git_refs - configured remote - full refs - repo given on
 test_expect_success '__git_refs - configured remote - remote name matches a directory' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -875,12 +880,14 @@ test_expect_success '__git_refs - unique remote branches for git checkout DWIMer
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/ambiguous
 	other/branch-in-other
 	other/main-in-other
 	remote/ambiguous
 	remote/branch-in-remote
 	matching-tag
+	HEAD
 	branch-in-other
 	branch-in-remote
 	main-in-other
@@ -904,6 +911,7 @@ test_expect_success '__git_refs - after --opt=' '
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	matching-tag
@@ -919,6 +927,7 @@ test_expect_success '__git_refs - after --opt= - full refs' '
 	cat >expected <<-EOF &&
 	refs/heads/main
 	refs/heads/matching-branch
+	refs/remotes/other/HEAD
 	refs/remotes/other/branch-in-other
 	refs/remotes/other/main-in-other
 	refs/tags/matching-tag
@@ -935,6 +944,7 @@ test_expect_success '__git refs - excluding refs' '
 	^HEAD
 	^main
 	^matching-branch
+	^other/HEAD
 	^other/branch-in-other
 	^other/main-in-other
 	^matching-tag
@@ -950,6 +960,7 @@ test_expect_success '__git refs - excluding full refs' '
 	cat >expected <<-EOF &&
 	^refs/heads/main
 	^refs/heads/matching-branch
+	^refs/remotes/other/HEAD
 	^refs/remotes/other/branch-in-other
 	^refs/remotes/other/main-in-other
 	^refs/tags/matching-tag
@@ -975,6 +986,7 @@ test_expect_success '__git_refs - do not filter refs unless told so' '
 	main
 	matching-branch
 	matching/branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	other/matching/branch-in-other
@@ -1095,6 +1107,7 @@ test_expect_success '__git_complete_refs - simple' '
 	HEAD Z
 	main Z
 	matching-branch Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	matching-tag Z
@@ -1123,6 +1136,7 @@ test_expect_success '__git_complete_refs - matching' '
 test_expect_success '__git_complete_refs - remote' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main-in-other Z
 	EOF
@@ -1139,9 +1153,11 @@ test_expect_success '__git_complete_refs - track' '
 	HEAD Z
 	main Z
 	matching-branch Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	matching-tag Z
+	HEAD Z
 	branch-in-other Z
 	main-in-other Z
 	EOF
@@ -1184,6 +1200,7 @@ test_expect_success '__git_complete_refs - suffix' '
 	HEAD.
 	main.
 	matching-branch.
+	other/HEAD.
 	other/branch-in-other.
 	other/main-in-other.
 	matching-tag.
@@ -1199,6 +1216,7 @@ test_expect_success '__git_complete_refs - suffix' '
 test_expect_success '__git_complete_fetch_refspecs - simple' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	HEAD:HEAD Z
+	HEAD:HEAD Z
 	branch-in-other:branch-in-other Z
 	main-in-other:main-in-other Z
 	EOF
@@ -1225,6 +1243,7 @@ test_expect_success '__git_complete_fetch_refspecs - matching' '
 test_expect_success '__git_complete_fetch_refspecs - prefix' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	+HEAD:HEAD Z
+	+HEAD:HEAD Z
 	+branch-in-other:branch-in-other Z
 	+main-in-other:main-in-other Z
 	EOF
@@ -1289,6 +1308,7 @@ test_expect_success '__git_complete_worktree_paths with -C' '
 
 test_expect_success 'git switch - with no options, complete local branches and unique remote branch names for DWIM logic' '
 	test_completion "git switch " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1435,11 +1455,13 @@ test_expect_success 'git-bisect - existing view subcommand is recognized and ena
 test_expect_success 'git checkout - completes refs and unique remote branches for DWIM' '
 	test_completion "git checkout " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1461,6 +1483,7 @@ test_expect_success 'git switch - with GIT_COMPLETION_CHECKOUT_NO_GUESS=1, compl
 
 test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_GUESS=1, complete local branches and unique remote names for DWIM logic' '
 	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git switch --guess " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1470,6 +1493,7 @@ test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_G
 
 test_expect_success 'git switch - a later --guess overrides previous --no-guess, complete local and remote unique branches for DWIM' '
 	test_completion "git switch --no-guess --guess " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1490,6 +1514,7 @@ test_expect_success 'git checkout - with GIT_COMPLETION_NO_GUESS=1 only complete
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1498,11 +1523,13 @@ test_expect_success 'git checkout - with GIT_COMPLETION_NO_GUESS=1 only complete
 test_expect_success 'git checkout - --guess overrides GIT_COMPLETION_NO_GUESS=1, complete refs and unique remote branches for DWIM' '
 	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git checkout --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1514,6 +1541,7 @@ test_expect_success 'git checkout - with --no-guess, only completes refs' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1522,11 +1550,13 @@ test_expect_success 'git checkout - with --no-guess, only completes refs' '
 test_expect_success 'git checkout - a later --guess overrides previous --no-guess, complete refs and unique remote branches for DWIM' '
 	test_completion "git checkout --no-guess --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1538,6 +1568,7 @@ test_expect_success 'git checkout - a later --no-guess overrides previous --gues
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1550,6 +1581,7 @@ test_expect_success 'git checkout - with checkout.guess = false, only completes
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1559,11 +1591,13 @@ test_expect_success 'git checkout - with checkout.guess = true, completes refs a
 	test_config checkout.guess true &&
 	test_completion "git checkout " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1573,11 +1607,13 @@ test_expect_success 'git checkout - a later --guess overrides previous checkout.
 	test_config checkout.guess false &&
 	test_completion "git checkout --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1590,6 +1626,7 @@ test_expect_success 'git checkout - a later --no-guess overrides previous checko
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1601,6 +1638,7 @@ test_expect_success 'git switch - with --detach, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1612,6 +1650,7 @@ test_expect_success 'git checkout - with --detach, complete only references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1783,6 +1822,7 @@ test_expect_success 'git switch - with -d, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1794,6 +1834,7 @@ test_expect_success 'git checkout - with -d, complete only references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1801,10 +1842,12 @@ test_expect_success 'git checkout - with -d, complete only references' '
 
 test_expect_success 'git switch - with --track, complete only remote branches' '
 	test_completion "git switch --track " <<-\EOF &&
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
 	test_completion "git switch -t " <<-\EOF
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1812,10 +1855,12 @@ test_expect_success 'git switch - with --track, complete only remote branches' '
 
 test_expect_success 'git checkout - with --track, complete only remote branches' '
 	test_completion "git checkout --track " <<-\EOF &&
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
 	test_completion "git checkout -t " <<-\EOF
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1834,6 +1879,7 @@ test_expect_success 'git checkout - with --no-track, complete only local referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1845,6 +1891,7 @@ test_expect_success 'git switch - with -c, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1856,6 +1903,7 @@ test_expect_success 'git switch - with -C, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1867,6 +1915,7 @@ test_expect_success 'git switch - with -c and --track, complete all references'
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1878,6 +1927,7 @@ test_expect_success 'git switch - with -C and --track, complete all references'
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1889,6 +1939,7 @@ test_expect_success 'git switch - with -c and --no-track, complete all reference
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1900,6 +1951,7 @@ test_expect_success 'git switch - with -C and --no-track, complete all reference
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1911,6 +1963,7 @@ test_expect_success 'git checkout - with -b, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1922,6 +1975,7 @@ test_expect_success 'git checkout - with -B, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1933,6 +1987,7 @@ test_expect_success 'git checkout - with -b and --track, complete all references
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1944,6 +1999,7 @@ test_expect_success 'git checkout - with -B and --track, complete all references
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1955,6 +2011,7 @@ test_expect_success 'git checkout - with -b and --no-track, complete all referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1966,6 +2023,7 @@ test_expect_success 'git checkout - with -B and --no-track, complete all referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1973,6 +2031,7 @@ test_expect_success 'git checkout - with -B and --no-track, complete all referen
 
 test_expect_success 'git switch - for -c, complete local branches and unique remote branches' '
 	test_completion "git switch -c " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1982,6 +2041,7 @@ test_expect_success 'git switch - for -c, complete local branches and unique rem
 
 test_expect_success 'git switch - for -C, complete local branches and unique remote branches' '
 	test_completion "git switch -C " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2019,6 +2079,7 @@ test_expect_success 'git switch - for -C with --no-track, complete local branche
 
 test_expect_success 'git checkout - for -b, complete local branches and unique remote branches' '
 	test_completion "git checkout -b " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2028,6 +2089,7 @@ test_expect_success 'git checkout - for -b, complete local branches and unique r
 
 test_expect_success 'git checkout - for -B, complete local branches and unique remote branches' '
 	test_completion "git checkout -B " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2065,6 +2127,7 @@ test_expect_success 'git checkout - for -B with --no-track, complete local branc
 
 test_expect_success 'git switch - with --orphan completes local branch names and unique remote branch names' '
 	test_completion "git switch --orphan " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2080,6 +2143,7 @@ test_expect_success 'git switch - --orphan with branch already provided complete
 
 test_expect_success 'git checkout - with --orphan completes local branch names and unique remote branch names' '
 	test_completion "git checkout --orphan " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2093,6 +2157,7 @@ test_expect_success 'git checkout - --orphan with branch already provided comple
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
-- 
2.47.0.296.gda1ecfef29.dirty
Junio C Hamano· Nov 19, 2024, 03:16 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v13 8/9] fetch: set remote/HEAD if it does not exist

Bence Ferdinandy <bence@ferdinandy.com> writes:
> If the user has remote/HEAD set already and it looks like it has changed
> on the server, then print a message, otherwise set it if we can.

The user uses remote/origin/HEAD to point at the "primary thing the user is interested in at the remote 'origin'". "git clone" propagates remote's HEAD to local because their HEAD is the opinion of the remote which branch they want the users to consider the primary thing. But the user can have valid reasons to consider a branch different from what the remote suggests as the primary thing, and an explicit "set-head" is a way to do so.

After such a set-up is made and the user is perfectly happy, would it make sense to repeatedly remind that their HEAD points at something the user is not interested in?

Perhaps it may make sense when given the "--verbose" option, or after the first time the difference was noticed, but otherwise, I suspect it becomes annoying to those who keep them deliberately different from each other.

I wonder if it is more sensible to automate for these two different classes of users with a new configuration per remote.

 - When "git clone" initially sets up, or "git remote set-head
   --auto" matches the local to the remote, we set the new
   "remote.$name.autoUpdateHEAD" configuration variable to true.
 - When "git remote set-head" explicitly sets remote/$name/HEAD
   to some value (if we can detect the case where it is different
   from what the remote has, that would be a plus, but it incurrs
   network traffic, so care must be taken to design this part), we
   drop the "remote.$name.autoUpdateHEAD" configuration variable,
   if and only if it is set to true.
 - When remote.$name.autoUpdateHEAD configuration is true, "git
   fetch" automatically matches remote/$name/HEAD to what the HEAD
   at the remote points at.  If remote.$name.autoUpdateHEAD is set
   to some other value (e.g., "warn"), show the warning so that the
   user can choose to do "git remote set-head" to a specific value
   (which would not drop the remote.$name.autoUpdateHEAD set to
   "warn") or "git remote set-head --auto" (which would start to
   follow whatever remote uses).
or something like that?

With something like the above, those who just want to follow along would get remote.$name.autoUpdateHEAD=true and silently follow along, while those who want to explicitly control would be able to do so. I dunno, but I am fairly negative on a persistent warning message that tells the user that the only normal case is to have remote/origin/HEAD point at the same branch the remote's HEAD points at.

Thanks.
Bence Ferdinandy· Nov 19, 2024, 11:27 UTC · re: Junio C Hamano · lore

Re: [PATCH v13 8/9] fetch: set remote/HEAD if it does not exist

On Tue Nov 19, 2024 at 04:16, Junio C Hamano <gitster@pobox.com> wrote:
Show 24 quoted lines
> Bence Ferdinandy <bence@ferdinandy.com> writes:
>
>> If the user has remote/HEAD set already and it looks like it has changed
>> on the server, then print a message, otherwise set it if we can.
>
> The user uses remote/origin/HEAD to point at the "primary thing the
> user is interested in at the remote 'origin'".  "git clone"
> propagates remote's HEAD to local because their HEAD is the opinion
> of the remote which branch they want the users to consider the
> primary thing.  But the user can have valid reasons to consider a
> branch different from what the remote suggests as the primary thing,
> and an explicit "set-head" is a way to do so.
>
> After such a set-up is made and the user is perfectly happy, would
> it make sense to repeatedly remind that their HEAD points at
> something the user is not interested in?
>
> Perhaps it may make sense when given the "--verbose" option, or
> after the first time the difference was noticed, but otherwise, I
> suspect it becomes annoying to those who keep them deliberately
> different from each other.
>
> I wonder if it is more sensible to automate for these two different
> classes of users with a new configuration per remote.

Since this discussion reaches a bit back I looked up the first message where the current behaviour was discussed, and where in the end it was said we don't need configuration (the very first implementation always updated HEAD). Starting here:

https://lore.kernel.org/git/xmqqseu7cfsl.fsf@gitster.g/
That being said:

Your suggestion seems to be the original tri-state configuration that came up in one of the original discussions. It was recently requested again for fetch to just do this automatically (https://lore.kernel.org/git/CAAHKNRGv19rhnqCkJMpE2FomNQBHvSS36aC=fh0UwO+9-6RRfA@mail.gmail.com), so at least some people would like to have this fully automated.

>
>  - When "git clone" initially sets up, or "git remote set-head
>    --auto" matches the local to the remote, we set the new
>    "remote.$name.autoUpdateHEAD" configuration variable to true.

I think this is a good default. -> Coming back here I think "warn" is a better default see below.

Show 7 quoted lines
>
>  - When "git remote set-head" explicitly sets remote/$name/HEAD
>    to some value (if we can detect the case where it is different
>    from what the remote has, that would be a plus, but it incurrs
>    network traffic, so care must be taken to design this part), we
>    drop the "remote.$name.autoUpdateHEAD" configuration variable,
>    if and only if it is set to true.

I also agree with this. I don't think it would be necessary to do a network check at this point. If somebody is deliberately changing the remote head manually, they are probably aware of the current situation and can always set their configuration to "warn" if they care, or just check manually once in a while. At least I feel needing network connection to do this would be more a hindrance than useful.

Show 9 quoted lines
>
>  - When remote.$name.autoUpdateHEAD configuration is true, "git
>    fetch" automatically matches remote/$name/HEAD to what the HEAD
>    at the remote points at.  If remote.$name.autoUpdateHEAD is set
>    to some other value (e.g., "warn"), show the warning so that the
>    user can choose to do "git remote set-head" to a specific value
>    (which would not drop the remote.$name.autoUpdateHEAD set to
>    "warn") or "git remote set-head --auto" (which would start to
>    follow whatever remote uses).
Make sense. 

One thing you haven't mentioned is when you start locally with git init && git remote add && git fetch. But since this whole series started by wanting to make this the same as cloning I assume it should be "true" as well. In fact, if the default behaviour is true this would just work.

On the other hand: considering your points in the linked very early discussion, maybe "warn" would be a better default? I'm not sure. Most people would not see warnings anyway since I doubt a) many users change their remote/HEAD manually or b) many projects change their HEAD at all or if they do, more than once. Gaving "true" as default would change the current behaviour with silently updating existing remote/HEADs. The warning could also tell them that `remote set-head --auto` will set this to automatic in the future as well and thus remove any warnings or that if they know what they want the can set it to false.

Show 8 quoted lines
>
> With something like the above, those who just want to follow along
> would get remote.$name.autoUpdateHEAD=true and silently follow
> along, while those who want to explicitly control would be able to
> do so.  I dunno, but I am fairly negative on a persistent warning
> message that tells the user that the only normal case is to have
> remote/origin/HEAD point at the same branch the remote's HEAD points
> at.
Fair point.
This series has a tendency for growing in complication in each iteration :)

Best, Bence

Junio C Hamano· Nov 20, 2024, 01:00 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v13 8/9] fetch: set remote/HEAD if it does not exist

"Bence Ferdinandy" <bence@ferdinandy.com> writes:
Show 5 quoted lines
> Your suggestion seems to be the original tri-state configuration that came up
> in one of the original discussions. It was recently requested again for fetch
> to just do this automatically
> (https://lore.kernel.org/git/CAAHKNRGv19rhnqCkJMpE2FomNQBHvSS36aC=fh0UwO+9-6RRfA@mail.gmail.com),
> so at least some people would like to have this fully automated.

And that is *not* the only option, and I am trying to help others by preventing unconditional warning messages from annoying them.

> On the other hand: considering your points in the linked very early discussion,
> maybe "warn" would be a better default?

Those who want to stick to their manual choice would have to do an explicit set-head once, whether the default is "true" or "warn", so their inconvenience would not affect the choice of the default.

I think "warn" is a good default _only_ for folks who want to verify and then expect they will follow what the upstream does 99% of the time. For them, when they do not want to switch to what the upstream says (which is the rest 1%), there is no good way to squelch the warning other than dropping the configuration, but then they will lose the ability to notice further flips at the remote after that, which may turn out to be a problem.

Instead of "true", "warn", and "false", we might need "if it is set to _this_ value, do not warn and keep the current setting" to help those who cautiously only allow the remote affect what they use after inspecting, but expect that the choice made by the remote will not be outrageous most of the time for them.

If we want to solve this completely for them, that is.  I dunno.

Those who want to blindly follow what the upstream says 100% of the time will have to switch to "true" just once manually, if the default were "warn", but once may be one time too many for them.

And that is why I said "clone" (and "fetch" setting a non-existing HEAD automatically) may want to default to "true".

> Fair point.
>
> This series has a tendency for growing in complication in each iteration :)

Anytime somebody gets swayed by an opinion to add unconditional warning or unconditional futzing without considering that such a move may annoy some users that are different from the opinion giver, we need to make sure there are escape hatches. Under the assumption that most projects only flip HEAD very rarely, and considering the fact that we have lived with manual setting with set-head and nothing else, we could drop the automatic setting by "fetch" altogether to reduce the scope of the series without losing much *and* more importantly without harming existing users.

I think we could make it a multi-step transition in order to keep changes to "git fetch" we make in each step smaller, which may go like so:

 1) Let "git fetch" create missing remotes/$name/HEAD.  No other
    changes.
 2) Introduce a remote.$name.followRemoteHEAD that can say "the last
    branch we observed that they point at is 'main'".  Set it when
    we clone, When "git fetch" notices that the remote flipped its
    HEAD to a different value, warn and update (which would give us
    the "warn only once, do not annoy users by constantly warning").
    The user can delete the configuration to squelch "git fetch" (in
    other words, the repository does not remember the last setting,
    so we refrain from warning "it has changed since the last time").
    No other changes.
 3) Allow remote.$name.followRemoteHEAD to be set to
    3-a) "always", which would squelch the warning and causes "git
    fetch" to always repoint the local remotes/$name/HEAD to
    whatever the remote says,
    3-b) "warn", which would cause "git fetch" to warn when the
    remote's HEAD is different from local remotes/$name/HEAD,
    3-c) "never", which would cause "git fetch" to even omit the
    "copy if it does not exist" we add in 1) to help those who
    deliberately want to keep remotes/$name/HEAD missing (to force
    themselves to be more explicit, saying things like "git checkout
    -b topic origin/maint" and "git rebase origin/master", but never
    "git checkout --detach origin").

And we can stop this round at the first step (plus any other changes outside "git fetch", like "set-head --auto").

Thanks.
Junio C Hamano· Nov 20, 2024, 02:28 UTC · re: Junio C Hamano · lore

Re: [PATCH v13 8/9] fetch: set remote/HEAD if it does not exist

Junio C Hamano <gitster@pobox.com> writes:
Show 10 quoted lines
> "Bence Ferdinandy" <bence@ferdinandy.com> writes:
>
>> Your suggestion seems to be the original tri-state configuration that came up
>> in one of the original discussions. It was recently requested again for fetch
>> to just do this automatically
>> (https://lore.kernel.org/git/CAAHKNRGv19rhnqCkJMpE2FomNQBHvSS36aC=fh0UwO+9-6RRfA@mail.gmail.com),
>> so at least some people would like to have this fully automated.
>
> And that is *not* the only option, and I am trying to help others
> by preventing unconditional warning messages from annoying them.

By the way, because I've left it out from my messages, some folks who are reading from sideways might not realize this, so let's make it a bit more explicit. One thing to note is that the assumption that HEAD at the remote rarely changes _only_ holds for forges and the like, a typically bare repository used for publishing without a working tree.

If you have a clone from a repository with a working tree (this happens when you have two places to work, either on two machines or two separate repositories on a same machine) to allow you to work here during the day and work there after work, the HEAD at the clone source may change to the branch you happen to be working on when left there. In such a setting, it is reasonable to make the repositories aware of each other (i.e. the after-work repository may have refs/remotes/at-work/* remote-tracking branches while the at-work repository keeps refs/remotes/after-work/* remote-tracking branches, to keep track of each other), and "git fetch" that warns every time it notices the remote end has HEAD pointing at a different branch would be annoying, as they would likely be working on different "project" when they leave after work.

Users who fall outside of a typical "we have a central repository and the only interaction with the repository is we clone and fetch/pull from it and nothing else" hosting site setting, are why I'll encourage folks to tread carefully when they are tempted to make anything unconditional.

Thanks.
Bence Ferdinandy· Nov 20, 2024, 10:45 UTC · re: Junio C Hamano · lore

Re: [PATCH v13 8/9] fetch: set remote/HEAD if it does not exist

On Wed Nov 20, 2024 at 02:00, Junio C Hamano <gitster@pobox.com> wrote:
Show 33 quoted lines
> "Bence Ferdinandy" <bence@ferdinandy.com> writes:
>
>> Your suggestion seems to be the original tri-state configuration that came up
>> in one of the original discussions. It was recently requested again for fetch
>> to just do this automatically
>> (https://lore.kernel.org/git/CAAHKNRGv19rhnqCkJMpE2FomNQBHvSS36aC=fh0UwO+9-6RRfA@mail.gmail.com),
>> so at least some people would like to have this fully automated.
>
> And that is *not* the only option, and I am trying to help others
> by preventing unconditional warning messages from annoying them.
>
>> On the other hand: considering your points in the linked very early discussion,
>> maybe "warn" would be a better default?
>
> Those who want to stick to their manual choice would have to do an
> explicit set-head once, whether the default is "true" or "warn", so
> their inconvenience would not affect the choice of the default.
>
> I think "warn" is a good default _only_ for folks who want to verify
> and then expect they will follow what the upstream does 99% of the
> time.  For them, when they do not want to switch to what the
> upstream says (which is the rest 1%), there is no good way to
> squelch the warning other than dropping the configuration, but then
> they will lose the ability to notice further flips at the remote
> after that, which may turn out to be a problem.
>
> Instead of "true", "warn", and "false", we might need "if it is set
> to _this_ value, do not warn and keep the current setting" to help
> those who cautiously only allow the remote affect what they use
> after inspecting, but expect that the choice made by the remote will
> not be outrageous most of the time for them.
>
> If we want to solve this completely for them, that is.  I dunno.

I initially thought you meant a fourth setting here, but what you are suggesting here is that in case the user has "remote.origin.followremotehead = never" set even in case of "init && remote add && fetch" HEAD should not be created, which is a bit different, because originally we discussed only not changing after the initial creation. This makes a lot of sense.

Show 7 quoted lines
>
> Those who want to blindly follow what the upstream says 100% of the
> time will have to switch to "true" just once manually, if the
> default were "warn", but once may be one time too many for them.
>
> And that is why I said "clone" (and "fetch" setting a non-existing
> HEAD automatically) may want to default to "true".
Fair point again.
Show 14 quoted lines
>
>> Fair point.
>>
>> This series has a tendency for growing in complication in each iteration :)
>
> Anytime somebody gets swayed by an opinion to add unconditional
> warning or unconditional futzing without considering that such a
> move may annoy some users that are different from the opinion giver,
> we need to make sure there are escape hatches.  Under the assumption
> that most projects only flip HEAD very rarely, and considering the
> fact that we have lived with manual setting with set-head and
> nothing else, we could drop the automatic setting by "fetch"
> altogether to reduce the scope of the series without losing much
> *and* more importantly without harming existing users.

Don't get me wrong, I'd like to do the right thing here, and appreciate that my perspective is rather limited. I was just reflecting on the fact that the first "working" version of the series was only two patches. Writing this is fun and educational for me.

>
> I think we could make it a multi-step transition in order to keep
> changes to "git fetch" we make in each step smaller, which may go
> like so:

That being said, if it's possible to graduate a part of the series when it's ready and where it makes sense to split, that would be welcome. As you write below the first such place would be remote set-head --auto. Not that that is ready yet, I need to fix the mistake you spotted, update a commit message, and add a test-coverage. But in that case I might send a v14 with just these updates and work on getting the fetch settings in parallel for a v15. Technically I would just need to build the series off next for a while, right?

>
>  1) Let "git fetch" create missing remotes/$name/HEAD.  No other
>     changes.

So this is a bit simpler version of what we have now (as long as it doesn't bother for example you, that there is a state which will create HEAD without being able to disable it :)) Should this even print anything? Probably not, clone also doesn't tell you that the HEAD was set.

Show 16 quoted lines
>
>  2) Introduce a remote.$name.followRemoteHEAD that can say "the last
>     branch we observed that they point at is 'main'".  Set it when
>     we clone, When "git fetch" notices that the remote flipped its
>     HEAD to a different value, warn and update (which would give us
>     the "warn only once, do not annoy users by constantly warning").
>     The user can delete the configuration to squelch "git fetch" (in
>     other words, the repository does not remember the last setting,
>     so we refrain from warning "it has changed since the last time").
>     No other changes.
>
>  3) Allow remote.$name.followRemoteHEAD to be set to
>
>     3-a) "always", which would squelch the warning and causes "git
>     fetch" to always repoint the local remotes/$name/HEAD to
>     whatever the remote says,

But this should print similar messages as remote set-head --auto when there is an actual change done, right? And should probably respect --quiet?

>
>     3-b) "warn", which would cause "git fetch" to warn when the
>     remote's HEAD is different from local remotes/$name/HEAD,
This should also respect --quite I assume.
Show 7 quoted lines
>
>     3-c) "never", which would cause "git fetch" to even omit the
>     "copy if it does not exist" we add in 1) to help those who
>     deliberately want to keep remotes/$name/HEAD missing (to force
>     themselves to be more explicit, saying things like "git checkout
>     -b topic origin/maint" and "git rebase origin/master", but never
>     "git checkout --detach origin").

I think skipping 2 and jumping straight to the final configuration would be easier, no need to wrap one's head around a configuration state that will essentially never really exist and most of the extra complexity (at least from my perspective) comes from having to have a configuration at all.

Thanks, Bence

Bence Ferdinandy· Nov 21, 2024, 22:55 UTC · re: Bence Ferdinandy · lore

[PATCH v14 00/10] set-head/fetch remote/HEAD updates

Notable changes since v13:
- As discussed in
  https://lore.kernel.org/git/xmqqed36btxm.fsf@gitster.g/ this iteration
  has dropped printing any warning during fetch and now only sets HEAD
  if it doesn't already exist, otherwise it doesn't do anything.
- A new patch adds a new test case for testing the failure branch of
  remote set-head. This also allowed to clear up a bug in a later patch.

I will start working on a follow up patch that will add configuration options to control how fetch sets HEAD or not. The current state should be fine for most people, except those who explicitly wish NOT to have remote/HEAD set. Other than that the current state is now hopefully mature enough to graduate (unless I made yet another mistake :)).

Bence Ferdinandy (10):
  t/t5505-remote: set default branch to main
  t/t5505-remote: test failure of set-head
  refs: standardize output of refs_read_symbolic_ref
  refs: atomically record overwritten ref in update_symref
  remote set-head: refactor for readability
  remote set-head: better output for --auto
  refs: add TRANSACTION_CREATE_EXISTS error
  refs: add create_only option to refs_update_symref_extended
  fetch: set remote/HEAD if it does not exist
  fetch set_head: handle mirrored bare repositories
 builtin/fetch.c                  |  74 +++++++++++++++++++++
 builtin/remote.c                 |  72 +++++++++++++++------
 refs.c                           |  47 ++++++++++++--
 refs.h                           |  19 +++++-
 refs/files-backend.c             |  31 +++++----
 refs/refs-internal.h             |   5 ++
 refs/reftable-backend.c          |  14 ++--
 t/t4207-log-decoration-colors.sh |   3 +-
 t/t5505-remote.sh                | 107 +++++++++++++++++++++++++++++--
 t/t5510-fetch.sh                 |  24 +++++++
 t/t5512-ls-remote.sh             |   2 +
 t/t5514-fetch-multiple.sh        |  17 ++++-
 t/t5516-fetch-push.sh            |   3 +-
 t/t5527-fetch-odd-refs.sh        |   3 +-
 t/t7900-maintenance.sh           |   3 +-
 t/t9210-scalar.sh                |   5 +-
 t/t9211-scalar-clone.sh          |   6 +-
 t/t9902-completion.sh            |  65 +++++++++++++++++++
 18 files changed, 440 insertions(+), 60 deletions(-)
-- 
2.47.0.298.g52a96ec17b
Bence Ferdinandy· Nov 21, 2024, 22:55 UTC · re: Bence Ferdinandy · lore

[PATCH v14 01/10] t/t5505-remote: set default branch to main

Consider the bare repository called "mirror" in the test. Running `git remote add --mirror -f origin ../one` will not change HEAD, consequently if init.defaultBranch is not the same as what HEAD in the remote ("one"), HEAD in "mirror" will be pointing to a non-existent reference. Hence if "mirror" is used as a remote by yet another repository, ls-remote will not show HEAD. On the other hand, if init.defaultBranch happens to match HEAD in "one", then ls-remote will show HEAD.

Since the "ci/run-build-and-tests.sh" script globally exports GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main for some (but not all) jobs, there may be a drift in some tests between how the test repositories are set up in the CI and during local testing, if the test itself uses "master" as default instead of "main". In particular, this happens in t5505-remote.sh. This issue does not manifest currently, as the test does not do any remote HEAD manipulation where this would come up, but should such things be added, a locally passing test would break the CI and vice-versa.

Set GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main in t5505-remote to be consistent with the CI.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v9: - new patch
        - a bandaid for the CI issue noticed by Taylor (cf:
          https://lore.kernel.org/git/Zw8IKyPkG0Hr6%2F5t@nand.local/), but
          see
          https://lore.kernel.org/git/D4ZAELFWJMKN.S88LJ6YK31LZ@ferdinandy.com/ for the root cause in detail
    
    v10: no change
    
    v11: no change
    
    v12: added forgotten sign-off
    
    v13: commit message udpated to be more precise
    
    v14: made explicit in the commit message that the default branch is only
         set for some CI jobs
 t/t5505-remote.sh | 3 +++
 1 file changed, 3 insertions(+)
Show changes to t/t5505-remote.sh +3 −0
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 532035933f..9b50276646 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -2,6 +2,9 @@
 
 test_description='git remote porcelain-ish'
 
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
 TEST_PASSES_SANITIZE_LEAK=true
 . ./test-lib.sh
 
-- 
2.47.0.298.g52a96ec17b
Bence Ferdinandy· Nov 21, 2024, 22:55 UTC · re: Bence Ferdinandy · lore

[PATCH v14 03/10] refs: standardize output of refs_read_symbolic_ref

When the symbolic reference we want to read with refs_read_symbolic_ref is actually not a symbolic reference, the files and the reftable backends return different values (1 and -1 respectively). Standardize the returned values so that 0 is success, -1 is a generic error and -2 is that the reference was actually non-symbolic.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v13: new patch
    
    v14: - simplified the logic in reftables backend (thanks Junio)
         - update the comment in refs.h (thanks Patrick)
         - rewrote comment in refs-internal.h to point to the one in refs.h
         - created NOT_A_SYMREF=-2 constant
 refs.h                  | 11 +++++++++++
 refs/files-backend.c    |  7 +++----
 refs/refs-internal.h    |  5 +++++
 refs/reftable-backend.c |  8 +++++---
 4 files changed, 24 insertions(+), 7 deletions(-)
Show changes to 4 files +24 −7

refs.h, refs/files-backend.c, refs/refs-internal.h, refs/reftable-backend.c

diff --git a/refs.h b/refs.h
index 108dfc93b3..22c2b45b8b 100644
--- a/refs.h
+++ b/refs.h
@@ -83,6 +83,17 @@ int refs_read_ref_full(struct ref_store *refs, const char *refname,
 
 int refs_read_ref(struct ref_store *refs, const char *refname, struct object_id *oid);
 
+#define NOT_A_SYMREF -2
+
+/*
+ * Read the symbolic ref named "refname" and write its immediate referent into
+ * the provided buffer. Referent is left empty if "refname" is not a symbolic
+ * ref. It does not resolve the symbolic reference recursively in case the
+ * target is also a symbolic ref.
+ *
+ * Returns 0 on success, -2 if the "refname" is not a symbolic ref,
+ * -1 otherwise.
+ */
 int refs_read_symbolic_ref(struct ref_store *ref_store, const char *refname,
 			   struct strbuf *referent);
 
diff --git a/refs/files-backend.c b/refs/files-backend.c
index 0824c0b8a9..4cc43c32f2 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -596,10 +596,9 @@ static int files_read_symbolic_ref(struct ref_store *ref_store, const char *refn
 	unsigned int type;
 
 	ret = read_ref_internal(ref_store, refname, &oid, referent, &type, &failure_errno, 1);
-	if (ret)
-		return ret;
-
-	return !(type & REF_ISSYMREF);
+	if (!ret && !(type & REF_ISSYMREF))
+		return NOT_A_SYMREF;
+	return ret;
 }
 
 int parse_loose_ref_contents(const struct git_hash_algo *algop,
diff --git a/refs/refs-internal.h b/refs/refs-internal.h
index 2313c830d8..1399fee61c 100644
--- a/refs/refs-internal.h
+++ b/refs/refs-internal.h
@@ -673,6 +673,11 @@ struct ref_storage_be {
 
 	ref_iterator_begin_fn *iterator_begin;
 	read_raw_ref_fn *read_raw_ref;
+
+	/*
+	 * Please refer to `refs_read_symbolic_ref()` for the expected
+	 * behaviour.
+	 */
 	read_symbolic_ref_fn *read_symbolic_ref;
 
 	reflog_iterator_begin_fn *reflog_iterator_begin;
diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
index 38eb14d591..1809e3426a 100644
--- a/refs/reftable-backend.c
+++ b/refs/reftable-backend.c
@@ -830,10 +830,12 @@ static int reftable_be_read_symbolic_ref(struct ref_store *ref_store,
 		return ret;
 
 	ret = reftable_stack_read_ref(stack, refname, &ref);
-	if (ret == 0 && ref.value_type == REFTABLE_REF_SYMREF)
+        if (ret)
+                ret = -1;
+        else if (ref.value_type == REFTABLE_REF_SYMREF)
 		strbuf_addstr(referent, ref.value.symref);
-	else
-		ret = -1;
+        else
+                ret = NOT_A_SYMREF;
 
 	reftable_ref_record_release(&ref);
 	return ret;
-- 
2.47.0.298.g52a96ec17b
karthik nayak· Nov 22, 2024, 10:37 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v14 03/10] refs: standardize output of refs_read_symbolic_ref

Bence Ferdinandy <bence@ferdinandy.com> writes:
[snip]
Show 18 quoted lines
> diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
> index 38eb14d591..1809e3426a 100644
> --- a/refs/reftable-backend.c
> +++ b/refs/reftable-backend.c
> @@ -830,10 +830,12 @@ static int reftable_be_read_symbolic_ref(struct ref_store *ref_store,
>  		return ret;
>
>  	ret = reftable_stack_read_ref(stack, refname, &ref);
> -	if (ret == 0 && ref.value_type == REFTABLE_REF_SYMREF)
> +        if (ret)
> +                ret = -1;
> +        else if (ref.value_type == REFTABLE_REF_SYMREF)
>  		strbuf_addstr(referent, ref.value.symref);
> -	else
> -		ret = -1;
> +        else
> +                ret = NOT_A_SYMREF;
>

I was building my series on top of this, and noticed whitespace issues here. A simple way to check your series is to run:

  $ git log --check --pretty=format:"---% h% s"
>  	reftable_ref_record_release(&ref);
>  	return ret;
> --
> 2.47.0.298.g52a96ec17b
Bence Ferdinandy· Nov 22, 2024, 10:53 UTC · re: karthik nayak · lore

Re: [PATCH v14 03/10] refs: standardize output of refs_read_symbolic_ref

On Fri Nov 22, 2024 at 11:37, karthik nayak <karthik.188@gmail.com> wrote:
Show 27 quoted lines
> Bence Ferdinandy <bence@ferdinandy.com> writes:
>
> [snip]
>
>> diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
>> index 38eb14d591..1809e3426a 100644
>> --- a/refs/reftable-backend.c
>> +++ b/refs/reftable-backend.c
>> @@ -830,10 +830,12 @@ static int reftable_be_read_symbolic_ref(struct ref_store *ref_store,
>>  		return ret;
>>
>>  	ret = reftable_stack_read_ref(stack, refname, &ref);
>> -	if (ret == 0 && ref.value_type == REFTABLE_REF_SYMREF)
>> +        if (ret)
>> +                ret = -1;
>> +        else if (ref.value_type == REFTABLE_REF_SYMREF)
>>  		strbuf_addstr(referent, ref.value.symref);
>> -	else
>> -		ret = -1;
>> +        else
>> +                ret = NOT_A_SYMREF;
>>
>
> I was building my series on top of this, and noticed whitespace issues
> here. A simple way to check your series is to run:
>
>   $ git log --check --pretty=format:"---% h% s"

I ran this on v15 and it didn't produce any output. I read what --check is in the manpages, although the format is a bit cryptic for me. What does that do exactly?

Anyhow if there was no output for v15 I should be fine, right?
Show 5 quoted lines
>
>>  	reftable_ref_record_release(&ref);
>>  	return ret;
>> --
>> 2.47.0.298.g52a96ec17b
-- 
bence.ferdinandy.com
Bence Ferdinandy· Nov 22, 2024, 10:55 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v14 03/10] refs: standardize output of refs_read_symbolic_ref

On Fri Nov 22, 2024 at 11:53, Bence Ferdinandy <bence@ferdinandy.com> wrote:
Show 35 quoted lines
>
> On Fri Nov 22, 2024 at 11:37, karthik nayak <karthik.188@gmail.com> wrote:
>> Bence Ferdinandy <bence@ferdinandy.com> writes:
>>
>> [snip]
>>
>>> diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
>>> index 38eb14d591..1809e3426a 100644
>>> --- a/refs/reftable-backend.c
>>> +++ b/refs/reftable-backend.c
>>> @@ -830,10 +830,12 @@ static int reftable_be_read_symbolic_ref(struct ref_store *ref_store,
>>>  		return ret;
>>>
>>>  	ret = reftable_stack_read_ref(stack, refname, &ref);
>>> -	if (ret == 0 && ref.value_type == REFTABLE_REF_SYMREF)
>>> +        if (ret)
>>> +                ret = -1;
>>> +        else if (ref.value_type == REFTABLE_REF_SYMREF)
>>>  		strbuf_addstr(referent, ref.value.symref);
>>> -	else
>>> -		ret = -1;
>>> +        else
>>> +                ret = NOT_A_SYMREF;
>>>
>>
>> I was building my series on top of this, and noticed whitespace issues
>> here. A simple way to check your series is to run:
>>
>>   $ git log --check --pretty=format:"---% h% s"
>
> I ran this on v15 and it didn't produce any output. I read what --check is in
> the manpages, although the format is a bit cryptic for me. What does that do
> exactly? 
>
> Anyhow if there was no output for v15 I should be fine, right?
But then again: this patch is exactly like v14 so I'm not so sure about that ...
-- 
bence.ferdinandy.com
karthik nayak· Nov 22, 2024, 11:30 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v14 03/10] refs: standardize output of refs_read_symbolic_ref

"Bence Ferdinandy" <bence@ferdinandy.com> writes:
Show 30 quoted lines
> On Fri Nov 22, 2024 at 11:37, karthik nayak <karthik.188@gmail.com> wrote:
>> Bence Ferdinandy <bence@ferdinandy.com> writes:
>>
>> [snip]
>>
>>> diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
>>> index 38eb14d591..1809e3426a 100644
>>> --- a/refs/reftable-backend.c
>>> +++ b/refs/reftable-backend.c
>>> @@ -830,10 +830,12 @@ static int reftable_be_read_symbolic_ref(struct ref_store *ref_store,
>>>  		return ret;
>>>
>>>  	ret = reftable_stack_read_ref(stack, refname, &ref);
>>> -	if (ret == 0 && ref.value_type == REFTABLE_REF_SYMREF)
>>> +        if (ret)
>>> +                ret = -1;
>>> +        else if (ref.value_type == REFTABLE_REF_SYMREF)
>>>  		strbuf_addstr(referent, ref.value.symref);
>>> -	else
>>> -		ret = -1;
>>> +        else
>>> +                ret = NOT_A_SYMREF;
>>>
>>
>> I was building my series on top of this, and noticed whitespace issues
>> here. A simple way to check your series is to run:
>>
>>   $ git log --check --pretty=format:"---% h% s"
>
> I ran this on v15 and it didn't produce any output.
Did you already post v15? I only see v14
> I read what --check is in
> the manpages, although the format is a bit cryptic for me. What does that do
> exactly?
>

It ensures that commits don't have conflict markers and that there are no trailing whitespaces and spaces followed by tabs by default.

Also this is included in the CI checks (see ci/check-whitespace.sh), so if you use either GitLab or GitHub you should see these shown as errors on the CI. You'll have to raise a MR/PR to trigger the CI I believe.

On a sidenote, do you think we should modify the manpage? I found it comprehensible, but would be nice to clarify anything cryptic.

> Anyhow if there was no output for v15 I should be fine, right?
>

At the least you should see `git log`'s output, but if there are issues they should be shown inline. So when you say 'no output' do you mean you see absolutely no output?

Show 11 quoted lines
>>
>>>  	reftable_ref_record_release(&ref);
>>>  	return ret;
>>> --
>>> 2.47.0.298.g52a96ec17b
>
>
>
>
> --
> bence.ferdinandy.com
Bence Ferdinandy· Nov 22, 2024, 12:23 UTC · re: karthik nayak · lore

Re: [PATCH v14 03/10] refs: standardize output of refs_read_symbolic_ref

On Fri Nov 22, 2024 at 12:30, karthik nayak <karthik.188@gmail.com> wrote:
Show 34 quoted lines
> "Bence Ferdinandy" <bence@ferdinandy.com> writes:
>
>> On Fri Nov 22, 2024 at 11:37, karthik nayak <karthik.188@gmail.com> wrote:
>>> Bence Ferdinandy <bence@ferdinandy.com> writes:
>>>
>>> [snip]
>>>
>>>> diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
>>>> index 38eb14d591..1809e3426a 100644
>>>> --- a/refs/reftable-backend.c
>>>> +++ b/refs/reftable-backend.c
>>>> @@ -830,10 +830,12 @@ static int reftable_be_read_symbolic_ref(struct ref_store *ref_store,
>>>>  		return ret;
>>>>
>>>>  	ret = reftable_stack_read_ref(stack, refname, &ref);
>>>> -	if (ret == 0 && ref.value_type == REFTABLE_REF_SYMREF)
>>>> +        if (ret)
>>>> +                ret = -1;
>>>> +        else if (ref.value_type == REFTABLE_REF_SYMREF)
>>>>  		strbuf_addstr(referent, ref.value.symref);
>>>> -	else
>>>> -		ret = -1;
>>>> +        else
>>>> +                ret = NOT_A_SYMREF;
>>>>
>>>
>>> I was building my series on top of this, and noticed whitespace issues
>>> here. A simple way to check your series is to run:
>>>
>>>   $ git log --check --pretty=format:"---% h% s"
>>
>> I ran this on v15 and it didn't produce any output.
>
> Did you already post v15? I only see v14
Not yet, but I'll be sending it in a pinch.
Show 12 quoted lines
>
>> I read what --check is in
>> the manpages, although the format is a bit cryptic for me. What does that do
>> exactly?
>>
>
> It ensures that commits don't have conflict markers and that there are
> no trailing whitespaces and spaces followed by tabs by default.
>
> Also this is included in the CI checks (see ci/check-whitespace.sh), so
> if you use either GitLab or GitHub you should see these shown as errors
> on the CI. You'll have to raise a MR/PR to trigger the CI I believe.

I've been running the CI by pushing to a fork since Taylor first caught an error I didn't see locally and it never flagged. Now that you mention it, I'll also add log --check to my CI-s.

>
> On a sidenote, do you think we should modify the manpage? I found it
> comprehensible, but would be nice to clarify anything cryptic.

No, --check was quite clear, the `--pretty=format:"---% h% s"` part was what was cryptic.

Show 7 quoted lines
>
>> Anyhow if there was no output for v15 I should be fine, right?
>>
>
> At the least you should see `git log`'s output, but if there are issues
> they should be shown inline. So when you say 'no output' do you mean you
> see absolutely no output?
Absolutely no output:
	https://asciinema.org/a/lsqp4e1bNst6cFWw9M2jX1IqC

But I figured out why: the whitespace and the tabs were not mixed on the line, just across lines. As I read it, that is not an error to have tabs on one line and spaces on the next.

Anyhow that should be now cleared up, thanks. Gotta say, I was expecting to learn about internals doing this, but I also ended up picking up a couple of usage things as well, like --range-diff for format patch and such.

Thanks, Bence

Junio C Hamano· Nov 25, 2024, 02:56 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v14 03/10] refs: standardize output of refs_read_symbolic_ref

"Bence Ferdinandy" <bence@ferdinandy.com> writes:
Show 10 quoted lines
>> At the least you should see `git log`'s output, but if there are issues
>> they should be shown inline. So when you say 'no output' do you mean you
>> see absolutely no output?
>
> Absolutely no output:
> 	https://asciinema.org/a/lsqp4e1bNst6cFWw9M2jX1IqC
>
> But I figured out why: the whitespace and the tabs were not mixed on the line,
> just across lines. As I read it, that is not an error to have tabs on one line
> and spaces on the next.
Our .gitattribute starts like so:
    * whitespace=!indent,trail,space
    *.[ch] whitespace=indent,trail,space diff=cpp

so, unless otherwise specified, we frown upon trailing whitespace and space before tab and indenting with non tab is permitted, but C source and header files have further care about "indent" (short for "indent-with-non-tab".

So mixed or not, if you indented with spaces and not tabs, that would be noticed.

> Anyhow that should be now cleared up, thanks. Gotta say, I was expecting to
> learn about internals doing this, but I also ended up picking up a couple of
> usage things as well, like --range-diff for format patch and such.

I usually have "--whitespace=fix" so if you did "git log" on the commits I made out of your patches, it is not surprising if your "log --check" was silent.

I re-applied your v14 with "git am -s --whitespace=nowarn" and here is what I saw.

commit 75a6a3e6597d5f3959eb269122e8c5f4e4baac0e
Author: Bence Ferdinandy <bence@ferdinandy.com>
Date:   Thu Nov 21 23:55:03 2024 +0100
    refs: standardize output of refs_read_symbolic_ref
    
    When the symbolic reference we want to read with refs_read_symbolic_ref
    is actually not a symbolic reference, the files and the reftable
    backends return different values (1 and -1 respectively). Standardize
    the returned values so that 0 is success, -1 is a generic error and -2
    is that the reference was actually non-symbolic.
    
    Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
    Signed-off-by: Junio C Hamano <gitster@pobox.com>
refs/reftable-backend.c:833: indent with spaces.
+        if (ret)
refs/reftable-backend.c:834: indent with spaces.
+                ret = -1;
refs/reftable-backend.c:835: indent with spaces.
+        else if (ref.value_type == REFTABLE_REF_SYMREF)
refs/reftable-backend.c:837: indent with spaces.
+        else
refs/reftable-backend.c:838: indent with spaces.
+                ret = NOT_A_SYMREF;
Bence Ferdinandy· Nov 26, 2024, 14:57 UTC · re: Junio C Hamano · lore

Re: [PATCH v14 03/10] refs: standardize output of refs_read_symbolic_ref

sorry about the other thread, I saw "extend whitespace checks" and I thought based on what Kartik wrote that `log --check` should have caught it, which is why I thought it might be appropriate there.

On Mon Nov 25, 2024 at 03:56, Junio C Hamano <gitster@pobox.com> wrote:
Show 25 quoted lines
> "Bence Ferdinandy" <bence@ferdinandy.com> writes:
>
>>> At the least you should see `git log`'s output, but if there are issues
>>> they should be shown inline. So when you say 'no output' do you mean you
>>> see absolutely no output?
>>
>> Absolutely no output:
>> 	https://asciinema.org/a/lsqp4e1bNst6cFWw9M2jX1IqC
>>
>> But I figured out why: the whitespace and the tabs were not mixed on the line,
>> just across lines. As I read it, that is not an error to have tabs on one line
>> and spaces on the next.
>
> Our .gitattribute starts like so:
>
>     * whitespace=!indent,trail,space
>     *.[ch] whitespace=indent,trail,space diff=cpp
>
> so, unless otherwise specified, we frown upon trailing whitespace
> and space before tab and indenting with non tab is permitted, but C
> source and header files have further care about "indent" (short for
> "indent-with-non-tab".
>
> So mixed or not, if you indented with spaces and not tabs, that
> would be noticed.

So `git log --check --pretty=format:"---% h% s"` was _not_ supposed to catch this?

I ran the CI with this patch again: https://github.com/ferdinandyb/git/actions/runs/12031250376

and it's all green, so wherever this _should_ have been caught: it didn't work.
Show 37 quoted lines
>
>> Anyhow that should be now cleared up, thanks. Gotta say, I was expecting to
>> learn about internals doing this, but I also ended up picking up a couple of
>> usage things as well, like --range-diff for format patch and such.
>
> I usually have "--whitespace=fix" so if you did "git log" on the
> commits I made out of your patches, it is not surprising if your
> "log --check" was silent.
>
> I re-applied your v14 with "git am -s --whitespace=nowarn" and here
> is what I saw.
>
> commit 75a6a3e6597d5f3959eb269122e8c5f4e4baac0e
> Author: Bence Ferdinandy <bence@ferdinandy.com>
> Date:   Thu Nov 21 23:55:03 2024 +0100
>
>     refs: standardize output of refs_read_symbolic_ref
>     
>     When the symbolic reference we want to read with refs_read_symbolic_ref
>     is actually not a symbolic reference, the files and the reftable
>     backends return different values (1 and -1 respectively). Standardize
>     the returned values so that 0 is success, -1 is a generic error and -2
>     is that the reference was actually non-symbolic.
>     
>     Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
>     Signed-off-by: Junio C Hamano <gitster@pobox.com>
>
> refs/reftable-backend.c:833: indent with spaces.
> +        if (ret)
> refs/reftable-backend.c:834: indent with spaces.
> +                ret = -1;
> refs/reftable-backend.c:835: indent with spaces.
> +        else if (ref.value_type == REFTABLE_REF_SYMREF)
> refs/reftable-backend.c:837: indent with spaces.
> +        else
> refs/reftable-backend.c:838: indent with spaces.
> +                ret = NOT_A_SYMREF;
So this did find it. Maybe something is misconfigured in the CI?

Best, Bence

karthik nayak· Nov 26, 2024, 16:53 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v14 03/10] refs: standardize output of refs_read_symbolic_ref

"Bence Ferdinandy" <bence@ferdinandy.com> writes:
> sorry about the other thread, I saw "extend whitespace checks" and I thought
> based on what Kartik wrote that `log --check` should have caught it, which is
> why I thought it might be appropriate there.
>

With v14, running `git log --check --pretty=format:"---% h% s" master..` gives me:

  --- f73b2c577b fetch set_head: handle mirrored bare repositories
  --- c0c84fb7f9 fetch: set remote/HEAD if it does not exist
  --- c366911074 refs: add create_only option to refs_update_symref_extended
  --- c47704d4df refs: add TRANSACTION_CREATE_EXISTS error
  --- 22e7a9bcae remote set-head: better output for --auto
  --- 25d9944eb2 remote set-head: refactor for readability
  --- 01fe46c842 refs: atomically record overwritten ref in update_symref
  --- fed56bc6cc refs: standardize output of refs_read_symbolic_ref
  refs/reftable-backend.c:833: indent with spaces.
  +        if (ret)
  refs/reftable-backend.c:834: indent with spaces.
  +                ret = -1;
  refs/reftable-backend.c:835: indent with spaces.
  +        else if (ref.value_type == REFTABLE_REF_SYMREF)
  refs/reftable-backend.c:837: indent with spaces.
  +        else
  refs/reftable-backend.c:838: indent with spaces.
  +                ret = NOT_A_SYMREF;
  --- d5534d6c67 t/t5505-remote: test failure of set-head
  --- a77b3b7774 t/t5505-remote: set default branch to main
Show 34 quoted lines
> On Mon Nov 25, 2024 at 03:56, Junio C Hamano <gitster@pobox.com> wrote:
>> "Bence Ferdinandy" <bence@ferdinandy.com> writes:
>>
>>>> At the least you should see `git log`'s output, but if there are issues
>>>> they should be shown inline. So when you say 'no output' do you mean you
>>>> see absolutely no output?
>>>
>>> Absolutely no output:
>>> 	https://asciinema.org/a/lsqp4e1bNst6cFWw9M2jX1IqC
>>>
>>> But I figured out why: the whitespace and the tabs were not mixed on the line,
>>> just across lines. As I read it, that is not an error to have tabs on one line
>>> and spaces on the next.
>>
>> Our .gitattribute starts like so:
>>
>>     * whitespace=!indent,trail,space
>>     *.[ch] whitespace=indent,trail,space diff=cpp
>>
>> so, unless otherwise specified, we frown upon trailing whitespace
>> and space before tab and indenting with non tab is permitted, but C
>> source and header files have further care about "indent" (short for
>> "indent-with-non-tab".
>>
>> So mixed or not, if you indented with spaces and not tabs, that
>> would be noticed.
>
> So `git log --check --pretty=format:"---% h% s"` was _not_ supposed to catch
> this?
>
> I ran the CI with this patch again:
> https://github.com/ferdinandyb/git/actions/runs/12031250376
>
> and it's all green, so wherever this _should_ have been caught: it didn't work.

I'm not an expert in GitHub actions, but if you look at `.github/workflows/check-whitespace.yml`, it says it is only triggered for `pull_request`. Did you raise a pull request? From your link, I don't see the whitespace job being triggered, so `it didn't work` would be inaccurate, since it didn't even trigger the job ;)

[snip]
Junio C Hamano· Nov 26, 2024, 20:02 UTC · re: karthik nayak · lore

Re: [PATCH v14 03/10] refs: standardize output of refs_read_symbolic_ref

karthik nayak <karthik.188@gmail.com> writes:
Show 9 quoted lines
> With v14, running `git log --check --pretty=format:"---% h% s" master..`
> gives me:
> ...
>   --- fed56bc6cc refs: standardize output of refs_read_symbolic_ref
>   refs/reftable-backend.c:833: indent with spaces.
>   +        if (ret)
>   refs/reftable-backend.c:834: indent with spaces.
>   +                ret = -1;
> ...

Thanks, the above matches what I saw in my message Bence responded to (to which you are responding). I was unsure if/how the status of each "diff --check" is propagated up to the driving "git log", but the job is reading from the output of the command and not using the exit status of "git log --check", so even if "git log --check" did not signal a check failure with its status, it did not matter ;-) A quick local check says "git log --check" does exit with 2 if there is an offending commit in the range, so we should be OK.

By the way, I appreciate the cuteness of "% s" but I do not see the
point of "% h", as I do not think of a situation where '%h' can ever
be empty.  It seems that this was carried from the first day when
GitHub CI learned the whitespace checks 32c83afc (ci: github action
- add check for whitespace errors, 2020-09-22).
Bence Ferdinandy· Nov 26, 2024, 20:56 UTC · re: Junio C Hamano · lore

Re: [PATCH v14 03/10] refs: standardize output of refs_read_symbolic_ref

On Tue Nov 26, 2024 at 21:02, Junio C Hamano <gitster@pobox.com> wrote:
Show 20 quoted lines
> karthik nayak <karthik.188@gmail.com> writes:
>
>> With v14, running `git log --check --pretty=format:"---% h% s" master..`
>> gives me:
>> ...
>>   --- fed56bc6cc refs: standardize output of refs_read_symbolic_ref
>>   refs/reftable-backend.c:833: indent with spaces.
>>   +        if (ret)
>>   refs/reftable-backend.c:834: indent with spaces.
>>   +                ret = -1;
>> ...
>
> Thanks, the above matches what I saw in my message Bence responded
> to (to which you are responding).  I was unsure if/how the status of
> each "diff --check" is propagated up to the driving "git log", but
> the job is reading from the output of the command and not using the
> exit status of "git log --check", so even if "git log --check" did
> not signal a check failure with its status, it did not matter ;-) A
> quick local check says "git log --check" does exit with 2 if there
> is an offending commit in the range, so we should be OK.

Ok, so `git diff --check master` at least does produce the output for me, and now that you mentioned the exit code, I checked that even though I get zero output from git log --check, the exit code is indeed 2. So now I just don't get why I don't see any output ...

Junio C Hamano· Nov 26, 2024, 21:39 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v14 03/10] refs: standardize output of refs_read_symbolic_ref

"Bence Ferdinandy" <bence@ferdinandy.com> writes:
> Ok, so `git diff --check master` at least does produce the output for me, and
> now that you mentioned the exit code, I checked that even though I get zero
> output from git log --check, the exit code is indeed 2. So now I just don't get
> why I don't see any output ...

I was about to say "perhaps you are accidentally giving a wrong revision range, like 'git checkout master && git log --check master..'?" but then the command should exit with 0 for an empty range, so that is not it.

Do let us know when you find out why ;-)
Thanks.
Bence Ferdinandy· Nov 26, 2024, 20:44 UTC · re: karthik nayak · lore

Re: [PATCH v14 03/10] refs: standardize output of refs_read_symbolic_ref

On Tue Nov 26, 2024 at 17:53, karthik nayak <karthik.188@gmail.com> wrote:
Show 39 quoted lines
> "Bence Ferdinandy" <bence@ferdinandy.com> writes:
>
>> sorry about the other thread, I saw "extend whitespace checks" and I thought
>> based on what Kartik wrote that `log --check` should have caught it, which is
>> why I thought it might be appropriate there.
>>
>
> With v14, running `git log --check --pretty=format:"---% h% s" master..`
> gives me:
>
>   --- f73b2c577b fetch set_head: handle mirrored bare repositories
>
>   --- c0c84fb7f9 fetch: set remote/HEAD if it does not exist
>
>   --- c366911074 refs: add create_only option to refs_update_symref_extended
>
>   --- c47704d4df refs: add TRANSACTION_CREATE_EXISTS error
>
>   --- 22e7a9bcae remote set-head: better output for --auto
>
>   --- 25d9944eb2 remote set-head: refactor for readability
>
>   --- 01fe46c842 refs: atomically record overwritten ref in update_symref
>
>   --- fed56bc6cc refs: standardize output of refs_read_symbolic_ref
>   refs/reftable-backend.c:833: indent with spaces.
>   +        if (ret)
>   refs/reftable-backend.c:834: indent with spaces.
>   +                ret = -1;
>   refs/reftable-backend.c:835: indent with spaces.
>   +        else if (ref.value_type == REFTABLE_REF_SYMREF)
>   refs/reftable-backend.c:837: indent with spaces.
>   +        else
>   refs/reftable-backend.c:838: indent with spaces.
>   +                ret = NOT_A_SYMREF;
>
>   --- d5534d6c67 t/t5505-remote: test failure of set-head
>
>   --- a77b3b7774 t/t5505-remote: set default branch to main

So at least now I know why I'm confused :) I copied your exact command (well, with upstream/master..), and I still have zero output. Since .gitattributes is committed to the repository, what else could be the difference?

[snip]
Show 14 quoted lines
>>
>> So `git log --check --pretty=format:"---% h% s"` was _not_ supposed to catch
>> this?
>>
>> I ran the CI with this patch again:
>> https://github.com/ferdinandyb/git/actions/runs/12031250376
>>
>> and it's all green, so wherever this _should_ have been caught: it didn't work.
>
> I'm not an expert in GitHub actions, but if you look at
> `.github/workflows/check-whitespace.yml`, it says it is only triggered
> for `pull_request`. Did you raise a pull request? From your link, I
> don't see the whitespace job being triggered, so `it didn't work` would
> be inaccurate, since it didn't even trigger the job ;)

Well that make sense, the commit introducing it also talks about pull requests. On the other hand: why does this run only on a pull request? The main.yml runs on both pull requests and pushes and since the project doesn't really use pull requests, it would make more sense to also run this on push? The check-style.yml is similar.

Bence Ferdinandy· Nov 22, 2024, 11:27 UTC · re: karthik nayak · lore

Re: [PATCH v14 03/10] refs: standardize output of refs_read_symbolic_ref

On Fri Nov 22, 2024 at 11:37, karthik nayak <karthik.188@gmail.com> wrote:
Show 25 quoted lines
> Bence Ferdinandy <bence@ferdinandy.com> writes:
>
> [snip]
>
>> diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
>> index 38eb14d591..1809e3426a 100644
>> --- a/refs/reftable-backend.c
>> +++ b/refs/reftable-backend.c
>> @@ -830,10 +830,12 @@ static int reftable_be_read_symbolic_ref(struct ref_store *ref_store,
>>  		return ret;
>>
>>  	ret = reftable_stack_read_ref(stack, refname, &ref);
>> -	if (ret == 0 && ref.value_type == REFTABLE_REF_SYMREF)
>> +        if (ret)
>> +                ret = -1;
>> +        else if (ref.value_type == REFTABLE_REF_SYMREF)
>>  		strbuf_addstr(referent, ref.value.symref);
>> -	else
>> -		ret = -1;
>> +        else
>> +                ret = NOT_A_SYMREF;
>>
>
> I was building my series on top of this, and noticed whitespace issues
> here. A simple way to check your series is to run:
Found it, thanks for catching, I had tabs and whitespaces mixed. 
git log --check didn't find a problem with that though. 
Show 5 quoted lines
>
>>  	reftable_ref_record_release(&ref);
>>  	return ret;
>> --
>> 2.47.0.298.g52a96ec17b
-- 
bence.ferdinandy.com
Bence Ferdinandy· Nov 21, 2024, 22:55 UTC · re: Bence Ferdinandy · lore

[PATCH v14 02/10] t/t5505-remote: test failure of set-head

The test coverage was missing a test for the failure branch of remote set-head auto's output. Add the missing text and while we are at it, correct a small grammatical mistake in the error's output ("setup" is the noun, "set up" is the verb).

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v14: new patch
 builtin/remote.c  |  2 +-
 t/t5505-remote.sh | 12 ++++++++++++
 2 files changed, 13 insertions(+), 1 deletion(-)
Show changes to 2 files +13 −1

builtin/remote.c, t/t5505-remote.sh

diff --git a/builtin/remote.c b/builtin/remote.c
index 76670ddd8b..8a182439f2 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1445,7 +1445,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		if (!refs_ref_exists(get_main_ref_store(the_repository), buf2.buf))
 			result |= error(_("Not a valid ref: %s"), buf2.buf);
 		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head"))
-			result |= error(_("Could not setup %s"), buf.buf);
+			result |= error(_("Could not set up %s"), buf.buf);
 		else if (opt_a)
 			printf("%s/HEAD set to %s\n", argv[0], head_name);
 		free(head_name);
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 9b50276646..4e127bf5b8 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -432,6 +432,18 @@ test_expect_success 'set-head --auto' '
 	)
 '
 
+test_expect_success REFFILES 'set-head --auto failure' '
+	(
+		cd test &&
+		touch .git/refs/remotes/origin/HEAD.lock &&
+		git remote set-head --auto origin 2>errormsg ||
+		tail -n1 errormsg >output &&
+		rm .git/refs/remotes/origin/HEAD.lock &&
+		echo "error: Could not set up refs/remotes/origin/HEAD" >expect &&
+		test_cmp expect output
+	)
+'
+
 test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
 	(
 		cd test &&
-- 
2.47.0.298.g52a96ec17b
Junio C Hamano· Nov 22, 2024, 04:54 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v14 02/10] t/t5505-remote: test failure of set-head

Bence Ferdinandy <bence@ferdinandy.com> writes:
> -			result |= error(_("Could not setup %s"), buf.buf);
> +			result |= error(_("Could not set up %s"), buf.buf);
Good eyes.
Show 9 quoted lines
> diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
> index 9b50276646..4e127bf5b8 100755
> --- a/t/t5505-remote.sh
> +++ b/t/t5505-remote.sh
> @@ -432,6 +432,18 @@ test_expect_success 'set-head --auto' '
>  	)
>  '
>  
> +test_expect_success REFFILES 'set-head --auto failure' '
Writing
	test_when_finished "rm -f test/.git/refs/remotes/origin/HEAD.lock" &&

here allows us not to worry about commands in the sequence before the "rm" below failing unexpectedly.

> +	(
> +		cd test &&
> +		touch .git/refs/remotes/origin/HEAD.lock &&
> +		git remote set-head --auto origin 2>errormsg ||
> +		tail -n1 errormsg >output &&

Are we saying that it is OK for set-head not to fail here? If not, then this should be

		test_must_fail git remote set-head ... 2>err &&
		tail -n 1 err >actual &&
instead.
Show 9 quoted lines
> +		rm .git/refs/remotes/origin/HEAD.lock &&
> +		echo "error: Could not set up refs/remotes/origin/HEAD" >expect &&
> +		test_cmp expect output
> +	)
> +'
> +
>  test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
>  	(
>  		cd test &&
Bence Ferdinandy· Nov 21, 2024, 22:55 UTC · re: Bence Ferdinandy · lore

[PATCH v14 04/10] refs: atomically record overwritten ref in update_symref

When updating a symref with update_symref it's currently not possible to know for sure what was the previous value that was overwritten. Extend refs_update_symref under a new function name, to record the value after the ref has been locked if the caller of refs_update_symref_extended requests it via a new variable in the function call. Make the return value of the function notify the caller, if the previous value was actually not a symbolic reference. Keep the original refs_update_symref function with the same signature, but now as a wrapper around refs_update_symref_extended.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    
    v5: - added before_target to reftables backend
        - added an extra safety check for transaction's existence in refs.c
    
    v6: - no change
    
    v7: - remove the whole before_target concept from the backends and
          handle checking it in refs.c instead (thanks Karthik)
        - rename the before_target to referent which is how the same concept
          is called in the backends
        - change commit prefix to be more in line with project standards
    
    v8: no change
    
    v9: - instead of adding parameters to refs_update_symref, rename what
          was in v8 as refs_update_symref_extended and make refs_update_symref
          a wrapper for that. This significantly reduces the number of files
          that need to be touched, and avoids adding a lot of dummy NULL-s
          in unrelated places.
    
    v10: no change
    
    v11: no change
    
    v12: no change
    
    v13: if referent is a non-symbolic ref, record the hash in referent and
        signal this with a return value of -1
    
    v14: - mistakes were made: trying to read the referent should NOT lead to
         an actual error, even if it doesn't work out in the end, that part of
         the code should only set the return to -2 or not touch it
         - the returned error code by the above was also incorrect and now
           is -2 (and uses NOT_A_SYMREF now)
 refs.c | 22 ++++++++++++++++++++--
 refs.h |  4 ++++
 2 files changed, 24 insertions(+), 2 deletions(-)
Show changes to 2 files +24 −2

refs.c, refs.h

diff --git a/refs.c b/refs.c
index 5f729ed412..d80efd58f0 100644
--- a/refs.c
+++ b/refs.c
@@ -2115,6 +2115,13 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
 
 int refs_update_symref(struct ref_store *refs, const char *ref,
 		       const char *target, const char *logmsg)
+{
+	return refs_update_symref_extended(refs, ref, target, logmsg, NULL);
+}
+
+int refs_update_symref_extended(struct ref_store *refs, const char *ref,
+		       const char *target, const char *logmsg,
+		       struct strbuf *referent)
 {
 	struct ref_transaction *transaction;
 	struct strbuf err = STRBUF_INIT;
@@ -2125,10 +2132,22 @@ int refs_update_symref(struct ref_store *refs, const char *ref,
 	    ref_transaction_update(transaction, ref, NULL, NULL,
 				   target, NULL, REF_NO_DEREF,
 				   logmsg, &err) ||
-	    ref_transaction_commit(transaction, &err)) {
+	    ref_transaction_prepare(transaction, &err)) {
 		ret = error("%s", err.buf);
+		goto cleanup;
 	}
+	if (referent && refs_read_symbolic_ref(refs, ref, referent) == NOT_A_SYMREF) {
+		struct object_id oid;
+		if (!refs_read_ref(refs, ref, &oid)) {
+			strbuf_addstr(referent, oid_to_hex(&oid));
+			ret = NOT_A_SYMREF;
+		}
+	}
+
+	if (ref_transaction_commit(transaction, &err))
+		ret = error("%s", err.buf);
 
+cleanup:
 	strbuf_release(&err);
 	if (transaction)
 		ref_transaction_free(transaction);
@@ -2948,4 +2967,3 @@ int ref_update_expects_existing_old_ref(struct ref_update *update)
 	return (update->flags & REF_HAVE_OLD) &&
 		(!is_null_oid(&update->old_oid) || update->old_target);
 }
-
diff --git a/refs.h b/refs.h
index 22c2b45b8b..5c46ac9f34 100644
--- a/refs.h
+++ b/refs.h
@@ -584,6 +584,10 @@ int refs_copy_existing_ref(struct ref_store *refs, const char *oldref,
 int refs_update_symref(struct ref_store *refs, const char *refname,
 		       const char *target, const char *logmsg);
 
+int refs_update_symref_extended(struct ref_store *refs, const char *refname,
+		       const char *target, const char *logmsg,
+		       struct strbuf *referent);
+
 enum action_on_err {
 	UPDATE_REFS_MSG_ON_ERR,
 	UPDATE_REFS_DIE_ON_ERR,
-- 
2.47.0.298.g52a96ec17b
Bence Ferdinandy· Nov 21, 2024, 22:55 UTC · re: Bence Ferdinandy · lore

[PATCH v14 05/10] remote set-head: refactor for readability

Make two different readability refactors:
Rename strbufs "buf" and "buf2" to something more explanatory.

Instead of calling get_main_ref_store(the_repository) multiple times, call it once and store the result in a new refs variable. Although this change probably offers some performance benefits, the main purpose is to shorten the line lengths of function calls using this variable.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v5: new patch (split from the next patch as a preparatory step)
    
    v6: no change
    
    v7: - change commit prefix to be more in line with project standards
    
    v8: no change
    
    v9: - further improve readability by renaming buf, and buf2 consistently
          with how patch 6 was already done
    
    v10: no change
    
    v11: no change
    
    v12: no change
    
    v13: more precise wording for commit message
    
    v14: no change
 builtin/remote.c | 23 ++++++++++++-----------
 1 file changed, 12 insertions(+), 11 deletions(-)
Show changes to builtin/remote.c +12 −11
diff --git a/builtin/remote.c b/builtin/remote.c
index 8a182439f2..bcc3ee91ef 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1402,8 +1402,9 @@ static int show(int argc, const char **argv, const char *prefix)
 static int set_head(int argc, const char **argv, const char *prefix)
 {
 	int i, opt_a = 0, opt_d = 0, result = 0;
-	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT;
 	char *head_name = NULL;
+	struct ref_store *refs = get_main_ref_store(the_repository);
 
 	struct option options[] = {
 		OPT_BOOL('a', "auto", &opt_a,
@@ -1415,7 +1416,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 	argc = parse_options(argc, argv, prefix, options,
 			     builtin_remote_sethead_usage, 0);
 	if (argc)
-		strbuf_addf(&buf, "refs/remotes/%s/HEAD", argv[0]);
+		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", argv[0]);
 
 	if (!opt_a && !opt_d && argc == 2) {
 		head_name = xstrdup(argv[1]);
@@ -1434,25 +1435,25 @@ static int set_head(int argc, const char **argv, const char *prefix)
 			head_name = xstrdup(states.heads.items[0].string);
 		free_remote_ref_states(&states);
 	} else if (opt_d && !opt_a && argc == 1) {
-		if (refs_delete_ref(get_main_ref_store(the_repository), NULL, buf.buf, NULL, REF_NO_DEREF))
-			result |= error(_("Could not delete %s"), buf.buf);
+		if (refs_delete_ref(refs, NULL, b_head.buf, NULL, REF_NO_DEREF))
+			result |= error(_("Could not delete %s"), b_head.buf);
 	} else
 		usage_with_options(builtin_remote_sethead_usage, options);
 
 	if (head_name) {
-		strbuf_addf(&buf2, "refs/remotes/%s/%s", argv[0], head_name);
+		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", argv[0], head_name);
 		/* make sure it's valid */
-		if (!refs_ref_exists(get_main_ref_store(the_repository), buf2.buf))
-			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head"))
-			result |= error(_("Could not set up %s"), buf.buf);
+		if (!refs_ref_exists(refs, b_remote_head.buf))
+			result |= error(_("Not a valid ref: %s"), b_remote_head.buf);
+		else if (refs_update_symref(refs, b_head.buf, b_remote_head.buf, "remote set-head"))
+			result |= error(_("Could not set up %s"), b_head.buf);
 		else if (opt_a)
 			printf("%s/HEAD set to %s\n", argv[0], head_name);
 		free(head_name);
 	}
 
-	strbuf_release(&buf);
-	strbuf_release(&buf2);
+	strbuf_release(&b_head);
+	strbuf_release(&b_remote_head);
 	return result;
 }
 
-- 
2.47.0.298.g52a96ec17b
Bence Ferdinandy· Nov 21, 2024, 22:55 UTC · re: Bence Ferdinandy · lore

[PATCH v14 07/10] refs: add TRANSACTION_CREATE_EXISTS error

Currently there is only one special error for transaction, for when there is a naming conflict, all other errors are dumped under a generic error. Add a new special error case for when the caller requests the reference to be updated only when it does not yet exist and the reference actually does exist.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    v5: no change
    v6: no change
    
    v7: - change commit prefix to be more in line with project standards
        - changed error checking to Karthik's suggestion
    
    v8: no change
    
    v9: - no change
    
    v10: no change
    
    v11: no change
    
    v12: no change
    
    v13: no change
    
    v14: no change
 refs.h                  |  4 +++-
 refs/files-backend.c    | 24 ++++++++++++++++--------
 refs/reftable-backend.c |  6 ++++--
 3 files changed, 23 insertions(+), 11 deletions(-)
Show changes to 3 files +23 −11

refs.h, refs/files-backend.c, refs/reftable-backend.c

diff --git a/refs.h b/refs.h
index 5c46ac9f34..b243739e4b 100644
--- a/refs.h
+++ b/refs.h
@@ -773,8 +773,10 @@ int ref_transaction_verify(struct ref_transaction *transaction,
 
 /* Naming conflict (for example, the ref names A and A/B conflict). */
 #define TRANSACTION_NAME_CONFLICT -1
+/* When only creation was requested, but the ref already exists. */
+#define TRANSACTION_CREATE_EXISTS -2
 /* All other errors. */
-#define TRANSACTION_GENERIC_ERROR -2
+#define TRANSACTION_GENERIC_ERROR -3
 
 /*
  * Perform the preparatory stages of committing `transaction`. Acquire
diff --git a/refs/files-backend.c b/refs/files-backend.c
index 4cc43c32f2..23ae74089d 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -2501,14 +2501,18 @@ static int split_symref_update(struct ref_update *update,
 static int check_old_oid(struct ref_update *update, struct object_id *oid,
 			 struct strbuf *err)
 {
+	int ret = TRANSACTION_GENERIC_ERROR;
+
 	if (!(update->flags & REF_HAVE_OLD) ||
 		   oideq(oid, &update->old_oid))
 		return 0;
 
-	if (is_null_oid(&update->old_oid))
+	if (is_null_oid(&update->old_oid)) {
 		strbuf_addf(err, "cannot lock ref '%s': "
 			    "reference already exists",
 			    ref_update_original_update_refname(update));
+		ret = TRANSACTION_CREATE_EXISTS;
+	}
 	else if (is_null_oid(oid))
 		strbuf_addf(err, "cannot lock ref '%s': "
 			    "reference is missing but expected %s",
@@ -2521,7 +2525,7 @@ static int check_old_oid(struct ref_update *update, struct object_id *oid,
 			    oid_to_hex(oid),
 			    oid_to_hex(&update->old_oid));
 
-	return -1;
+	return ret;
 }
 
 /*
@@ -2601,9 +2605,11 @@ static int lock_ref_for_update(struct files_ref_store *refs,
 					ret = TRANSACTION_GENERIC_ERROR;
 					goto out;
 				}
-			} else if  (check_old_oid(update, &lock->old_oid, err)) {
-				ret = TRANSACTION_GENERIC_ERROR;
-				goto out;
+			} else {
+				ret = check_old_oid(update, &lock->old_oid, err);
+				if  (ret) {
+					goto out;
+				}
 			}
 		} else {
 			/*
@@ -2634,9 +2640,11 @@ static int lock_ref_for_update(struct files_ref_store *refs,
 				    update->old_target);
 			ret = TRANSACTION_GENERIC_ERROR;
 			goto out;
-		} else if  (check_old_oid(update, &lock->old_oid, err)) {
-			ret = TRANSACTION_GENERIC_ERROR;
-			goto out;
+		} else {
+			ret = check_old_oid(update, &lock->old_oid, err);
+			if  (ret) {
+				goto out;
+			}
 		}
 
 		/*
diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
index 1809e3426a..75b04e84a2 100644
--- a/refs/reftable-backend.c
+++ b/refs/reftable-backend.c
@@ -1208,10 +1208,13 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 				goto done;
 			}
 		} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {
-			if (is_null_oid(&u->old_oid))
+			ret = TRANSACTION_NAME_CONFLICT;
+			if (is_null_oid(&u->old_oid)) {
 				strbuf_addf(err, _("cannot lock ref '%s': "
 						   "reference already exists"),
 					    ref_update_original_update_refname(u));
+				ret = TRANSACTION_CREATE_EXISTS;
+			}
 			else if (is_null_oid(&current_oid))
 				strbuf_addf(err, _("cannot lock ref '%s': "
 						   "reference is missing but expected %s"),
@@ -1223,7 +1226,6 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 					    ref_update_original_update_refname(u),
 					    oid_to_hex(&current_oid),
 					    oid_to_hex(&u->old_oid));
-			ret = -1;
 			goto done;
 		}
 
-- 
2.47.0.298.g52a96ec17b
Bence Ferdinandy· Nov 21, 2024, 22:55 UTC · re: Bence Ferdinandy · lore

[PATCH v14 08/10] refs: add create_only option to refs_update_symref_extended

Allow the caller to specify that it only wants to update the symref if it does not already exist. Silently ignore the error from the transaction API if the symref already exists.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    v5: no change
    
    v6: - switched from bool to int for create_only
        - refactored logic in refs_update_symref with goto as layed out by
          Junio
    
    v7: - change commit prefix to be more in line with project standards
        - refactored code to accommodate changes in the first patch, but
          otherwise no change
    
    v8: no change
    
    v9: - no change (except for following through the
          refs_update_symref_extended refactoring)
    
    v10: no change
    
    v11: no change
    
    v12: no change
    
    v13: changes only due to changes in previous patch
    
    v14: no change
 builtin/remote.c |  2 +-
 refs.c           | 33 ++++++++++++++++++++++++---------
 refs.h           |  2 +-
 3 files changed, 26 insertions(+), 11 deletions(-)
Show changes to 3 files +26 −11

builtin/remote.c, refs.c, refs.h

diff --git a/builtin/remote.c b/builtin/remote.c
index 6d659d63ca..3dc1135b5c 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1476,7 +1476,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		goto cleanup;
 	}
 	was_detached = refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
-			"remote set-head", &b_local_head);
+			"remote set-head", &b_local_head, 0);
 	if (was_detached == -1) {
 		result |= error(_("Could not set up %s"), b_head.buf);
 		goto cleanup;
diff --git a/refs.c b/refs.c
index d80efd58f0..2efa6bcc5c 100644
--- a/refs.c
+++ b/refs.c
@@ -2116,26 +2116,38 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
 int refs_update_symref(struct ref_store *refs, const char *ref,
 		       const char *target, const char *logmsg)
 {
-	return refs_update_symref_extended(refs, ref, target, logmsg, NULL);
+	return refs_update_symref_extended(refs, ref, target, logmsg, NULL, 0);
 }
 
 int refs_update_symref_extended(struct ref_store *refs, const char *ref,
 		       const char *target, const char *logmsg,
-		       struct strbuf *referent)
+		       struct strbuf *referent, int create_only)
 {
 	struct ref_transaction *transaction;
 	struct strbuf err = STRBUF_INIT;
-	int ret = 0;
+	int ret = 0, prepret = 0;
 
 	transaction = ref_store_transaction_begin(refs, &err);
-	if (!transaction ||
-	    ref_transaction_update(transaction, ref, NULL, NULL,
-				   target, NULL, REF_NO_DEREF,
-				   logmsg, &err) ||
-	    ref_transaction_prepare(transaction, &err)) {
+	if (!transaction) {
+	error_return:
 		ret = error("%s", err.buf);
 		goto cleanup;
 	}
+	if (create_only) {
+		if (ref_transaction_create(transaction, ref, NULL, target,
+					   REF_NO_DEREF, logmsg, &err))
+			goto error_return;
+		prepret = ref_transaction_prepare(transaction, &err);
+		if (prepret && prepret != TRANSACTION_CREATE_EXISTS)
+			goto error_return;
+	} else {
+		if (ref_transaction_update(transaction, ref, NULL, NULL,
+					   target, NULL, REF_NO_DEREF,
+					   logmsg, &err) ||
+			ref_transaction_prepare(transaction, &err))
+			goto error_return;
+	}
+
 	if (referent && refs_read_symbolic_ref(refs, ref, referent) == NOT_A_SYMREF) {
 		struct object_id oid;
 		if (!refs_read_ref(refs, ref, &oid)) {
@@ -2144,8 +2156,11 @@ int refs_update_symref_extended(struct ref_store *refs, const char *ref,
 		}
 	}
 
+	if (prepret == TRANSACTION_CREATE_EXISTS)
+		goto cleanup;
+
 	if (ref_transaction_commit(transaction, &err))
-		ret = error("%s", err.buf);
+		goto error_return;
 
 cleanup:
 	strbuf_release(&err);
diff --git a/refs.h b/refs.h
index b243739e4b..be38377b1f 100644
--- a/refs.h
+++ b/refs.h
@@ -586,7 +586,7 @@ int refs_update_symref(struct ref_store *refs, const char *refname,
 
 int refs_update_symref_extended(struct ref_store *refs, const char *refname,
 		       const char *target, const char *logmsg,
-		       struct strbuf *referent);
+		       struct strbuf *referent, int create_only);
 
 enum action_on_err {
 	UPDATE_REFS_MSG_ON_ERR,
-- 
2.47.0.298.g52a96ec17b
Bence Ferdinandy· Nov 21, 2024, 22:55 UTC · re: Bence Ferdinandy · lore

[PATCH v14 06/10] remote set-head: better output for --auto

Currently, set-head --auto will print a message saying "remote/HEAD set to branch", which implies something was changed.

Change the output of --auto, so the output actually reflects what was done: a) set a previously unset HEAD, b) change HEAD because remote changed or c) no updates. As edge cases, if HEAD is changed from a previous symbolic reference that was not a remote branch, explicitly call attention to this fact, and also notify the user if the previous reference was not a symbolic reference.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v1-v2:
    
        was RFC in https://lore.kernel.org/git/20240910203835.2288291-1-bence@ferdinandy.com/
    
    v3:
    
        This patch was originally sent along when I thought set-head was
        going to be invoked by fetch, but the discussion on the RFC
        concluded that it should be not. This opened the possibility to make
        it more explicit.
    
        Note: although I feel both things the patch does are really just
        cosmetic, an argument could be made for breaking it into two, one
        for the no-op part and one for the --auto print update.
    
        Was sent in:
        https://lore.kernel.org/git/20240915221055.904107-1-bence@ferdinandy.com/
    
    v4:
        - changes are now handled atomically via the ref update transaction
        - outputs have changed along the lines of Junio's suggestion
        - minor refactor to set_head for improved legibility
    
    v5: - the minor refactor has been split out into its own patch
    
    v6: - fixed uninitialized prev_head
        - fixed case of unusual previous target
        - fixed a test that would have been actually broken at this patch
          (the output was only correct with the later update to fetch)
        - added 4 tests for the 4 output cases
    
    v7: - change commit prefix to be more in line with project standards
        - fixed tests to also work with the reftable backend
        - renamed report function, fixed style issue with checking buf len
        - fixed not releasing an strbuf
    
    v8: no change
    
    v9: - mark output strings in report_set_head_auto as translatable
        - rename buf_prev to b_local_head for consistency
        - use ${SQ} in tests instead of '\''
    
    v10: no change
    
    v11: no change
    
    v12: no change
    
    v13: added handling the edge case of previous remote/HEAD being
    a detached HEAD
    
    v14: - fixed badly named variable
         - fixed not reporting errors correctly
 builtin/remote.c  | 59 +++++++++++++++++++++++++++++++++++---------
 t/t5505-remote.sh | 63 ++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 109 insertions(+), 13 deletions(-)
Show changes to 2 files +109 −13

builtin/remote.c, t/t5505-remote.sh

diff --git a/builtin/remote.c b/builtin/remote.c
index bcc3ee91ef..6d659d63ca 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1399,10 +1399,38 @@ static int show(int argc, const char **argv, const char *prefix)
 	return result;
 }
 
+static void report_set_head_auto(const char *remote, const char *head_name,
+			struct strbuf *b_local_head, int was_detached) {
+	struct strbuf buf_prefix = STRBUF_INIT;
+	const char *prev_head = NULL;
+
+	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
+	skip_prefix(b_local_head->buf, buf_prefix.buf, &prev_head);
+
+	if (prev_head && !strcmp(prev_head, head_name))
+		printf(_("'%s/HEAD' is unchanged and points to '%s'\n"),
+			remote, head_name);
+	else if (prev_head)
+		printf(_("'%s/HEAD' has changed from '%s' and now points to '%s'\n"),
+			remote, prev_head, head_name);
+	else if (!b_local_head->len)
+		printf(_("'%s/HEAD' is now created and points to '%s'\n"),
+			remote, head_name);
+	else if (was_detached && b_local_head->len)
+		printf(_("'%s/HEAD' was detached at '%s' and now points to '%s'\n"),
+			remote, b_local_head->buf, head_name);
+	else
+		printf(_("'%s/HEAD' used to point to '%s' "
+			"(which is not a remote branch), but now points to '%s'\n"),
+			remote, b_local_head->buf, head_name);
+	strbuf_release(&buf_prefix);
+}
+
 static int set_head(int argc, const char **argv, const char *prefix)
 {
-	int i, opt_a = 0, opt_d = 0, result = 0;
-	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT;
+	int i, opt_a = 0, opt_d = 0, result = 0, was_detached;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
+		b_local_head = STRBUF_INIT;
 	char *head_name = NULL;
 	struct ref_store *refs = get_main_ref_store(the_repository);
 
@@ -1440,20 +1468,27 @@ static int set_head(int argc, const char **argv, const char *prefix)
 	} else
 		usage_with_options(builtin_remote_sethead_usage, options);
 
-	if (head_name) {
-		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", argv[0], head_name);
-		/* make sure it's valid */
-		if (!refs_ref_exists(refs, b_remote_head.buf))
-			result |= error(_("Not a valid ref: %s"), b_remote_head.buf);
-		else if (refs_update_symref(refs, b_head.buf, b_remote_head.buf, "remote set-head"))
-			result |= error(_("Could not set up %s"), b_head.buf);
-		else if (opt_a)
-			printf("%s/HEAD set to %s\n", argv[0], head_name);
-		free(head_name);
+	if (!head_name)
+		goto cleanup;
+	strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", argv[0], head_name);
+	if (!refs_ref_exists(refs, b_remote_head.buf)) {
+		result |= error(_("Not a valid ref: %s"), b_remote_head.buf);
+		goto cleanup;
+	}
+	was_detached = refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
+			"remote set-head", &b_local_head);
+	if (was_detached == -1) {
+		result |= error(_("Could not set up %s"), b_head.buf);
+		goto cleanup;
 	}
+	if (opt_a)
+		report_set_head_auto(argv[0], head_name, &b_local_head, was_detached);
 
+cleanup:
+	free(head_name);
 	strbuf_release(&b_head);
 	strbuf_release(&b_remote_head);
+	strbuf_release(&b_local_head);
 	return result;
 }
 
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 4e127bf5b8..e6acff4afe 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -444,12 +444,63 @@ test_expect_success REFFILES 'set-head --auto failure' '
 	)
 '
 
+test_expect_success 'set-head --auto detects creation' '
+	(
+		cd test &&
+		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} is now created and points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto to update a non symbolic ref' '
+	(
+		cd test &&
+		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
+		git update-ref refs/remotes/origin/HEAD HEAD &&
+		HEAD=$(git log --pretty="%H") &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} was detached at ${SQ}${HEAD}${SQ} and now points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects no change' '
+	(
+		cd test &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} is unchanged and points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects change' '
+	(
+		cd test &&
+		git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/ahead &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} has changed from ${SQ}ahead${SQ} and now points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects strange ref' '
+	(
+		cd test &&
+		git symbolic-ref refs/remotes/origin/HEAD refs/heads/main &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} used to point to ${SQ}refs/heads/main${SQ} (which is not a remote branch), but now points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
 test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
 	(
 		cd test &&
 		git fetch two "refs/heads/*:refs/remotes/two/*" &&
 		git remote set-head --auto two >output 2>&1 &&
-		echo "two/HEAD set to main" >expect &&
+		echo "${SQ}two/HEAD${SQ} is now created and points to ${SQ}main${SQ}" >expect &&
 		test_cmp expect output
 	)
 '
@@ -468,6 +519,16 @@ test_expect_success 'set-head explicit' '
 	)
 '
 
+test_expect_success 'set-head --auto reports change' '
+	(
+		cd test &&
+		git remote set-head origin side2 &&
+		git remote set-head --auto origin >output 2>&1 &&
+		echo "${SQ}origin/HEAD${SQ} has changed from ${SQ}side2${SQ} and now points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
 cat >test/expect <<EOF
 Pruning origin
 URL: $(pwd)/one
-- 
2.47.0.298.g52a96ec17b
Bence Ferdinandy· Nov 21, 2024, 22:55 UTC · re: Bence Ferdinandy · lore

[PATCH v14 10/10] fetch set_head: handle mirrored bare repositories

When adding a remote to bare repository with "git remote add --mirror",
running fetch will fail to update HEAD to the remote's HEAD, since it
does not know how to handle bare repositories. On the other hand HEAD
already has content, since "git init --bare" has already set HEAD to
whatever is the default branch set for the user. Unless this - by chance
- is the same as the remote's HEAD, HEAD will be pointing to a bad
symref. Teach set_head to handle bare repositories, by overwriting HEAD
so it mirrors the remote's HEAD.

Note, that in this case overriding the local HEAD reference is necessary, since HEAD will exist before fetch can be run, but this should not be an issue, since the whole purpose of --mirror is to be an exact mirror of the remote, so following any changes to HEAD makes sense.

Also note, that although "git remote set-head" also fails when trying to update the remote's locally tracked HEAD in a mirrored bare repository, the usage of the command does not make much sense after this patch: fetch will update the remote HEAD correctly, and setting it manually to something else is antithetical to the concept of mirroring.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    state:
        * last review: none
    
    v10: - new patch
         - handles the issue discovered in
           https://lore.kernel.org/git/D4ZAELFWJMKN.S88LJ6YK31LZ@ferdinandy.com/T/
    
    v11: no change
    
    v12: no change
    
    v13: properly print the previous head if it was detached
    
    v14: no change
 builtin/fetch.c   | 16 +++++++++++-----
 t/t5505-remote.sh | 10 ++++++++++
 2 files changed, 21 insertions(+), 5 deletions(-)
Show changes to 2 files +21 −5

builtin/fetch.c, t/t5505-remote.sh

diff --git a/builtin/fetch.c b/builtin/fetch.c
index b569c87f97..5103793191 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1585,7 +1585,7 @@ static const char *strip_refshead(const char *name){
 
 static int set_head(const struct ref *remote_refs)
 {
-	int result = 0;
+	int result = 0, is_bare;
 	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT;
 	const char *remote = gtransport->remote->name;
 	char *head_name = NULL;
@@ -1617,15 +1617,21 @@ static int set_head(const struct ref *remote_refs)
 
 	if (!head_name)
 		goto cleanup;
-	strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
-	strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote, head_name);
+	is_bare = is_bare_repository();
+	if (is_bare) {
+		strbuf_addstr(&b_head, "HEAD");
+		strbuf_addf(&b_remote_head, "refs/heads/%s", head_name);
+	} else {
+		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
+		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote, head_name);
+	}
 		/* make sure it's valid */
-	if (!refs_ref_exists(refs, b_remote_head.buf)) {
+	if (!is_bare && !refs_ref_exists(refs, b_remote_head.buf)) {
 		result = 1;
 		goto cleanup;
 	}
 	if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
-					"fetch", NULL, 1))
+					"fetch", NULL, !is_bare))
 		result = 1;
 
 cleanup:
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 01d2601174..b4f1ae5fb1 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -569,6 +569,16 @@ test_expect_success 'add --mirror && prune' '
 	)
 '
 
+test_expect_success 'add --mirror setting HEAD' '
+	mkdir headmirror &&
+	(
+		cd headmirror &&
+		git init --bare -b notmain &&
+		git remote add --mirror -f origin ../one &&
+		test "$(git symbolic-ref HEAD)" = "refs/heads/main"
+	)
+'
+
 test_expect_success 'add --mirror=fetch' '
 	mkdir mirror-fetch &&
 	git init -b main mirror-fetch/parent &&
-- 
2.47.0.298.g52a96ec17b
Bence Ferdinandy· Nov 21, 2024, 22:55 UTC · re: Bence Ferdinandy · lore

[PATCH v14 09/10] fetch: set remote/HEAD if it does not exist

When cloning a repository remote/HEAD is created, but when the user creates a repository with git init, and later adds a remote, remote/HEAD is only created if the user explicitly runs a variant of "remote set-head". Attempt to set remote/HEAD during fetch, if the user does not have it already set. Silently ignore any errors.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v3: - does not rely on remote set-head anymore so it only authenticates
        once
        - uses the new REF_CREATE_ONLY to atomically check if the ref exists
          and only write it if it doesn't
        - in all other cases the maximum it does is print a warning
    
    v4: - instead of the discarded REF_CREATE_ONLY, it uses the existing,
          but updated transaction api to request a silent create only
        - it now uses the atomic before_target to determine reporting
        - refactored for legibility
    
    v5: - instead of printing a not too useful message, it now fails
          silently, this in line with the objective to only set up
          remote/HEAD automatically if the right thing is trivial, for
          everything else there is remote set-head
        - fixed all failing tests
        - added two new tests, one for checking if remote/HEAD is set to the
          correct one, and one to test that we do not override remote/HEAD
          if it has changed on the server from what we have locally
    
    v6: - fixed style issues and unintended extra empty line
        - updated function call with bool to int from previous patch's
          change
        - removed calls to error(...) inherited from builtin/remote.c so we
          actually fail silently
        - set the test for remote set-head --auto to the correct value here,
          which was previously erronously set in the remote set-head patch
    
    v7: - no change
    
    v8: - changed logmsg in call to refs_update_symref from "remote
          set-head" to "fetch"
    
    v9: - follow through with refs_update_symref_extended
        - fix test errors uncovered by the new patch
    
    v10: no change
    
    v11: fixed some memory leaks
    
    v12: no change
    
    v13: - fix printed information if the local HEAD is detached
         - remove accidental formatting noise in a test
    
    v14: remove report_set_head_auto: as discussed, any noise should only be
    created with an opt out, and the configuration for this will be added in
    a later patch
 builtin/fetch.c                  | 68 ++++++++++++++++++++++++++++++++
 t/t4207-log-decoration-colors.sh |  3 +-
 t/t5505-remote.sh                | 21 +++++++---
 t/t5510-fetch.sh                 | 24 +++++++++++
 t/t5512-ls-remote.sh             |  2 +
 t/t5514-fetch-multiple.sh        | 17 +++++++-
 t/t5516-fetch-push.sh            |  3 +-
 t/t5527-fetch-odd-refs.sh        |  3 +-
 t/t7900-maintenance.sh           |  3 +-
 t/t9210-scalar.sh                |  5 ++-
 t/t9211-scalar-clone.sh          |  6 +--
 t/t9902-completion.sh            | 65 ++++++++++++++++++++++++++++++
 12 files changed, 203 insertions(+), 17 deletions(-)
Show changes to 12 files +203 −17

builtin/fetch.c, t/t4207-log-decoration-colors.sh, t/t5505-remote.sh, t/t5510-fetch.sh, t/t5512-ls-remote.sh, t/t5514-fetch-multiple.sh, t/t5516-fetch-push.sh, t/t5527-fetch-odd-refs.sh, t/t7900-maintenance.sh, t/t9210-scalar.sh, t/t9211-scalar-clone.sh, t/t9902-completion.sh

diff --git a/builtin/fetch.c b/builtin/fetch.c
index 18eff4e5fa..b569c87f97 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1578,6 +1578,66 @@ static int backfill_tags(struct display_state *display_state,
 	return retcode;
 }
 
+static const char *strip_refshead(const char *name){
+	skip_prefix(name, "refs/heads/", &name);
+	return name;
+}
+
+static int set_head(const struct ref *remote_refs)
+{
+	int result = 0;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT;
+	const char *remote = gtransport->remote->name;
+	char *head_name = NULL;
+	struct ref *ref, *matches;
+	struct ref *fetch_map = NULL, **fetch_map_tail = &fetch_map;
+	struct refspec_item refspec = {
+		.force = 0,
+		.pattern = 1,
+		.src = (char *) "refs/heads/*",
+		.dst = (char *) "refs/heads/*",
+	};
+	struct string_list heads = STRING_LIST_INIT_DUP;
+	struct ref_store *refs = get_main_ref_store(the_repository);
+
+	get_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);
+	matches = guess_remote_head(find_ref_by_name(remote_refs, "HEAD"),
+				    fetch_map, 1);
+	for (ref = matches; ref; ref = ref->next) {
+		string_list_append(&heads, strip_refshead(ref->name));
+	}
+
+
+	if (!heads.nr)
+		result = 1;
+	else if (heads.nr > 1)
+		result = 1;
+	else
+		head_name = xstrdup(heads.items[0].string);
+
+	if (!head_name)
+		goto cleanup;
+	strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
+	strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote, head_name);
+		/* make sure it's valid */
+	if (!refs_ref_exists(refs, b_remote_head.buf)) {
+		result = 1;
+		goto cleanup;
+	}
+	if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
+					"fetch", NULL, 1))
+		result = 1;
+
+cleanup:
+	free(head_name);
+	free_refs(fetch_map);
+	free_refs(matches);
+	string_list_clear(&heads, 0);
+	strbuf_release(&b_head);
+	strbuf_release(&b_remote_head);
+	return result;
+}
+
 static int do_fetch(struct transport *transport,
 		    struct refspec *rs,
 		    const struct fetch_config *config)
@@ -1647,6 +1707,8 @@ static int do_fetch(struct transport *transport,
 				    "refs/tags/");
 	}
 
+	strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
+
 	if (must_list_refs) {
 		trace2_region_enter("fetch", "remote_refs", the_repository);
 		remote_refs = transport_get_remote_refs(transport,
@@ -1791,6 +1853,12 @@ static int do_fetch(struct transport *transport,
 				  "you need to specify exactly one branch with the --set-upstream option"));
 		}
 	}
+	if (set_head(remote_refs))
+		;
+		/*
+		 * Way too many cases where this can go wrong
+		 * so let's just fail silently for now.
+		 */
 
 cleanup:
 	if (retcode) {
diff --git a/t/t4207-log-decoration-colors.sh b/t/t4207-log-decoration-colors.sh
index 73ea9e5155..d55d22cb2f 100755
--- a/t/t4207-log-decoration-colors.sh
+++ b/t/t4207-log-decoration-colors.sh
@@ -59,7 +59,8 @@ ${c_reset}${c_tag}tag: ${c_reset}${c_tag}v1.0${c_reset}${c_commit}, \
 ${c_reset}${c_tag}tag: ${c_reset}${c_tag}B${c_reset}${c_commit})${c_reset} B
 ${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
 ${c_tag}tag: ${c_reset}${c_tag}A1${c_reset}${c_commit}, \
-${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit})${c_reset} A1
+${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit}, \
+${c_reset}${c_remoteBranch}other/HEAD${c_reset}${c_commit})${c_reset} A1
 	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
 ${c_stash}refs/stash${c_reset}${c_commit})${c_reset} On main: Changes to A.t
 	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index e6acff4afe..01d2601174 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -74,7 +74,7 @@ test_expect_success 'add another remote' '
 		cd test &&
 		git remote add -f second ../two &&
 		tokens_match "origin second" "$(git remote)" &&
-		check_tracking_branch second main side another &&
+		check_tracking_branch second main side another HEAD &&
 		git for-each-ref "--format=%(refname)" refs/remotes |
 		sed -e "/^refs\/remotes\/origin\//d" \
 		    -e "/^refs\/remotes\/second\//d" >actual &&
@@ -500,7 +500,7 @@ test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
 		cd test &&
 		git fetch two "refs/heads/*:refs/remotes/two/*" &&
 		git remote set-head --auto two >output 2>&1 &&
-		echo "${SQ}two/HEAD${SQ} is now created and points to ${SQ}main${SQ}" >expect &&
+		echo "${SQ}two/HEAD${SQ} is unchanged and points to ${SQ}main${SQ}" >expect &&
 		test_cmp expect output
 	)
 '
@@ -788,8 +788,10 @@ test_expect_success 'reject --no-no-tags' '
 '
 
 cat >one/expect <<\EOF
+  apis/HEAD -> apis/main
   apis/main
   apis/side
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
@@ -807,11 +809,14 @@ test_expect_success 'update' '
 '
 
 cat >one/expect <<\EOF
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
+  manduca/HEAD -> manduca/main
   manduca/main
   manduca/side
+  megaloprepus/HEAD -> megaloprepus/main
   megaloprepus/main
   megaloprepus/side
 EOF
@@ -819,7 +824,7 @@ EOF
 test_expect_success 'update with arguments' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -851,10 +856,13 @@ test_expect_success 'update --prune' '
 '
 
 cat >one/expect <<-\EOF
+  apis/HEAD -> apis/main
   apis/main
   apis/side
+  manduca/HEAD -> manduca/main
   manduca/main
   manduca/side
+  megaloprepus/HEAD -> megaloprepus/main
   megaloprepus/main
   megaloprepus/side
 EOF
@@ -862,7 +870,7 @@ EOF
 test_expect_success 'update default' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -874,6 +882,7 @@ test_expect_success 'update default' '
 '
 
 cat >one/expect <<\EOF
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
@@ -882,7 +891,7 @@ EOF
 test_expect_success 'update default (overridden, with funny whitespace)' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -896,7 +905,7 @@ test_expect_success 'update default (overridden, with funny whitespace)' '
 test_expect_success 'update (with remotes.default defined)' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index 0890b9f61c..87698341f5 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -75,6 +75,30 @@ test_expect_success "fetch test for-merge" '
 	cut -f -2 .git/FETCH_HEAD >actual &&
 	test_cmp expected actual'
 
+test_expect_success "fetch test remote HEAD" '
+	cd "$D" &&
+	cd two &&
+	git fetch &&
+	git rev-parse --verify refs/remotes/origin/HEAD &&
+	git rev-parse --verify refs/remotes/origin/main &&
+	head=$(git rev-parse refs/remotes/origin/HEAD) &&
+	branch=$(git rev-parse refs/remotes/origin/main) &&
+	test "z$head" = "z$branch"'
+
+test_expect_success "fetch test remote HEAD change" '
+	cd "$D" &&
+	cd two &&
+	git switch -c other &&
+	git push -u origin other &&
+	git rev-parse --verify refs/remotes/origin/HEAD &&
+	git rev-parse --verify refs/remotes/origin/main &&
+	git rev-parse --verify refs/remotes/origin/other &&
+	git remote set-head origin other &&
+	git fetch &&
+	head=$(git rev-parse refs/remotes/origin/HEAD) &&
+	branch=$(git rev-parse refs/remotes/origin/other) &&
+	test "z$head" = "z$branch"'
+
 test_expect_success 'fetch --prune on its own works as expected' '
 	cd "$D" &&
 	git clone . prune &&
diff --git a/t/t5512-ls-remote.sh b/t/t5512-ls-remote.sh
index 64b3491e4e..1b3865e154 100755
--- a/t/t5512-ls-remote.sh
+++ b/t/t5512-ls-remote.sh
@@ -293,6 +293,8 @@ test_expect_success 'ls-remote with filtered symref (refname)' '
 	cat >expect <<-EOF &&
 	ref: refs/heads/main	HEAD
 	$rev	HEAD
+	ref: refs/remotes/origin/main	refs/remotes/origin/HEAD
+	$rev	refs/remotes/origin/HEAD
 	EOF
 	git ls-remote --symref . HEAD >actual &&
 	test_cmp expect actual
diff --git a/t/t5514-fetch-multiple.sh b/t/t5514-fetch-multiple.sh
index 579872c258..e3482b27b2 100755
--- a/t/t5514-fetch-multiple.sh
+++ b/t/t5514-fetch-multiple.sh
@@ -45,14 +45,17 @@ test_expect_success setup '
 '
 
 cat > test/expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
   origin/HEAD -> origin/main
   origin/main
   origin/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -97,6 +100,7 @@ cat > expect << EOF
   origin/HEAD -> origin/main
   origin/main
   origin/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
@@ -112,8 +116,10 @@ test_expect_success 'git fetch --multiple (but only one remote)' '
 '
 
 cat > expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -141,7 +147,7 @@ test_expect_success 'git fetch --multiple (bad remote names)' '
 
 test_expect_success 'git fetch --all (skipFetchAll)' '
 	(cd test4 &&
-	 for b in $(git branch -r)
+	 for b in $(git branch -r | grep -v HEAD)
 	 do
 		git branch -r -d $b || exit 1
 	 done &&
@@ -153,11 +159,14 @@ test_expect_success 'git fetch --all (skipFetchAll)' '
 '
 
 cat > expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -165,7 +174,7 @@ EOF
 
 test_expect_success 'git fetch --multiple (ignoring skipFetchAll)' '
 	(cd test4 &&
-	 for b in $(git branch -r)
+	 for b in $(git branch -r | grep -v HEAD)
 	 do
 		git branch -r -d $b || exit 1
 	 done &&
@@ -221,14 +230,17 @@ test_expect_success 'git fetch --multiple --jobs=0 picks a default' '
 
 create_fetch_all_expect () {
 	cat >expect <<-\EOF
+	  one/HEAD -> one/main
 	  one/main
 	  one/side
 	  origin/HEAD -> origin/main
 	  origin/main
 	  origin/side
+	  three/HEAD -> three/main
 	  three/another
 	  three/main
 	  three/side
+	  two/HEAD -> two/main
 	  two/another
 	  two/main
 	  two/side
@@ -265,6 +277,7 @@ test_expect_success 'git fetch (fetch all remotes with fetch.all = true)' '
 
 create_fetch_one_expect () {
 	cat >expect <<-\EOF
+	  one/HEAD -> one/main
 	  one/main
 	  one/side
 	  origin/HEAD -> origin/main
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 331778bd42..5a051aa0c7 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1395,7 +1395,8 @@ test_expect_success 'fetch follows tags by default' '
 		git tag -m "annotated" tag &&
 		git for-each-ref >tmp1 &&
 		sed -n "p; s|refs/heads/main$|refs/remotes/origin/main|p" tmp1 |
-		sort -k 3 >../expect
+		sed -n "p; s|refs/heads/main$|refs/remotes/origin/HEAD|p"  |
+		sort -k 4 >../expect
 	) &&
 	test_when_finished "rm -rf dst" &&
 	git init dst &&
diff --git a/t/t5527-fetch-odd-refs.sh b/t/t5527-fetch-odd-refs.sh
index 98ece27c6a..d3996af6ee 100755
--- a/t/t5527-fetch-odd-refs.sh
+++ b/t/t5527-fetch-odd-refs.sh
@@ -52,7 +52,8 @@ test_expect_success LONG_REF 'fetch handles extremely long refname' '
 	long
 	main
 	EOF
-	git for-each-ref --format="%(subject)" refs/remotes/long >actual &&
+	git for-each-ref --format="%(subject)" refs/remotes/long \
+		--exclude=refs/remotes/long/HEAD >actual &&
 	test_cmp expect actual
 '
 
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index c224c8450c..edb85b7145 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -329,7 +329,8 @@ test_expect_success 'incremental-repack task' '
 
 	# Delete refs that have not been repacked in these packs.
 	git for-each-ref --format="delete %(refname)" \
-		refs/prefetch refs/tags refs/remotes >refs &&
+		refs/prefetch refs/tags refs/remotes \
+		--exclude=refs/remotes/*/HEAD >refs &&
 	git update-ref --stdin <refs &&
 
 	# Replace the object directory with this pack layout.
diff --git a/t/t9210-scalar.sh b/t/t9210-scalar.sh
index a30b2c9f70..2237844550 100755
--- a/t/t9210-scalar.sh
+++ b/t/t9210-scalar.sh
@@ -151,7 +151,8 @@ test_expect_success 'scalar clone' '
 			"$(pwd)" &&
 
 		git for-each-ref --format="%(refname)" refs/remotes/origin/ >actual &&
-		echo "refs/remotes/origin/parallel" >expect &&
+		echo "refs/remotes/origin/HEAD" >>expect &&
+		echo "refs/remotes/origin/parallel" >>expect &&
 		test_cmp expect actual &&
 
 		test_path_is_missing 1/2 &&
@@ -220,7 +221,7 @@ test_expect_success 'scalar reconfigure --all with includeIf.onbranch' '
 	done
 '
 
- test_expect_success 'scalar reconfigure --all with detached HEADs' '
+test_expect_success 'scalar reconfigure --all with detached HEADs' '
 	repos="two three four" &&
 	for num in $repos
 	do
diff --git a/t/t9211-scalar-clone.sh b/t/t9211-scalar-clone.sh
index c16ea67c1d..d9cb6b9a3e 100755
--- a/t/t9211-scalar-clone.sh
+++ b/t/t9211-scalar-clone.sh
@@ -32,7 +32,7 @@ test_expect_success 'set up repository to clone' '
 	)
 '
 
-cleanup_clone () {
+cleanup_clone() {
 	rm -rf "$1"
 }
 
@@ -128,7 +128,7 @@ test_expect_success '--single-branch clones HEAD only' '
 	(
 		cd $enlistment/src &&
 		git for-each-ref refs/remotes/origin >out &&
-		test_line_count = 1 out &&
+		test_line_count = 2 out &&
 		grep "refs/remotes/origin/base" out
 	) &&
 
@@ -142,7 +142,7 @@ test_expect_success '--no-single-branch clones all branches' '
 	(
 		cd $enlistment/src &&
 		git for-each-ref refs/remotes/origin >out &&
-		test_line_count = 2 out &&
+		test_line_count = 3 out &&
 		grep "refs/remotes/origin/base" out &&
 		grep "refs/remotes/origin/parallel" out
 	) &&
diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh
index cc6aa9f0cd..b663c4609e 100755
--- a/t/t9902-completion.sh
+++ b/t/t9902-completion.sh
@@ -658,6 +658,7 @@ test_expect_success '__git_refs - simple' '
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	matching-tag
@@ -673,6 +674,7 @@ test_expect_success '__git_refs - full refs' '
 	cat >expected <<-EOF &&
 	refs/heads/main
 	refs/heads/matching-branch
+	refs/remotes/other/HEAD
 	refs/remotes/other/branch-in-other
 	refs/remotes/other/main-in-other
 	refs/tags/matching-tag
@@ -729,6 +731,7 @@ test_expect_success '__git_refs - remote on local file system - full refs' '
 test_expect_success '__git_refs - configured remote' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -756,6 +759,7 @@ test_expect_success '__git_refs - configured remote - full refs' '
 test_expect_success '__git_refs - configured remote - repo given on the command line' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -787,6 +791,7 @@ test_expect_success '__git_refs - configured remote - full refs - repo given on
 test_expect_success '__git_refs - configured remote - remote name matches a directory' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -875,12 +880,14 @@ test_expect_success '__git_refs - unique remote branches for git checkout DWIMer
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/ambiguous
 	other/branch-in-other
 	other/main-in-other
 	remote/ambiguous
 	remote/branch-in-remote
 	matching-tag
+	HEAD
 	branch-in-other
 	branch-in-remote
 	main-in-other
@@ -904,6 +911,7 @@ test_expect_success '__git_refs - after --opt=' '
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	matching-tag
@@ -919,6 +927,7 @@ test_expect_success '__git_refs - after --opt= - full refs' '
 	cat >expected <<-EOF &&
 	refs/heads/main
 	refs/heads/matching-branch
+	refs/remotes/other/HEAD
 	refs/remotes/other/branch-in-other
 	refs/remotes/other/main-in-other
 	refs/tags/matching-tag
@@ -935,6 +944,7 @@ test_expect_success '__git refs - excluding refs' '
 	^HEAD
 	^main
 	^matching-branch
+	^other/HEAD
 	^other/branch-in-other
 	^other/main-in-other
 	^matching-tag
@@ -950,6 +960,7 @@ test_expect_success '__git refs - excluding full refs' '
 	cat >expected <<-EOF &&
 	^refs/heads/main
 	^refs/heads/matching-branch
+	^refs/remotes/other/HEAD
 	^refs/remotes/other/branch-in-other
 	^refs/remotes/other/main-in-other
 	^refs/tags/matching-tag
@@ -975,6 +986,7 @@ test_expect_success '__git_refs - do not filter refs unless told so' '
 	main
 	matching-branch
 	matching/branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	other/matching/branch-in-other
@@ -1095,6 +1107,7 @@ test_expect_success '__git_complete_refs - simple' '
 	HEAD Z
 	main Z
 	matching-branch Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	matching-tag Z
@@ -1123,6 +1136,7 @@ test_expect_success '__git_complete_refs - matching' '
 test_expect_success '__git_complete_refs - remote' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main-in-other Z
 	EOF
@@ -1139,9 +1153,11 @@ test_expect_success '__git_complete_refs - track' '
 	HEAD Z
 	main Z
 	matching-branch Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	matching-tag Z
+	HEAD Z
 	branch-in-other Z
 	main-in-other Z
 	EOF
@@ -1184,6 +1200,7 @@ test_expect_success '__git_complete_refs - suffix' '
 	HEAD.
 	main.
 	matching-branch.
+	other/HEAD.
 	other/branch-in-other.
 	other/main-in-other.
 	matching-tag.
@@ -1199,6 +1216,7 @@ test_expect_success '__git_complete_refs - suffix' '
 test_expect_success '__git_complete_fetch_refspecs - simple' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	HEAD:HEAD Z
+	HEAD:HEAD Z
 	branch-in-other:branch-in-other Z
 	main-in-other:main-in-other Z
 	EOF
@@ -1225,6 +1243,7 @@ test_expect_success '__git_complete_fetch_refspecs - matching' '
 test_expect_success '__git_complete_fetch_refspecs - prefix' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	+HEAD:HEAD Z
+	+HEAD:HEAD Z
 	+branch-in-other:branch-in-other Z
 	+main-in-other:main-in-other Z
 	EOF
@@ -1289,6 +1308,7 @@ test_expect_success '__git_complete_worktree_paths with -C' '
 
 test_expect_success 'git switch - with no options, complete local branches and unique remote branch names for DWIM logic' '
 	test_completion "git switch " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1435,11 +1455,13 @@ test_expect_success 'git-bisect - existing view subcommand is recognized and ena
 test_expect_success 'git checkout - completes refs and unique remote branches for DWIM' '
 	test_completion "git checkout " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1461,6 +1483,7 @@ test_expect_success 'git switch - with GIT_COMPLETION_CHECKOUT_NO_GUESS=1, compl
 
 test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_GUESS=1, complete local branches and unique remote names for DWIM logic' '
 	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git switch --guess " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1470,6 +1493,7 @@ test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_G
 
 test_expect_success 'git switch - a later --guess overrides previous --no-guess, complete local and remote unique branches for DWIM' '
 	test_completion "git switch --no-guess --guess " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1490,6 +1514,7 @@ test_expect_success 'git checkout - with GIT_COMPLETION_NO_GUESS=1 only complete
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1498,11 +1523,13 @@ test_expect_success 'git checkout - with GIT_COMPLETION_NO_GUESS=1 only complete
 test_expect_success 'git checkout - --guess overrides GIT_COMPLETION_NO_GUESS=1, complete refs and unique remote branches for DWIM' '
 	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git checkout --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1514,6 +1541,7 @@ test_expect_success 'git checkout - with --no-guess, only completes refs' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1522,11 +1550,13 @@ test_expect_success 'git checkout - with --no-guess, only completes refs' '
 test_expect_success 'git checkout - a later --guess overrides previous --no-guess, complete refs and unique remote branches for DWIM' '
 	test_completion "git checkout --no-guess --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1538,6 +1568,7 @@ test_expect_success 'git checkout - a later --no-guess overrides previous --gues
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1550,6 +1581,7 @@ test_expect_success 'git checkout - with checkout.guess = false, only completes
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1559,11 +1591,13 @@ test_expect_success 'git checkout - with checkout.guess = true, completes refs a
 	test_config checkout.guess true &&
 	test_completion "git checkout " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1573,11 +1607,13 @@ test_expect_success 'git checkout - a later --guess overrides previous checkout.
 	test_config checkout.guess false &&
 	test_completion "git checkout --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1590,6 +1626,7 @@ test_expect_success 'git checkout - a later --no-guess overrides previous checko
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1601,6 +1638,7 @@ test_expect_success 'git switch - with --detach, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1612,6 +1650,7 @@ test_expect_success 'git checkout - with --detach, complete only references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1783,6 +1822,7 @@ test_expect_success 'git switch - with -d, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1794,6 +1834,7 @@ test_expect_success 'git checkout - with -d, complete only references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1801,10 +1842,12 @@ test_expect_success 'git checkout - with -d, complete only references' '
 
 test_expect_success 'git switch - with --track, complete only remote branches' '
 	test_completion "git switch --track " <<-\EOF &&
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
 	test_completion "git switch -t " <<-\EOF
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1812,10 +1855,12 @@ test_expect_success 'git switch - with --track, complete only remote branches' '
 
 test_expect_success 'git checkout - with --track, complete only remote branches' '
 	test_completion "git checkout --track " <<-\EOF &&
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
 	test_completion "git checkout -t " <<-\EOF
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1834,6 +1879,7 @@ test_expect_success 'git checkout - with --no-track, complete only local referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1845,6 +1891,7 @@ test_expect_success 'git switch - with -c, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1856,6 +1903,7 @@ test_expect_success 'git switch - with -C, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1867,6 +1915,7 @@ test_expect_success 'git switch - with -c and --track, complete all references'
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1878,6 +1927,7 @@ test_expect_success 'git switch - with -C and --track, complete all references'
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1889,6 +1939,7 @@ test_expect_success 'git switch - with -c and --no-track, complete all reference
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1900,6 +1951,7 @@ test_expect_success 'git switch - with -C and --no-track, complete all reference
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1911,6 +1963,7 @@ test_expect_success 'git checkout - with -b, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1922,6 +1975,7 @@ test_expect_success 'git checkout - with -B, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1933,6 +1987,7 @@ test_expect_success 'git checkout - with -b and --track, complete all references
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1944,6 +1999,7 @@ test_expect_success 'git checkout - with -B and --track, complete all references
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1955,6 +2011,7 @@ test_expect_success 'git checkout - with -b and --no-track, complete all referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1966,6 +2023,7 @@ test_expect_success 'git checkout - with -B and --no-track, complete all referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1973,6 +2031,7 @@ test_expect_success 'git checkout - with -B and --no-track, complete all referen
 
 test_expect_success 'git switch - for -c, complete local branches and unique remote branches' '
 	test_completion "git switch -c " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1982,6 +2041,7 @@ test_expect_success 'git switch - for -c, complete local branches and unique rem
 
 test_expect_success 'git switch - for -C, complete local branches and unique remote branches' '
 	test_completion "git switch -C " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2019,6 +2079,7 @@ test_expect_success 'git switch - for -C with --no-track, complete local branche
 
 test_expect_success 'git checkout - for -b, complete local branches and unique remote branches' '
 	test_completion "git checkout -b " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2028,6 +2089,7 @@ test_expect_success 'git checkout - for -b, complete local branches and unique r
 
 test_expect_success 'git checkout - for -B, complete local branches and unique remote branches' '
 	test_completion "git checkout -B " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2065,6 +2127,7 @@ test_expect_success 'git checkout - for -B with --no-track, complete local branc
 
 test_expect_success 'git switch - with --orphan completes local branch names and unique remote branch names' '
 	test_completion "git switch --orphan " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2080,6 +2143,7 @@ test_expect_success 'git switch - --orphan with branch already provided complete
 
 test_expect_success 'git checkout - with --orphan completes local branch names and unique remote branch names' '
 	test_completion "git checkout --orphan " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2093,6 +2157,7 @@ test_expect_success 'git checkout - --orphan with branch already provided comple
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
-- 
2.47.0.298.g52a96ec17b
Bence Ferdinandy· Nov 22, 2024, 12:28 UTC · re: Bence Ferdinandy · lore

[PATCH v15 00/10] set-head/fetch remote/HEAD updates

2/10: use test_must_fail and test_when_finished 3/10: whitespace fix

Bence Ferdinandy (10):
  t/t5505-remote: set default branch to main
  t/t5505-remote: test failure of set-head
  refs: standardize output of refs_read_symbolic_ref
  refs: atomically record overwritten ref in update_symref
  remote set-head: refactor for readability
  remote set-head: better output for --auto
  refs: add TRANSACTION_CREATE_EXISTS error
  refs: add create_only option to refs_update_symref_extended
  fetch: set remote/HEAD if it does not exist
  fetch set_head: handle mirrored bare repositories
 builtin/fetch.c                  |  74 +++++++++++++++++++++
 builtin/remote.c                 |  72 +++++++++++++++------
 refs.c                           |  47 ++++++++++++--
 refs.h                           |  19 +++++-
 refs/files-backend.c             |  31 +++++----
 refs/refs-internal.h             |   5 ++
 refs/reftable-backend.c          |  12 ++--
 t/t4207-log-decoration-colors.sh |   3 +-
 t/t5505-remote.sh                | 107 +++++++++++++++++++++++++++++--
 t/t5510-fetch.sh                 |  24 +++++++
 t/t5512-ls-remote.sh             |   2 +
 t/t5514-fetch-multiple.sh        |  17 ++++-
 t/t5516-fetch-push.sh            |   3 +-
 t/t5527-fetch-odd-refs.sh        |   3 +-
 t/t7900-maintenance.sh           |   3 +-
 t/t9210-scalar.sh                |   5 +-
 t/t9211-scalar-clone.sh          |   6 +-
 t/t9902-completion.sh            |  65 +++++++++++++++++++
 18 files changed, 439 insertions(+), 59 deletions(-)
Range-diff against v14:
 1:  49cfd222d5 =  1:  2e1d001d1a t/t5505-remote: set default branch to main
 2:  54bf9c7fff !  2:  d24e62035a t/t5505-remote: test failure of set-head
    @@ Commit message
      ## Notes ##
         v14: new patch
     
    +    v15: - use test_must_fail and test_when_finished
    +
      ## builtin/remote.c ##
     @@ builtin/remote.c: static int set_head(int argc, const char **argv, const char *prefix)
      		if (!refs_ref_exists(get_main_ref_store(the_repository), buf2.buf))
    @@ t/t5505-remote.sh: test_expect_success 'set-head --auto' '
      '
      
     +test_expect_success REFFILES 'set-head --auto failure' '
    ++	test_when_finished "rm -f test/.git/refs/remotes/origin/HEAD.lock" &&
     +	(
     +		cd test &&
     +		touch .git/refs/remotes/origin/HEAD.lock &&
    -+		git remote set-head --auto origin 2>errormsg ||
    -+		tail -n1 errormsg >output &&
    -+		rm .git/refs/remotes/origin/HEAD.lock &&
    ++		test_must_fail git remote set-head --auto origin 2>err &&
    ++		tail -n1 err >output &&
     +		echo "error: Could not set up refs/remotes/origin/HEAD" >expect &&
     +		test_cmp expect output
     +	)
 3:  c95362236e !  3:  1218d521e5 refs: standardize output of refs_read_symbolic_ref
    @@ Notes
              - rewrote comment in refs-internal.h to point to the one in refs.h
              - created NOT_A_SYMREF=-2 constant
     
    +    v15: fix whitespace issues
    +
      ## refs.h ##
     @@ refs.h: int refs_read_ref_full(struct ref_store *refs, const char *refname,
      
    @@ refs/reftable-backend.c: static int reftable_be_read_symbolic_ref(struct ref_sto
      
      	ret = reftable_stack_read_ref(stack, refname, &ref);
     -	if (ret == 0 && ref.value_type == REFTABLE_REF_SYMREF)
    -+        if (ret)
    -+                ret = -1;
    -+        else if (ref.value_type == REFTABLE_REF_SYMREF)
    ++	if (ret)
    ++		ret = -1;
    ++	else if (ref.value_type == REFTABLE_REF_SYMREF)
      		strbuf_addstr(referent, ref.value.symref);
    --	else
    + 	else
     -		ret = -1;
    -+        else
    -+                ret = NOT_A_SYMREF;
    ++		ret = NOT_A_SYMREF;
      
      	reftable_ref_record_release(&ref);
      	return ret;
 4:  21533e0e6b !  4:  4d5f3f4f01 refs: atomically record overwritten ref in update_symref
    @@ Notes
              - the returned error code by the above was also incorrect and now
                is -2 (and uses NOT_A_SYMREF now)
     
    +    v15: no change
    +
      ## refs.c ##
     @@ refs.c: int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
      
 5:  2455768c24 !  5:  6f2cb51b0b remote set-head: refactor for readability
    @@ Notes
     
         v14: no change
     
    +    v15: no change
    +
      ## builtin/remote.c ##
     @@ builtin/remote.c: static int show(int argc, const char **argv, const char *prefix)
      static int set_head(int argc, const char **argv, const char *prefix)
 6:  396537a598 !  6:  ca8687bae1 remote set-head: better output for --auto
    @@ Notes
         v14: - fixed badly named variable
              - fixed not reporting errors correctly
     
    +    v15: no change
    +
      ## builtin/remote.c ##
     @@ builtin/remote.c: static int show(int argc, const char **argv, const char *prefix)
      	return result;
 7:  05e97155d1 !  7:  45bcb33b52 refs: add TRANSACTION_CREATE_EXISTS error
    @@ Notes
     
         v14: no change
     
    +    v15: no change
    +
      ## refs.h ##
     @@ refs.h: int ref_transaction_verify(struct ref_transaction *transaction,
      
 8:  3ea8b1b8b6 !  8:  a8a522a089 refs: add create_only option to refs_update_symref_extended
    @@ Notes
     
         v14: no change
     
    +    v15: no change
    +
      ## builtin/remote.c ##
     @@ builtin/remote.c: static int set_head(int argc, const char **argv, const char *prefix)
      		goto cleanup;
 9:  287c25b801 !  9:  ff294461d8 fetch: set remote/HEAD if it does not exist
    @@ Notes
         created with an opt out, and the configuration for this will be added in
         a later patch
     
    +    v15: no change
    +
      ## builtin/fetch.c ##
     @@ builtin/fetch.c: static int backfill_tags(struct display_state *display_state,
      	return retcode;
10:  05d8526399 ! 10:  0e96260ab6 fetch set_head: handle mirrored bare repositories
    @@ Notes
     
         v14: no change
     
    +    v15: no change
    +
      ## builtin/fetch.c ##
     @@ builtin/fetch.c: static const char *strip_refshead(const char *name){
      
-- 
2.47.0.298.g52a96ec17b
Bence Ferdinandy· Nov 22, 2024, 12:28 UTC · re: Bence Ferdinandy · lore

[PATCH v15 01/10] t/t5505-remote: set default branch to main

Consider the bare repository called "mirror" in the test. Running `git remote add --mirror -f origin ../one` will not change HEAD, consequently if init.defaultBranch is not the same as what HEAD in the remote ("one"), HEAD in "mirror" will be pointing to a non-existent reference. Hence if "mirror" is used as a remote by yet another repository, ls-remote will not show HEAD. On the other hand, if init.defaultBranch happens to match HEAD in "one", then ls-remote will show HEAD.

Since the "ci/run-build-and-tests.sh" script globally exports GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main for some (but not all) jobs, there may be a drift in some tests between how the test repositories are set up in the CI and during local testing, if the test itself uses "master" as default instead of "main". In particular, this happens in t5505-remote.sh. This issue does not manifest currently, as the test does not do any remote HEAD manipulation where this would come up, but should such things be added, a locally passing test would break the CI and vice-versa.

Set GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main in t5505-remote to be consistent with the CI.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v9: - new patch
        - a bandaid for the CI issue noticed by Taylor (cf:
          https://lore.kernel.org/git/Zw8IKyPkG0Hr6%2F5t@nand.local/), but
          see
          https://lore.kernel.org/git/D4ZAELFWJMKN.S88LJ6YK31LZ@ferdinandy.com/ for the root cause in detail
    
    v10: no change
    
    v11: no change
    
    v12: added forgotten sign-off
    
    v13: commit message udpated to be more precise
    
    v14: made explicit in the commit message that the default branch is only
         set for some CI jobs
    
    v15: no change
 t/t5505-remote.sh | 3 +++
 1 file changed, 3 insertions(+)
Show changes to t/t5505-remote.sh +3 −0
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 532035933f..9b50276646 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -2,6 +2,9 @@
 
 test_description='git remote porcelain-ish'
 
+GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
+export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
+
 TEST_PASSES_SANITIZE_LEAK=true
 . ./test-lib.sh
 
-- 
2.47.0.298.g52a96ec17b
Bence Ferdinandy· Nov 22, 2024, 12:28 UTC · re: Bence Ferdinandy · lore

[PATCH v15 03/10] refs: standardize output of refs_read_symbolic_ref

When the symbolic reference we want to read with refs_read_symbolic_ref is actually not a symbolic reference, the files and the reftable backends return different values (1 and -1 respectively). Standardize the returned values so that 0 is success, -1 is a generic error and -2 is that the reference was actually non-symbolic.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v13: new patch
    
    v14: - simplified the logic in reftables backend (thanks Junio)
         - update the comment in refs.h (thanks Patrick)
         - rewrote comment in refs-internal.h to point to the one in refs.h
         - created NOT_A_SYMREF=-2 constant
    
    v15: fix whitespace issues
 refs.h                  | 11 +++++++++++
 refs/files-backend.c    |  7 +++----
 refs/refs-internal.h    |  5 +++++
 refs/reftable-backend.c |  6 ++++--
 4 files changed, 23 insertions(+), 6 deletions(-)
Show changes to 4 files +23 −6

refs.h, refs/files-backend.c, refs/refs-internal.h, refs/reftable-backend.c

diff --git a/refs.h b/refs.h
index 108dfc93b3..22c2b45b8b 100644
--- a/refs.h
+++ b/refs.h
@@ -83,6 +83,17 @@ int refs_read_ref_full(struct ref_store *refs, const char *refname,
 
 int refs_read_ref(struct ref_store *refs, const char *refname, struct object_id *oid);
 
+#define NOT_A_SYMREF -2
+
+/*
+ * Read the symbolic ref named "refname" and write its immediate referent into
+ * the provided buffer. Referent is left empty if "refname" is not a symbolic
+ * ref. It does not resolve the symbolic reference recursively in case the
+ * target is also a symbolic ref.
+ *
+ * Returns 0 on success, -2 if the "refname" is not a symbolic ref,
+ * -1 otherwise.
+ */
 int refs_read_symbolic_ref(struct ref_store *ref_store, const char *refname,
 			   struct strbuf *referent);
 
diff --git a/refs/files-backend.c b/refs/files-backend.c
index 0824c0b8a9..4cc43c32f2 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -596,10 +596,9 @@ static int files_read_symbolic_ref(struct ref_store *ref_store, const char *refn
 	unsigned int type;
 
 	ret = read_ref_internal(ref_store, refname, &oid, referent, &type, &failure_errno, 1);
-	if (ret)
-		return ret;
-
-	return !(type & REF_ISSYMREF);
+	if (!ret && !(type & REF_ISSYMREF))
+		return NOT_A_SYMREF;
+	return ret;
 }
 
 int parse_loose_ref_contents(const struct git_hash_algo *algop,
diff --git a/refs/refs-internal.h b/refs/refs-internal.h
index 2313c830d8..1399fee61c 100644
--- a/refs/refs-internal.h
+++ b/refs/refs-internal.h
@@ -673,6 +673,11 @@ struct ref_storage_be {
 
 	ref_iterator_begin_fn *iterator_begin;
 	read_raw_ref_fn *read_raw_ref;
+
+	/*
+	 * Please refer to `refs_read_symbolic_ref()` for the expected
+	 * behaviour.
+	 */
 	read_symbolic_ref_fn *read_symbolic_ref;
 
 	reflog_iterator_begin_fn *reflog_iterator_begin;
diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
index 38eb14d591..e8afcda1f9 100644
--- a/refs/reftable-backend.c
+++ b/refs/reftable-backend.c
@@ -830,10 +830,12 @@ static int reftable_be_read_symbolic_ref(struct ref_store *ref_store,
 		return ret;
 
 	ret = reftable_stack_read_ref(stack, refname, &ref);
-	if (ret == 0 && ref.value_type == REFTABLE_REF_SYMREF)
+	if (ret)
+		ret = -1;
+	else if (ref.value_type == REFTABLE_REF_SYMREF)
 		strbuf_addstr(referent, ref.value.symref);
 	else
-		ret = -1;
+		ret = NOT_A_SYMREF;
 
 	reftable_ref_record_release(&ref);
 	return ret;
-- 
2.47.0.298.g52a96ec17b
Bence Ferdinandy· Nov 22, 2024, 12:28 UTC · re: Bence Ferdinandy · lore

[PATCH v15 02/10] t/t5505-remote: test failure of set-head

The test coverage was missing a test for the failure branch of remote set-head auto's output. Add the missing text and while we are at it, correct a small grammatical mistake in the error's output ("setup" is the noun, "set up" is the verb).

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v14: new patch
    
    v15: - use test_must_fail and test_when_finished
 builtin/remote.c  |  2 +-
 t/t5505-remote.sh | 12 ++++++++++++
 2 files changed, 13 insertions(+), 1 deletion(-)
Show changes to 2 files +13 −1

builtin/remote.c, t/t5505-remote.sh

diff --git a/builtin/remote.c b/builtin/remote.c
index 9093600965..8bca3fb04e 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1445,7 +1445,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		if (!refs_ref_exists(get_main_ref_store(the_repository), buf2.buf))
 			result |= error(_("Not a valid ref: %s"), buf2.buf);
 		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head"))
-			result |= error(_("Could not setup %s"), buf.buf);
+			result |= error(_("Could not set up %s"), buf.buf);
 		else if (opt_a)
 			printf("%s/HEAD set to %s\n", argv[0], head_name);
 		free(head_name);
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 9b50276646..61e3ecc1af 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -432,6 +432,18 @@ test_expect_success 'set-head --auto' '
 	)
 '
 
+test_expect_success REFFILES 'set-head --auto failure' '
+	test_when_finished "rm -f test/.git/refs/remotes/origin/HEAD.lock" &&
+	(
+		cd test &&
+		touch .git/refs/remotes/origin/HEAD.lock &&
+		test_must_fail git remote set-head --auto origin 2>err &&
+		tail -n1 err >output &&
+		echo "error: Could not set up refs/remotes/origin/HEAD" >expect &&
+		test_cmp expect output
+	)
+'
+
 test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
 	(
 		cd test &&
-- 
2.47.0.298.g52a96ec17b
Bence Ferdinandy· Nov 22, 2024, 12:28 UTC · re: Bence Ferdinandy · lore

[PATCH v15 04/10] refs: atomically record overwritten ref in update_symref

When updating a symref with update_symref it's currently not possible to know for sure what was the previous value that was overwritten. Extend refs_update_symref under a new function name, to record the value after the ref has been locked if the caller of refs_update_symref_extended requests it via a new variable in the function call. Make the return value of the function notify the caller, if the previous value was actually not a symbolic reference. Keep the original refs_update_symref function with the same signature, but now as a wrapper around refs_update_symref_extended.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    
    v5: - added before_target to reftables backend
        - added an extra safety check for transaction's existence in refs.c
    
    v6: - no change
    
    v7: - remove the whole before_target concept from the backends and
          handle checking it in refs.c instead (thanks Karthik)
        - rename the before_target to referent which is how the same concept
          is called in the backends
        - change commit prefix to be more in line with project standards
    
    v8: no change
    
    v9: - instead of adding parameters to refs_update_symref, rename what
          was in v8 as refs_update_symref_extended and make refs_update_symref
          a wrapper for that. This significantly reduces the number of files
          that need to be touched, and avoids adding a lot of dummy NULL-s
          in unrelated places.
    
    v10: no change
    
    v11: no change
    
    v12: no change
    
    v13: if referent is a non-symbolic ref, record the hash in referent and
        signal this with a return value of -1
    
    v14: - mistakes were made: trying to read the referent should NOT lead to
         an actual error, even if it doesn't work out in the end, that part of
         the code should only set the return to -2 or not touch it
         - the returned error code by the above was also incorrect and now
           is -2 (and uses NOT_A_SYMREF now)
    
    v15: no change
 refs.c | 22 ++++++++++++++++++++--
 refs.h |  4 ++++
 2 files changed, 24 insertions(+), 2 deletions(-)
Show changes to 2 files +24 −2

refs.c, refs.h

diff --git a/refs.c b/refs.c
index 5f729ed412..d80efd58f0 100644
--- a/refs.c
+++ b/refs.c
@@ -2115,6 +2115,13 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
 
 int refs_update_symref(struct ref_store *refs, const char *ref,
 		       const char *target, const char *logmsg)
+{
+	return refs_update_symref_extended(refs, ref, target, logmsg, NULL);
+}
+
+int refs_update_symref_extended(struct ref_store *refs, const char *ref,
+		       const char *target, const char *logmsg,
+		       struct strbuf *referent)
 {
 	struct ref_transaction *transaction;
 	struct strbuf err = STRBUF_INIT;
@@ -2125,10 +2132,22 @@ int refs_update_symref(struct ref_store *refs, const char *ref,
 	    ref_transaction_update(transaction, ref, NULL, NULL,
 				   target, NULL, REF_NO_DEREF,
 				   logmsg, &err) ||
-	    ref_transaction_commit(transaction, &err)) {
+	    ref_transaction_prepare(transaction, &err)) {
 		ret = error("%s", err.buf);
+		goto cleanup;
 	}
+	if (referent && refs_read_symbolic_ref(refs, ref, referent) == NOT_A_SYMREF) {
+		struct object_id oid;
+		if (!refs_read_ref(refs, ref, &oid)) {
+			strbuf_addstr(referent, oid_to_hex(&oid));
+			ret = NOT_A_SYMREF;
+		}
+	}
+
+	if (ref_transaction_commit(transaction, &err))
+		ret = error("%s", err.buf);
 
+cleanup:
 	strbuf_release(&err);
 	if (transaction)
 		ref_transaction_free(transaction);
@@ -2948,4 +2967,3 @@ int ref_update_expects_existing_old_ref(struct ref_update *update)
 	return (update->flags & REF_HAVE_OLD) &&
 		(!is_null_oid(&update->old_oid) || update->old_target);
 }
-
diff --git a/refs.h b/refs.h
index 22c2b45b8b..5c46ac9f34 100644
--- a/refs.h
+++ b/refs.h
@@ -584,6 +584,10 @@ int refs_copy_existing_ref(struct ref_store *refs, const char *oldref,
 int refs_update_symref(struct ref_store *refs, const char *refname,
 		       const char *target, const char *logmsg);
 
+int refs_update_symref_extended(struct ref_store *refs, const char *refname,
+		       const char *target, const char *logmsg,
+		       struct strbuf *referent);
+
 enum action_on_err {
 	UPDATE_REFS_MSG_ON_ERR,
 	UPDATE_REFS_DIE_ON_ERR,
-- 
2.47.0.298.g52a96ec17b
Bence Ferdinandy· Nov 22, 2024, 12:28 UTC · re: Bence Ferdinandy · lore

[PATCH v15 05/10] remote set-head: refactor for readability

Make two different readability refactors:
Rename strbufs "buf" and "buf2" to something more explanatory.

Instead of calling get_main_ref_store(the_repository) multiple times, call it once and store the result in a new refs variable. Although this change probably offers some performance benefits, the main purpose is to shorten the line lengths of function calls using this variable.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v5: new patch (split from the next patch as a preparatory step)
    
    v6: no change
    
    v7: - change commit prefix to be more in line with project standards
    
    v8: no change
    
    v9: - further improve readability by renaming buf, and buf2 consistently
          with how patch 6 was already done
    
    v10: no change
    
    v11: no change
    
    v12: no change
    
    v13: more precise wording for commit message
    
    v14: no change
    
    v15: no change
 builtin/remote.c | 23 ++++++++++++-----------
 1 file changed, 12 insertions(+), 11 deletions(-)
Show changes to builtin/remote.c +12 −11
diff --git a/builtin/remote.c b/builtin/remote.c
index 8bca3fb04e..47ca650de8 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1402,8 +1402,9 @@ static int show(int argc, const char **argv, const char *prefix)
 static int set_head(int argc, const char **argv, const char *prefix)
 {
 	int i, opt_a = 0, opt_d = 0, result = 0;
-	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT;
 	char *head_name = NULL;
+	struct ref_store *refs = get_main_ref_store(the_repository);
 
 	struct option options[] = {
 		OPT_BOOL('a', "auto", &opt_a,
@@ -1415,7 +1416,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 	argc = parse_options(argc, argv, prefix, options,
 			     builtin_remote_sethead_usage, 0);
 	if (argc)
-		strbuf_addf(&buf, "refs/remotes/%s/HEAD", argv[0]);
+		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", argv[0]);
 
 	if (!opt_a && !opt_d && argc == 2) {
 		head_name = xstrdup(argv[1]);
@@ -1434,25 +1435,25 @@ static int set_head(int argc, const char **argv, const char *prefix)
 			head_name = xstrdup(states.heads.items[0].string);
 		free_remote_ref_states(&states);
 	} else if (opt_d && !opt_a && argc == 1) {
-		if (refs_delete_ref(get_main_ref_store(the_repository), NULL, buf.buf, NULL, REF_NO_DEREF))
-			result |= error(_("Could not delete %s"), buf.buf);
+		if (refs_delete_ref(refs, NULL, b_head.buf, NULL, REF_NO_DEREF))
+			result |= error(_("Could not delete %s"), b_head.buf);
 	} else
 		usage_with_options(builtin_remote_sethead_usage, options);
 
 	if (head_name) {
-		strbuf_addf(&buf2, "refs/remotes/%s/%s", argv[0], head_name);
+		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", argv[0], head_name);
 		/* make sure it's valid */
-		if (!refs_ref_exists(get_main_ref_store(the_repository), buf2.buf))
-			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head"))
-			result |= error(_("Could not set up %s"), buf.buf);
+		if (!refs_ref_exists(refs, b_remote_head.buf))
+			result |= error(_("Not a valid ref: %s"), b_remote_head.buf);
+		else if (refs_update_symref(refs, b_head.buf, b_remote_head.buf, "remote set-head"))
+			result |= error(_("Could not set up %s"), b_head.buf);
 		else if (opt_a)
 			printf("%s/HEAD set to %s\n", argv[0], head_name);
 		free(head_name);
 	}
 
-	strbuf_release(&buf);
-	strbuf_release(&buf2);
+	strbuf_release(&b_head);
+	strbuf_release(&b_remote_head);
 	return result;
 }
 
-- 
2.47.0.298.g52a96ec17b
Bence Ferdinandy· Nov 22, 2024, 12:28 UTC · re: Bence Ferdinandy · lore

[PATCH v15 07/10] refs: add TRANSACTION_CREATE_EXISTS error

Currently there is only one special error for transaction, for when there is a naming conflict, all other errors are dumped under a generic error. Add a new special error case for when the caller requests the reference to be updated only when it does not yet exist and the reference actually does exist.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    v5: no change
    v6: no change
    
    v7: - change commit prefix to be more in line with project standards
        - changed error checking to Karthik's suggestion
    
    v8: no change
    
    v9: - no change
    
    v10: no change
    
    v11: no change
    
    v12: no change
    
    v13: no change
    
    v14: no change
    
    v15: no change
 refs.h                  |  4 +++-
 refs/files-backend.c    | 24 ++++++++++++++++--------
 refs/reftable-backend.c |  6 ++++--
 3 files changed, 23 insertions(+), 11 deletions(-)
Show changes to 3 files +23 −11

refs.h, refs/files-backend.c, refs/reftable-backend.c

diff --git a/refs.h b/refs.h
index 5c46ac9f34..b243739e4b 100644
--- a/refs.h
+++ b/refs.h
@@ -773,8 +773,10 @@ int ref_transaction_verify(struct ref_transaction *transaction,
 
 /* Naming conflict (for example, the ref names A and A/B conflict). */
 #define TRANSACTION_NAME_CONFLICT -1
+/* When only creation was requested, but the ref already exists. */
+#define TRANSACTION_CREATE_EXISTS -2
 /* All other errors. */
-#define TRANSACTION_GENERIC_ERROR -2
+#define TRANSACTION_GENERIC_ERROR -3
 
 /*
  * Perform the preparatory stages of committing `transaction`. Acquire
diff --git a/refs/files-backend.c b/refs/files-backend.c
index 4cc43c32f2..23ae74089d 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -2501,14 +2501,18 @@ static int split_symref_update(struct ref_update *update,
 static int check_old_oid(struct ref_update *update, struct object_id *oid,
 			 struct strbuf *err)
 {
+	int ret = TRANSACTION_GENERIC_ERROR;
+
 	if (!(update->flags & REF_HAVE_OLD) ||
 		   oideq(oid, &update->old_oid))
 		return 0;
 
-	if (is_null_oid(&update->old_oid))
+	if (is_null_oid(&update->old_oid)) {
 		strbuf_addf(err, "cannot lock ref '%s': "
 			    "reference already exists",
 			    ref_update_original_update_refname(update));
+		ret = TRANSACTION_CREATE_EXISTS;
+	}
 	else if (is_null_oid(oid))
 		strbuf_addf(err, "cannot lock ref '%s': "
 			    "reference is missing but expected %s",
@@ -2521,7 +2525,7 @@ static int check_old_oid(struct ref_update *update, struct object_id *oid,
 			    oid_to_hex(oid),
 			    oid_to_hex(&update->old_oid));
 
-	return -1;
+	return ret;
 }
 
 /*
@@ -2601,9 +2605,11 @@ static int lock_ref_for_update(struct files_ref_store *refs,
 					ret = TRANSACTION_GENERIC_ERROR;
 					goto out;
 				}
-			} else if  (check_old_oid(update, &lock->old_oid, err)) {
-				ret = TRANSACTION_GENERIC_ERROR;
-				goto out;
+			} else {
+				ret = check_old_oid(update, &lock->old_oid, err);
+				if  (ret) {
+					goto out;
+				}
 			}
 		} else {
 			/*
@@ -2634,9 +2640,11 @@ static int lock_ref_for_update(struct files_ref_store *refs,
 				    update->old_target);
 			ret = TRANSACTION_GENERIC_ERROR;
 			goto out;
-		} else if  (check_old_oid(update, &lock->old_oid, err)) {
-			ret = TRANSACTION_GENERIC_ERROR;
-			goto out;
+		} else {
+			ret = check_old_oid(update, &lock->old_oid, err);
+			if  (ret) {
+				goto out;
+			}
 		}
 
 		/*
diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
index e8afcda1f9..965c17d795 100644
--- a/refs/reftable-backend.c
+++ b/refs/reftable-backend.c
@@ -1208,10 +1208,13 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 				goto done;
 			}
 		} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {
-			if (is_null_oid(&u->old_oid))
+			ret = TRANSACTION_NAME_CONFLICT;
+			if (is_null_oid(&u->old_oid)) {
 				strbuf_addf(err, _("cannot lock ref '%s': "
 						   "reference already exists"),
 					    ref_update_original_update_refname(u));
+				ret = TRANSACTION_CREATE_EXISTS;
+			}
 			else if (is_null_oid(&current_oid))
 				strbuf_addf(err, _("cannot lock ref '%s': "
 						   "reference is missing but expected %s"),
@@ -1223,7 +1226,6 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 					    ref_update_original_update_refname(u),
 					    oid_to_hex(&current_oid),
 					    oid_to_hex(&u->old_oid));
-			ret = -1;
 			goto done;
 		}
 
-- 
2.47.0.298.g52a96ec17b
Bence Ferdinandy· Nov 22, 2024, 12:28 UTC · re: Bence Ferdinandy · lore

[PATCH v15 06/10] remote set-head: better output for --auto

Currently, set-head --auto will print a message saying "remote/HEAD set to branch", which implies something was changed.

Change the output of --auto, so the output actually reflects what was done: a) set a previously unset HEAD, b) change HEAD because remote changed or c) no updates. As edge cases, if HEAD is changed from a previous symbolic reference that was not a remote branch, explicitly call attention to this fact, and also notify the user if the previous reference was not a symbolic reference.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v1-v2:
    
        was RFC in https://lore.kernel.org/git/20240910203835.2288291-1-bence@ferdinandy.com/
    
    v3:
    
        This patch was originally sent along when I thought set-head was
        going to be invoked by fetch, but the discussion on the RFC
        concluded that it should be not. This opened the possibility to make
        it more explicit.
    
        Note: although I feel both things the patch does are really just
        cosmetic, an argument could be made for breaking it into two, one
        for the no-op part and one for the --auto print update.
    
        Was sent in:
        https://lore.kernel.org/git/20240915221055.904107-1-bence@ferdinandy.com/
    
    v4:
        - changes are now handled atomically via the ref update transaction
        - outputs have changed along the lines of Junio's suggestion
        - minor refactor to set_head for improved legibility
    
    v5: - the minor refactor has been split out into its own patch
    
    v6: - fixed uninitialized prev_head
        - fixed case of unusual previous target
        - fixed a test that would have been actually broken at this patch
          (the output was only correct with the later update to fetch)
        - added 4 tests for the 4 output cases
    
    v7: - change commit prefix to be more in line with project standards
        - fixed tests to also work with the reftable backend
        - renamed report function, fixed style issue with checking buf len
        - fixed not releasing an strbuf
    
    v8: no change
    
    v9: - mark output strings in report_set_head_auto as translatable
        - rename buf_prev to b_local_head for consistency
        - use ${SQ} in tests instead of '\''
    
    v10: no change
    
    v11: no change
    
    v12: no change
    
    v13: added handling the edge case of previous remote/HEAD being
    a detached HEAD
    
    v14: - fixed badly named variable
         - fixed not reporting errors correctly
    
    v15: no change
 builtin/remote.c  | 59 +++++++++++++++++++++++++++++++++++---------
 t/t5505-remote.sh | 63 ++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 109 insertions(+), 13 deletions(-)
Show changes to 2 files +109 −13

builtin/remote.c, t/t5505-remote.sh

diff --git a/builtin/remote.c b/builtin/remote.c
index 47ca650de8..e1f15e68f4 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1399,10 +1399,38 @@ static int show(int argc, const char **argv, const char *prefix)
 	return result;
 }
 
+static void report_set_head_auto(const char *remote, const char *head_name,
+			struct strbuf *b_local_head, int was_detached) {
+	struct strbuf buf_prefix = STRBUF_INIT;
+	const char *prev_head = NULL;
+
+	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
+	skip_prefix(b_local_head->buf, buf_prefix.buf, &prev_head);
+
+	if (prev_head && !strcmp(prev_head, head_name))
+		printf(_("'%s/HEAD' is unchanged and points to '%s'\n"),
+			remote, head_name);
+	else if (prev_head)
+		printf(_("'%s/HEAD' has changed from '%s' and now points to '%s'\n"),
+			remote, prev_head, head_name);
+	else if (!b_local_head->len)
+		printf(_("'%s/HEAD' is now created and points to '%s'\n"),
+			remote, head_name);
+	else if (was_detached && b_local_head->len)
+		printf(_("'%s/HEAD' was detached at '%s' and now points to '%s'\n"),
+			remote, b_local_head->buf, head_name);
+	else
+		printf(_("'%s/HEAD' used to point to '%s' "
+			"(which is not a remote branch), but now points to '%s'\n"),
+			remote, b_local_head->buf, head_name);
+	strbuf_release(&buf_prefix);
+}
+
 static int set_head(int argc, const char **argv, const char *prefix)
 {
-	int i, opt_a = 0, opt_d = 0, result = 0;
-	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT;
+	int i, opt_a = 0, opt_d = 0, result = 0, was_detached;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
+		b_local_head = STRBUF_INIT;
 	char *head_name = NULL;
 	struct ref_store *refs = get_main_ref_store(the_repository);
 
@@ -1440,20 +1468,27 @@ static int set_head(int argc, const char **argv, const char *prefix)
 	} else
 		usage_with_options(builtin_remote_sethead_usage, options);
 
-	if (head_name) {
-		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", argv[0], head_name);
-		/* make sure it's valid */
-		if (!refs_ref_exists(refs, b_remote_head.buf))
-			result |= error(_("Not a valid ref: %s"), b_remote_head.buf);
-		else if (refs_update_symref(refs, b_head.buf, b_remote_head.buf, "remote set-head"))
-			result |= error(_("Could not set up %s"), b_head.buf);
-		else if (opt_a)
-			printf("%s/HEAD set to %s\n", argv[0], head_name);
-		free(head_name);
+	if (!head_name)
+		goto cleanup;
+	strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", argv[0], head_name);
+	if (!refs_ref_exists(refs, b_remote_head.buf)) {
+		result |= error(_("Not a valid ref: %s"), b_remote_head.buf);
+		goto cleanup;
+	}
+	was_detached = refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
+			"remote set-head", &b_local_head);
+	if (was_detached == -1) {
+		result |= error(_("Could not set up %s"), b_head.buf);
+		goto cleanup;
 	}
+	if (opt_a)
+		report_set_head_auto(argv[0], head_name, &b_local_head, was_detached);
 
+cleanup:
+	free(head_name);
 	strbuf_release(&b_head);
 	strbuf_release(&b_remote_head);
+	strbuf_release(&b_local_head);
 	return result;
 }
 
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 61e3ecc1af..d15b579c95 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -444,12 +444,63 @@ test_expect_success REFFILES 'set-head --auto failure' '
 	)
 '
 
+test_expect_success 'set-head --auto detects creation' '
+	(
+		cd test &&
+		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} is now created and points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto to update a non symbolic ref' '
+	(
+		cd test &&
+		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
+		git update-ref refs/remotes/origin/HEAD HEAD &&
+		HEAD=$(git log --pretty="%H") &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} was detached at ${SQ}${HEAD}${SQ} and now points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects no change' '
+	(
+		cd test &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} is unchanged and points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects change' '
+	(
+		cd test &&
+		git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/ahead &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} has changed from ${SQ}ahead${SQ} and now points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success 'set-head --auto detects strange ref' '
+	(
+		cd test &&
+		git symbolic-ref refs/remotes/origin/HEAD refs/heads/main &&
+		git remote set-head --auto origin >output &&
+		echo "${SQ}origin/HEAD${SQ} used to point to ${SQ}refs/heads/main${SQ} (which is not a remote branch), but now points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
 test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
 	(
 		cd test &&
 		git fetch two "refs/heads/*:refs/remotes/two/*" &&
 		git remote set-head --auto two >output 2>&1 &&
-		echo "two/HEAD set to main" >expect &&
+		echo "${SQ}two/HEAD${SQ} is now created and points to ${SQ}main${SQ}" >expect &&
 		test_cmp expect output
 	)
 '
@@ -468,6 +519,16 @@ test_expect_success 'set-head explicit' '
 	)
 '
 
+test_expect_success 'set-head --auto reports change' '
+	(
+		cd test &&
+		git remote set-head origin side2 &&
+		git remote set-head --auto origin >output 2>&1 &&
+		echo "${SQ}origin/HEAD${SQ} has changed from ${SQ}side2${SQ} and now points to ${SQ}main${SQ}" >expect &&
+		test_cmp expect output
+	)
+'
+
 cat >test/expect <<EOF
 Pruning origin
 URL: $(pwd)/one
-- 
2.47.0.298.g52a96ec17b
Bence Ferdinandy· Nov 22, 2024, 12:28 UTC · re: Bence Ferdinandy · lore

[PATCH v15 08/10] refs: add create_only option to refs_update_symref_extended

Allow the caller to specify that it only wants to update the symref if it does not already exist. Silently ignore the error from the transaction API if the symref already exists.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    v5: no change
    
    v6: - switched from bool to int for create_only
        - refactored logic in refs_update_symref with goto as layed out by
          Junio
    
    v7: - change commit prefix to be more in line with project standards
        - refactored code to accommodate changes in the first patch, but
          otherwise no change
    
    v8: no change
    
    v9: - no change (except for following through the
          refs_update_symref_extended refactoring)
    
    v10: no change
    
    v11: no change
    
    v12: no change
    
    v13: changes only due to changes in previous patch
    
    v14: no change
    
    v15: no change
 builtin/remote.c |  2 +-
 refs.c           | 33 ++++++++++++++++++++++++---------
 refs.h           |  2 +-
 3 files changed, 26 insertions(+), 11 deletions(-)
Show changes to 3 files +26 −11

builtin/remote.c, refs.c, refs.h

diff --git a/builtin/remote.c b/builtin/remote.c
index e1f15e68f4..4a8b2ef678 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1476,7 +1476,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		goto cleanup;
 	}
 	was_detached = refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
-			"remote set-head", &b_local_head);
+			"remote set-head", &b_local_head, 0);
 	if (was_detached == -1) {
 		result |= error(_("Could not set up %s"), b_head.buf);
 		goto cleanup;
diff --git a/refs.c b/refs.c
index d80efd58f0..2efa6bcc5c 100644
--- a/refs.c
+++ b/refs.c
@@ -2116,26 +2116,38 @@ int peel_iterated_oid(struct repository *r, const struct object_id *base, struct
 int refs_update_symref(struct ref_store *refs, const char *ref,
 		       const char *target, const char *logmsg)
 {
-	return refs_update_symref_extended(refs, ref, target, logmsg, NULL);
+	return refs_update_symref_extended(refs, ref, target, logmsg, NULL, 0);
 }
 
 int refs_update_symref_extended(struct ref_store *refs, const char *ref,
 		       const char *target, const char *logmsg,
-		       struct strbuf *referent)
+		       struct strbuf *referent, int create_only)
 {
 	struct ref_transaction *transaction;
 	struct strbuf err = STRBUF_INIT;
-	int ret = 0;
+	int ret = 0, prepret = 0;
 
 	transaction = ref_store_transaction_begin(refs, &err);
-	if (!transaction ||
-	    ref_transaction_update(transaction, ref, NULL, NULL,
-				   target, NULL, REF_NO_DEREF,
-				   logmsg, &err) ||
-	    ref_transaction_prepare(transaction, &err)) {
+	if (!transaction) {
+	error_return:
 		ret = error("%s", err.buf);
 		goto cleanup;
 	}
+	if (create_only) {
+		if (ref_transaction_create(transaction, ref, NULL, target,
+					   REF_NO_DEREF, logmsg, &err))
+			goto error_return;
+		prepret = ref_transaction_prepare(transaction, &err);
+		if (prepret && prepret != TRANSACTION_CREATE_EXISTS)
+			goto error_return;
+	} else {
+		if (ref_transaction_update(transaction, ref, NULL, NULL,
+					   target, NULL, REF_NO_DEREF,
+					   logmsg, &err) ||
+			ref_transaction_prepare(transaction, &err))
+			goto error_return;
+	}
+
 	if (referent && refs_read_symbolic_ref(refs, ref, referent) == NOT_A_SYMREF) {
 		struct object_id oid;
 		if (!refs_read_ref(refs, ref, &oid)) {
@@ -2144,8 +2156,11 @@ int refs_update_symref_extended(struct ref_store *refs, const char *ref,
 		}
 	}
 
+	if (prepret == TRANSACTION_CREATE_EXISTS)
+		goto cleanup;
+
 	if (ref_transaction_commit(transaction, &err))
-		ret = error("%s", err.buf);
+		goto error_return;
 
 cleanup:
 	strbuf_release(&err);
diff --git a/refs.h b/refs.h
index b243739e4b..be38377b1f 100644
--- a/refs.h
+++ b/refs.h
@@ -586,7 +586,7 @@ int refs_update_symref(struct ref_store *refs, const char *refname,
 
 int refs_update_symref_extended(struct ref_store *refs, const char *refname,
 		       const char *target, const char *logmsg,
-		       struct strbuf *referent);
+		       struct strbuf *referent, int create_only);
 
 enum action_on_err {
 	UPDATE_REFS_MSG_ON_ERR,
-- 
2.47.0.298.g52a96ec17b
Bence Ferdinandy· Nov 22, 2024, 12:28 UTC · re: Bence Ferdinandy · lore

[PATCH v15 10/10] fetch set_head: handle mirrored bare repositories

When adding a remote to bare repository with "git remote add --mirror",
running fetch will fail to update HEAD to the remote's HEAD, since it
does not know how to handle bare repositories. On the other hand HEAD
already has content, since "git init --bare" has already set HEAD to
whatever is the default branch set for the user. Unless this - by chance
- is the same as the remote's HEAD, HEAD will be pointing to a bad
symref. Teach set_head to handle bare repositories, by overwriting HEAD
so it mirrors the remote's HEAD.

Note, that in this case overriding the local HEAD reference is necessary, since HEAD will exist before fetch can be run, but this should not be an issue, since the whole purpose of --mirror is to be an exact mirror of the remote, so following any changes to HEAD makes sense.

Also note, that although "git remote set-head" also fails when trying to update the remote's locally tracked HEAD in a mirrored bare repository, the usage of the command does not make much sense after this patch: fetch will update the remote HEAD correctly, and setting it manually to something else is antithetical to the concept of mirroring.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    state:
        * last review: none
    
    v10: - new patch
         - handles the issue discovered in
           https://lore.kernel.org/git/D4ZAELFWJMKN.S88LJ6YK31LZ@ferdinandy.com/T/
    
    v11: no change
    
    v12: no change
    
    v13: properly print the previous head if it was detached
    
    v14: no change
    
    v15: no change
 builtin/fetch.c   | 16 +++++++++++-----
 t/t5505-remote.sh | 10 ++++++++++
 2 files changed, 21 insertions(+), 5 deletions(-)
Show changes to 2 files +21 −5

builtin/fetch.c, t/t5505-remote.sh

diff --git a/builtin/fetch.c b/builtin/fetch.c
index bbfaf50b63..2f416cf867 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1581,7 +1581,7 @@ static const char *strip_refshead(const char *name){
 
 static int set_head(const struct ref *remote_refs)
 {
-	int result = 0;
+	int result = 0, is_bare;
 	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT;
 	const char *remote = gtransport->remote->name;
 	char *head_name = NULL;
@@ -1613,15 +1613,21 @@ static int set_head(const struct ref *remote_refs)
 
 	if (!head_name)
 		goto cleanup;
-	strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
-	strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote, head_name);
+	is_bare = is_bare_repository();
+	if (is_bare) {
+		strbuf_addstr(&b_head, "HEAD");
+		strbuf_addf(&b_remote_head, "refs/heads/%s", head_name);
+	} else {
+		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
+		strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote, head_name);
+	}
 		/* make sure it's valid */
-	if (!refs_ref_exists(refs, b_remote_head.buf)) {
+	if (!is_bare && !refs_ref_exists(refs, b_remote_head.buf)) {
 		result = 1;
 		goto cleanup;
 	}
 	if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
-					"fetch", NULL, 1))
+					"fetch", NULL, !is_bare))
 		result = 1;
 
 cleanup:
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index afa261409f..2600add82a 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -569,6 +569,16 @@ test_expect_success 'add --mirror && prune' '
 	)
 '
 
+test_expect_success 'add --mirror setting HEAD' '
+	mkdir headmirror &&
+	(
+		cd headmirror &&
+		git init --bare -b notmain &&
+		git remote add --mirror -f origin ../one &&
+		test "$(git symbolic-ref HEAD)" = "refs/heads/main"
+	)
+'
+
 test_expect_success 'add --mirror=fetch' '
 	mkdir mirror-fetch &&
 	git init -b main mirror-fetch/parent &&
-- 
2.47.0.298.g52a96ec17b
Bence Ferdinandy· Nov 22, 2024, 12:28 UTC · re: Bence Ferdinandy · lore

[PATCH v15 09/10] fetch: set remote/HEAD if it does not exist

When cloning a repository remote/HEAD is created, but when the user creates a repository with git init, and later adds a remote, remote/HEAD is only created if the user explicitly runs a variant of "remote set-head". Attempt to set remote/HEAD during fetch, if the user does not have it already set. Silently ignore any errors.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v3: - does not rely on remote set-head anymore so it only authenticates
        once
        - uses the new REF_CREATE_ONLY to atomically check if the ref exists
          and only write it if it doesn't
        - in all other cases the maximum it does is print a warning
    
    v4: - instead of the discarded REF_CREATE_ONLY, it uses the existing,
          but updated transaction api to request a silent create only
        - it now uses the atomic before_target to determine reporting
        - refactored for legibility
    
    v5: - instead of printing a not too useful message, it now fails
          silently, this in line with the objective to only set up
          remote/HEAD automatically if the right thing is trivial, for
          everything else there is remote set-head
        - fixed all failing tests
        - added two new tests, one for checking if remote/HEAD is set to the
          correct one, and one to test that we do not override remote/HEAD
          if it has changed on the server from what we have locally
    
    v6: - fixed style issues and unintended extra empty line
        - updated function call with bool to int from previous patch's
          change
        - removed calls to error(...) inherited from builtin/remote.c so we
          actually fail silently
        - set the test for remote set-head --auto to the correct value here,
          which was previously erronously set in the remote set-head patch
    
    v7: - no change
    
    v8: - changed logmsg in call to refs_update_symref from "remote
          set-head" to "fetch"
    
    v9: - follow through with refs_update_symref_extended
        - fix test errors uncovered by the new patch
    
    v10: no change
    
    v11: fixed some memory leaks
    
    v12: no change
    
    v13: - fix printed information if the local HEAD is detached
         - remove accidental formatting noise in a test
    
    v14: remove report_set_head_auto: as discussed, any noise should only be
    created with an opt out, and the configuration for this will be added in
    a later patch
    
    v15: no change
 builtin/fetch.c                  | 68 ++++++++++++++++++++++++++++++++
 t/t4207-log-decoration-colors.sh |  3 +-
 t/t5505-remote.sh                | 21 +++++++---
 t/t5510-fetch.sh                 | 24 +++++++++++
 t/t5512-ls-remote.sh             |  2 +
 t/t5514-fetch-multiple.sh        | 17 +++++++-
 t/t5516-fetch-push.sh            |  3 +-
 t/t5527-fetch-odd-refs.sh        |  3 +-
 t/t7900-maintenance.sh           |  3 +-
 t/t9210-scalar.sh                |  5 ++-
 t/t9211-scalar-clone.sh          |  6 +--
 t/t9902-completion.sh            | 65 ++++++++++++++++++++++++++++++
 12 files changed, 203 insertions(+), 17 deletions(-)
Show changes to 12 files +203 −17

builtin/fetch.c, t/t4207-log-decoration-colors.sh, t/t5505-remote.sh, t/t5510-fetch.sh, t/t5512-ls-remote.sh, t/t5514-fetch-multiple.sh, t/t5516-fetch-push.sh, t/t5527-fetch-odd-refs.sh, t/t7900-maintenance.sh, t/t9210-scalar.sh, t/t9211-scalar-clone.sh, t/t9902-completion.sh

diff --git a/builtin/fetch.c b/builtin/fetch.c
index 06b4611958..bbfaf50b63 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1574,6 +1574,66 @@ static int backfill_tags(struct display_state *display_state,
 	return retcode;
 }
 
+static const char *strip_refshead(const char *name){
+	skip_prefix(name, "refs/heads/", &name);
+	return name;
+}
+
+static int set_head(const struct ref *remote_refs)
+{
+	int result = 0;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT;
+	const char *remote = gtransport->remote->name;
+	char *head_name = NULL;
+	struct ref *ref, *matches;
+	struct ref *fetch_map = NULL, **fetch_map_tail = &fetch_map;
+	struct refspec_item refspec = {
+		.force = 0,
+		.pattern = 1,
+		.src = (char *) "refs/heads/*",
+		.dst = (char *) "refs/heads/*",
+	};
+	struct string_list heads = STRING_LIST_INIT_DUP;
+	struct ref_store *refs = get_main_ref_store(the_repository);
+
+	get_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0);
+	matches = guess_remote_head(find_ref_by_name(remote_refs, "HEAD"),
+				    fetch_map, 1);
+	for (ref = matches; ref; ref = ref->next) {
+		string_list_append(&heads, strip_refshead(ref->name));
+	}
+
+
+	if (!heads.nr)
+		result = 1;
+	else if (heads.nr > 1)
+		result = 1;
+	else
+		head_name = xstrdup(heads.items[0].string);
+
+	if (!head_name)
+		goto cleanup;
+	strbuf_addf(&b_head, "refs/remotes/%s/HEAD", remote);
+	strbuf_addf(&b_remote_head, "refs/remotes/%s/%s", remote, head_name);
+		/* make sure it's valid */
+	if (!refs_ref_exists(refs, b_remote_head.buf)) {
+		result = 1;
+		goto cleanup;
+	}
+	if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
+					"fetch", NULL, 1))
+		result = 1;
+
+cleanup:
+	free(head_name);
+	free_refs(fetch_map);
+	free_refs(matches);
+	string_list_clear(&heads, 0);
+	strbuf_release(&b_head);
+	strbuf_release(&b_remote_head);
+	return result;
+}
+
 static int do_fetch(struct transport *transport,
 		    struct refspec *rs,
 		    const struct fetch_config *config)
@@ -1643,6 +1703,8 @@ static int do_fetch(struct transport *transport,
 				    "refs/tags/");
 	}
 
+	strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
+
 	if (must_list_refs) {
 		trace2_region_enter("fetch", "remote_refs", the_repository);
 		remote_refs = transport_get_remote_refs(transport,
@@ -1787,6 +1849,12 @@ static int do_fetch(struct transport *transport,
 				  "you need to specify exactly one branch with the --set-upstream option"));
 		}
 	}
+	if (set_head(remote_refs))
+		;
+		/*
+		 * Way too many cases where this can go wrong
+		 * so let's just fail silently for now.
+		 */
 
 cleanup:
 	if (retcode) {
diff --git a/t/t4207-log-decoration-colors.sh b/t/t4207-log-decoration-colors.sh
index 73ea9e5155..d55d22cb2f 100755
--- a/t/t4207-log-decoration-colors.sh
+++ b/t/t4207-log-decoration-colors.sh
@@ -59,7 +59,8 @@ ${c_reset}${c_tag}tag: ${c_reset}${c_tag}v1.0${c_reset}${c_commit}, \
 ${c_reset}${c_tag}tag: ${c_reset}${c_tag}B${c_reset}${c_commit})${c_reset} B
 ${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
 ${c_tag}tag: ${c_reset}${c_tag}A1${c_reset}${c_commit}, \
-${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit})${c_reset} A1
+${c_reset}${c_remoteBranch}other/main${c_reset}${c_commit}, \
+${c_reset}${c_remoteBranch}other/HEAD${c_reset}${c_commit})${c_reset} A1
 	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
 ${c_stash}refs/stash${c_reset}${c_commit})${c_reset} On main: Changes to A.t
 	${c_commit}COMMIT_ID${c_reset}${c_commit} (${c_reset}\
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index d15b579c95..afa261409f 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -74,7 +74,7 @@ test_expect_success 'add another remote' '
 		cd test &&
 		git remote add -f second ../two &&
 		tokens_match "origin second" "$(git remote)" &&
-		check_tracking_branch second main side another &&
+		check_tracking_branch second main side another HEAD &&
 		git for-each-ref "--format=%(refname)" refs/remotes |
 		sed -e "/^refs\/remotes\/origin\//d" \
 		    -e "/^refs\/remotes\/second\//d" >actual &&
@@ -500,7 +500,7 @@ test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
 		cd test &&
 		git fetch two "refs/heads/*:refs/remotes/two/*" &&
 		git remote set-head --auto two >output 2>&1 &&
-		echo "${SQ}two/HEAD${SQ} is now created and points to ${SQ}main${SQ}" >expect &&
+		echo "${SQ}two/HEAD${SQ} is unchanged and points to ${SQ}main${SQ}" >expect &&
 		test_cmp expect output
 	)
 '
@@ -788,8 +788,10 @@ test_expect_success 'reject --no-no-tags' '
 '
 
 cat >one/expect <<\EOF
+  apis/HEAD -> apis/main
   apis/main
   apis/side
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
@@ -807,11 +809,14 @@ test_expect_success 'update' '
 '
 
 cat >one/expect <<\EOF
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
+  manduca/HEAD -> manduca/main
   manduca/main
   manduca/side
+  megaloprepus/HEAD -> megaloprepus/main
   megaloprepus/main
   megaloprepus/side
 EOF
@@ -819,7 +824,7 @@ EOF
 test_expect_success 'update with arguments' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -851,10 +856,13 @@ test_expect_success 'update --prune' '
 '
 
 cat >one/expect <<-\EOF
+  apis/HEAD -> apis/main
   apis/main
   apis/side
+  manduca/HEAD -> manduca/main
   manduca/main
   manduca/side
+  megaloprepus/HEAD -> megaloprepus/main
   megaloprepus/main
   megaloprepus/side
 EOF
@@ -862,7 +870,7 @@ EOF
 test_expect_success 'update default' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -874,6 +882,7 @@ test_expect_success 'update default' '
 '
 
 cat >one/expect <<\EOF
+  drosophila/HEAD -> drosophila/main
   drosophila/another
   drosophila/main
   drosophila/side
@@ -882,7 +891,7 @@ EOF
 test_expect_success 'update default (overridden, with funny whitespace)' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
@@ -896,7 +905,7 @@ test_expect_success 'update default (overridden, with funny whitespace)' '
 test_expect_success 'update (with remotes.default defined)' '
 	(
 		cd one &&
-		for b in $(git branch -r)
+		for b in $(git branch -r | grep -v HEAD)
 		do
 		git branch -r -d $b || exit 1
 		done &&
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index 0890b9f61c..87698341f5 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -75,6 +75,30 @@ test_expect_success "fetch test for-merge" '
 	cut -f -2 .git/FETCH_HEAD >actual &&
 	test_cmp expected actual'
 
+test_expect_success "fetch test remote HEAD" '
+	cd "$D" &&
+	cd two &&
+	git fetch &&
+	git rev-parse --verify refs/remotes/origin/HEAD &&
+	git rev-parse --verify refs/remotes/origin/main &&
+	head=$(git rev-parse refs/remotes/origin/HEAD) &&
+	branch=$(git rev-parse refs/remotes/origin/main) &&
+	test "z$head" = "z$branch"'
+
+test_expect_success "fetch test remote HEAD change" '
+	cd "$D" &&
+	cd two &&
+	git switch -c other &&
+	git push -u origin other &&
+	git rev-parse --verify refs/remotes/origin/HEAD &&
+	git rev-parse --verify refs/remotes/origin/main &&
+	git rev-parse --verify refs/remotes/origin/other &&
+	git remote set-head origin other &&
+	git fetch &&
+	head=$(git rev-parse refs/remotes/origin/HEAD) &&
+	branch=$(git rev-parse refs/remotes/origin/other) &&
+	test "z$head" = "z$branch"'
+
 test_expect_success 'fetch --prune on its own works as expected' '
 	cd "$D" &&
 	git clone . prune &&
diff --git a/t/t5512-ls-remote.sh b/t/t5512-ls-remote.sh
index 64b3491e4e..1b3865e154 100755
--- a/t/t5512-ls-remote.sh
+++ b/t/t5512-ls-remote.sh
@@ -293,6 +293,8 @@ test_expect_success 'ls-remote with filtered symref (refname)' '
 	cat >expect <<-EOF &&
 	ref: refs/heads/main	HEAD
 	$rev	HEAD
+	ref: refs/remotes/origin/main	refs/remotes/origin/HEAD
+	$rev	refs/remotes/origin/HEAD
 	EOF
 	git ls-remote --symref . HEAD >actual &&
 	test_cmp expect actual
diff --git a/t/t5514-fetch-multiple.sh b/t/t5514-fetch-multiple.sh
index 579872c258..e3482b27b2 100755
--- a/t/t5514-fetch-multiple.sh
+++ b/t/t5514-fetch-multiple.sh
@@ -45,14 +45,17 @@ test_expect_success setup '
 '
 
 cat > test/expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
   origin/HEAD -> origin/main
   origin/main
   origin/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -97,6 +100,7 @@ cat > expect << EOF
   origin/HEAD -> origin/main
   origin/main
   origin/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
@@ -112,8 +116,10 @@ test_expect_success 'git fetch --multiple (but only one remote)' '
 '
 
 cat > expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -141,7 +147,7 @@ test_expect_success 'git fetch --multiple (bad remote names)' '
 
 test_expect_success 'git fetch --all (skipFetchAll)' '
 	(cd test4 &&
-	 for b in $(git branch -r)
+	 for b in $(git branch -r | grep -v HEAD)
 	 do
 		git branch -r -d $b || exit 1
 	 done &&
@@ -153,11 +159,14 @@ test_expect_success 'git fetch --all (skipFetchAll)' '
 '
 
 cat > expect << EOF
+  one/HEAD -> one/main
   one/main
   one/side
+  three/HEAD -> three/main
   three/another
   three/main
   three/side
+  two/HEAD -> two/main
   two/another
   two/main
   two/side
@@ -165,7 +174,7 @@ EOF
 
 test_expect_success 'git fetch --multiple (ignoring skipFetchAll)' '
 	(cd test4 &&
-	 for b in $(git branch -r)
+	 for b in $(git branch -r | grep -v HEAD)
 	 do
 		git branch -r -d $b || exit 1
 	 done &&
@@ -221,14 +230,17 @@ test_expect_success 'git fetch --multiple --jobs=0 picks a default' '
 
 create_fetch_all_expect () {
 	cat >expect <<-\EOF
+	  one/HEAD -> one/main
 	  one/main
 	  one/side
 	  origin/HEAD -> origin/main
 	  origin/main
 	  origin/side
+	  three/HEAD -> three/main
 	  three/another
 	  three/main
 	  three/side
+	  two/HEAD -> two/main
 	  two/another
 	  two/main
 	  two/side
@@ -265,6 +277,7 @@ test_expect_success 'git fetch (fetch all remotes with fetch.all = true)' '
 
 create_fetch_one_expect () {
 	cat >expect <<-\EOF
+	  one/HEAD -> one/main
 	  one/main
 	  one/side
 	  origin/HEAD -> origin/main
diff --git a/t/t5516-fetch-push.sh b/t/t5516-fetch-push.sh
index 331778bd42..5a051aa0c7 100755
--- a/t/t5516-fetch-push.sh
+++ b/t/t5516-fetch-push.sh
@@ -1395,7 +1395,8 @@ test_expect_success 'fetch follows tags by default' '
 		git tag -m "annotated" tag &&
 		git for-each-ref >tmp1 &&
 		sed -n "p; s|refs/heads/main$|refs/remotes/origin/main|p" tmp1 |
-		sort -k 3 >../expect
+		sed -n "p; s|refs/heads/main$|refs/remotes/origin/HEAD|p"  |
+		sort -k 4 >../expect
 	) &&
 	test_when_finished "rm -rf dst" &&
 	git init dst &&
diff --git a/t/t5527-fetch-odd-refs.sh b/t/t5527-fetch-odd-refs.sh
index 98ece27c6a..d3996af6ee 100755
--- a/t/t5527-fetch-odd-refs.sh
+++ b/t/t5527-fetch-odd-refs.sh
@@ -52,7 +52,8 @@ test_expect_success LONG_REF 'fetch handles extremely long refname' '
 	long
 	main
 	EOF
-	git for-each-ref --format="%(subject)" refs/remotes/long >actual &&
+	git for-each-ref --format="%(subject)" refs/remotes/long \
+		--exclude=refs/remotes/long/HEAD >actual &&
 	test_cmp expect actual
 '
 
diff --git a/t/t7900-maintenance.sh b/t/t7900-maintenance.sh
index c224c8450c..edb85b7145 100755
--- a/t/t7900-maintenance.sh
+++ b/t/t7900-maintenance.sh
@@ -329,7 +329,8 @@ test_expect_success 'incremental-repack task' '
 
 	# Delete refs that have not been repacked in these packs.
 	git for-each-ref --format="delete %(refname)" \
-		refs/prefetch refs/tags refs/remotes >refs &&
+		refs/prefetch refs/tags refs/remotes \
+		--exclude=refs/remotes/*/HEAD >refs &&
 	git update-ref --stdin <refs &&
 
 	# Replace the object directory with this pack layout.
diff --git a/t/t9210-scalar.sh b/t/t9210-scalar.sh
index a30b2c9f70..2237844550 100755
--- a/t/t9210-scalar.sh
+++ b/t/t9210-scalar.sh
@@ -151,7 +151,8 @@ test_expect_success 'scalar clone' '
 			"$(pwd)" &&
 
 		git for-each-ref --format="%(refname)" refs/remotes/origin/ >actual &&
-		echo "refs/remotes/origin/parallel" >expect &&
+		echo "refs/remotes/origin/HEAD" >>expect &&
+		echo "refs/remotes/origin/parallel" >>expect &&
 		test_cmp expect actual &&
 
 		test_path_is_missing 1/2 &&
@@ -220,7 +221,7 @@ test_expect_success 'scalar reconfigure --all with includeIf.onbranch' '
 	done
 '
 
- test_expect_success 'scalar reconfigure --all with detached HEADs' '
+test_expect_success 'scalar reconfigure --all with detached HEADs' '
 	repos="two three four" &&
 	for num in $repos
 	do
diff --git a/t/t9211-scalar-clone.sh b/t/t9211-scalar-clone.sh
index c16ea67c1d..d9cb6b9a3e 100755
--- a/t/t9211-scalar-clone.sh
+++ b/t/t9211-scalar-clone.sh
@@ -32,7 +32,7 @@ test_expect_success 'set up repository to clone' '
 	)
 '
 
-cleanup_clone () {
+cleanup_clone() {
 	rm -rf "$1"
 }
 
@@ -128,7 +128,7 @@ test_expect_success '--single-branch clones HEAD only' '
 	(
 		cd $enlistment/src &&
 		git for-each-ref refs/remotes/origin >out &&
-		test_line_count = 1 out &&
+		test_line_count = 2 out &&
 		grep "refs/remotes/origin/base" out
 	) &&
 
@@ -142,7 +142,7 @@ test_expect_success '--no-single-branch clones all branches' '
 	(
 		cd $enlistment/src &&
 		git for-each-ref refs/remotes/origin >out &&
-		test_line_count = 2 out &&
+		test_line_count = 3 out &&
 		grep "refs/remotes/origin/base" out &&
 		grep "refs/remotes/origin/parallel" out
 	) &&
diff --git a/t/t9902-completion.sh b/t/t9902-completion.sh
index cc6aa9f0cd..b663c4609e 100755
--- a/t/t9902-completion.sh
+++ b/t/t9902-completion.sh
@@ -658,6 +658,7 @@ test_expect_success '__git_refs - simple' '
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	matching-tag
@@ -673,6 +674,7 @@ test_expect_success '__git_refs - full refs' '
 	cat >expected <<-EOF &&
 	refs/heads/main
 	refs/heads/matching-branch
+	refs/remotes/other/HEAD
 	refs/remotes/other/branch-in-other
 	refs/remotes/other/main-in-other
 	refs/tags/matching-tag
@@ -729,6 +731,7 @@ test_expect_success '__git_refs - remote on local file system - full refs' '
 test_expect_success '__git_refs - configured remote' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -756,6 +759,7 @@ test_expect_success '__git_refs - configured remote - full refs' '
 test_expect_success '__git_refs - configured remote - repo given on the command line' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -787,6 +791,7 @@ test_expect_success '__git_refs - configured remote - full refs - repo given on
 test_expect_success '__git_refs - configured remote - remote name matches a directory' '
 	cat >expected <<-EOF &&
 	HEAD
+	HEAD
 	branch-in-other
 	main-in-other
 	EOF
@@ -875,12 +880,14 @@ test_expect_success '__git_refs - unique remote branches for git checkout DWIMer
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/ambiguous
 	other/branch-in-other
 	other/main-in-other
 	remote/ambiguous
 	remote/branch-in-remote
 	matching-tag
+	HEAD
 	branch-in-other
 	branch-in-remote
 	main-in-other
@@ -904,6 +911,7 @@ test_expect_success '__git_refs - after --opt=' '
 	HEAD
 	main
 	matching-branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	matching-tag
@@ -919,6 +927,7 @@ test_expect_success '__git_refs - after --opt= - full refs' '
 	cat >expected <<-EOF &&
 	refs/heads/main
 	refs/heads/matching-branch
+	refs/remotes/other/HEAD
 	refs/remotes/other/branch-in-other
 	refs/remotes/other/main-in-other
 	refs/tags/matching-tag
@@ -935,6 +944,7 @@ test_expect_success '__git refs - excluding refs' '
 	^HEAD
 	^main
 	^matching-branch
+	^other/HEAD
 	^other/branch-in-other
 	^other/main-in-other
 	^matching-tag
@@ -950,6 +960,7 @@ test_expect_success '__git refs - excluding full refs' '
 	cat >expected <<-EOF &&
 	^refs/heads/main
 	^refs/heads/matching-branch
+	^refs/remotes/other/HEAD
 	^refs/remotes/other/branch-in-other
 	^refs/remotes/other/main-in-other
 	^refs/tags/matching-tag
@@ -975,6 +986,7 @@ test_expect_success '__git_refs - do not filter refs unless told so' '
 	main
 	matching-branch
 	matching/branch
+	other/HEAD
 	other/branch-in-other
 	other/main-in-other
 	other/matching/branch-in-other
@@ -1095,6 +1107,7 @@ test_expect_success '__git_complete_refs - simple' '
 	HEAD Z
 	main Z
 	matching-branch Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	matching-tag Z
@@ -1123,6 +1136,7 @@ test_expect_success '__git_complete_refs - matching' '
 test_expect_success '__git_complete_refs - remote' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main-in-other Z
 	EOF
@@ -1139,9 +1153,11 @@ test_expect_success '__git_complete_refs - track' '
 	HEAD Z
 	main Z
 	matching-branch Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	matching-tag Z
+	HEAD Z
 	branch-in-other Z
 	main-in-other Z
 	EOF
@@ -1184,6 +1200,7 @@ test_expect_success '__git_complete_refs - suffix' '
 	HEAD.
 	main.
 	matching-branch.
+	other/HEAD.
 	other/branch-in-other.
 	other/main-in-other.
 	matching-tag.
@@ -1199,6 +1216,7 @@ test_expect_success '__git_complete_refs - suffix' '
 test_expect_success '__git_complete_fetch_refspecs - simple' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	HEAD:HEAD Z
+	HEAD:HEAD Z
 	branch-in-other:branch-in-other Z
 	main-in-other:main-in-other Z
 	EOF
@@ -1225,6 +1243,7 @@ test_expect_success '__git_complete_fetch_refspecs - matching' '
 test_expect_success '__git_complete_fetch_refspecs - prefix' '
 	sed -e "s/Z$//" >expected <<-EOF &&
 	+HEAD:HEAD Z
+	+HEAD:HEAD Z
 	+branch-in-other:branch-in-other Z
 	+main-in-other:main-in-other Z
 	EOF
@@ -1289,6 +1308,7 @@ test_expect_success '__git_complete_worktree_paths with -C' '
 
 test_expect_success 'git switch - with no options, complete local branches and unique remote branch names for DWIM logic' '
 	test_completion "git switch " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1435,11 +1455,13 @@ test_expect_success 'git-bisect - existing view subcommand is recognized and ena
 test_expect_success 'git checkout - completes refs and unique remote branches for DWIM' '
 	test_completion "git checkout " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1461,6 +1483,7 @@ test_expect_success 'git switch - with GIT_COMPLETION_CHECKOUT_NO_GUESS=1, compl
 
 test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_GUESS=1, complete local branches and unique remote names for DWIM logic' '
 	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git switch --guess " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1470,6 +1493,7 @@ test_expect_success 'git switch - --guess overrides GIT_COMPLETION_CHECKOUT_NO_G
 
 test_expect_success 'git switch - a later --guess overrides previous --no-guess, complete local and remote unique branches for DWIM' '
 	test_completion "git switch --no-guess --guess " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1490,6 +1514,7 @@ test_expect_success 'git checkout - with GIT_COMPLETION_NO_GUESS=1 only complete
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1498,11 +1523,13 @@ test_expect_success 'git checkout - with GIT_COMPLETION_NO_GUESS=1 only complete
 test_expect_success 'git checkout - --guess overrides GIT_COMPLETION_NO_GUESS=1, complete refs and unique remote branches for DWIM' '
 	GIT_COMPLETION_CHECKOUT_NO_GUESS=1 test_completion "git checkout --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1514,6 +1541,7 @@ test_expect_success 'git checkout - with --no-guess, only completes refs' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1522,11 +1550,13 @@ test_expect_success 'git checkout - with --no-guess, only completes refs' '
 test_expect_success 'git checkout - a later --guess overrides previous --no-guess, complete refs and unique remote branches for DWIM' '
 	test_completion "git checkout --no-guess --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1538,6 +1568,7 @@ test_expect_success 'git checkout - a later --no-guess overrides previous --gues
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1550,6 +1581,7 @@ test_expect_success 'git checkout - with checkout.guess = false, only completes
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1559,11 +1591,13 @@ test_expect_success 'git checkout - with checkout.guess = true, completes refs a
 	test_config checkout.guess true &&
 	test_completion "git checkout " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1573,11 +1607,13 @@ test_expect_success 'git checkout - a later --guess overrides previous checkout.
 	test_config checkout.guess false &&
 	test_completion "git checkout --guess " <<-\EOF
 	HEAD Z
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1590,6 +1626,7 @@ test_expect_success 'git checkout - a later --no-guess overrides previous checko
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1601,6 +1638,7 @@ test_expect_success 'git switch - with --detach, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1612,6 +1650,7 @@ test_expect_success 'git checkout - with --detach, complete only references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1783,6 +1822,7 @@ test_expect_success 'git switch - with -d, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1794,6 +1834,7 @@ test_expect_success 'git checkout - with -d, complete only references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1801,10 +1842,12 @@ test_expect_success 'git checkout - with -d, complete only references' '
 
 test_expect_success 'git switch - with --track, complete only remote branches' '
 	test_completion "git switch --track " <<-\EOF &&
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
 	test_completion "git switch -t " <<-\EOF
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1812,10 +1855,12 @@ test_expect_success 'git switch - with --track, complete only remote branches' '
 
 test_expect_success 'git checkout - with --track, complete only remote branches' '
 	test_completion "git checkout --track " <<-\EOF &&
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
 	test_completion "git checkout -t " <<-\EOF
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1834,6 +1879,7 @@ test_expect_success 'git checkout - with --no-track, complete only local referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1845,6 +1891,7 @@ test_expect_success 'git switch - with -c, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1856,6 +1903,7 @@ test_expect_success 'git switch - with -C, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1867,6 +1915,7 @@ test_expect_success 'git switch - with -c and --track, complete all references'
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1878,6 +1927,7 @@ test_expect_success 'git switch - with -C and --track, complete all references'
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1889,6 +1939,7 @@ test_expect_success 'git switch - with -c and --no-track, complete all reference
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1900,6 +1951,7 @@ test_expect_success 'git switch - with -C and --no-track, complete all reference
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1911,6 +1963,7 @@ test_expect_success 'git checkout - with -b, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1922,6 +1975,7 @@ test_expect_success 'git checkout - with -B, complete all references' '
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1933,6 +1987,7 @@ test_expect_success 'git checkout - with -b and --track, complete all references
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1944,6 +1999,7 @@ test_expect_success 'git checkout - with -B and --track, complete all references
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1955,6 +2011,7 @@ test_expect_success 'git checkout - with -b and --no-track, complete all referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1966,6 +2023,7 @@ test_expect_success 'git checkout - with -B and --no-track, complete all referen
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
@@ -1973,6 +2031,7 @@ test_expect_success 'git checkout - with -B and --no-track, complete all referen
 
 test_expect_success 'git switch - for -c, complete local branches and unique remote branches' '
 	test_completion "git switch -c " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -1982,6 +2041,7 @@ test_expect_success 'git switch - for -c, complete local branches and unique rem
 
 test_expect_success 'git switch - for -C, complete local branches and unique remote branches' '
 	test_completion "git switch -C " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2019,6 +2079,7 @@ test_expect_success 'git switch - for -C with --no-track, complete local branche
 
 test_expect_success 'git checkout - for -b, complete local branches and unique remote branches' '
 	test_completion "git checkout -b " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2028,6 +2089,7 @@ test_expect_success 'git checkout - for -b, complete local branches and unique r
 
 test_expect_success 'git checkout - for -B, complete local branches and unique remote branches' '
 	test_completion "git checkout -B " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2065,6 +2127,7 @@ test_expect_success 'git checkout - for -B with --no-track, complete local branc
 
 test_expect_success 'git switch - with --orphan completes local branch names and unique remote branch names' '
 	test_completion "git switch --orphan " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2080,6 +2143,7 @@ test_expect_success 'git switch - --orphan with branch already provided complete
 
 test_expect_success 'git checkout - with --orphan completes local branch names and unique remote branch names' '
 	test_completion "git checkout --orphan " <<-\EOF
+	HEAD Z
 	branch-in-other Z
 	main Z
 	main-in-other Z
@@ -2093,6 +2157,7 @@ test_expect_success 'git checkout - --orphan with branch already provided comple
 	main Z
 	matching-branch Z
 	matching-tag Z
+	other/HEAD Z
 	other/branch-in-other Z
 	other/main-in-other Z
 	EOF
-- 
2.47.0.298.g52a96ec17b
Josh Steadmon· Dec 5, 2024, 18:58 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v15 09/10] fetch: set remote/HEAD if it does not exist

On 2024.11.22 13:28, Bence Ferdinandy wrote:
Show 7 quoted lines
> When cloning a repository remote/HEAD is created, but when the user
> creates a repository with git init, and later adds a remote, remote/HEAD
> is only created if the user explicitly runs a variant of "remote
> set-head". Attempt to set remote/HEAD during fetch, if the user does not
> have it already set. Silently ignore any errors.
> 
> Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>

At $DAYJOB, we noticed that this breaks `git fetch --tags`, although I haven't had a chance to figure out what causes the error just yet.

I was able to bisect down to this commit using Jonathan Tan's reproduction script:

rm -rf test_tag_1 test_tag_2 GIT=~/git/bin-wrappers/git mkdir test_tag_1 && cd test_tag_1 REMOTE=$(pwd) $GIT init . touch foo.txt $GIT add foo.txt $GIT commit foo.txt -m "commit one" $GIT tag foo cd .. mkdir test_tag_2 && cd test_tag_2 $GIT init . echo fetch --tags $GIT fetch --tags "file://$REMOTE" echo regular fetch $GIT fetch "file://$REMOTE" 'refs/tags/*:refs/tags/*' $GIT --version

Prior to this change, the first `$GIT fetch --tags "file://$REMOTE"` fetches the `foo` tag; with this change, it does not.

Josh Steadmon· Dec 5, 2024, 19:50 UTC · re: Josh Steadmon · lore

Re: [PATCH v15 09/10] fetch: set remote/HEAD if it does not exist

On 2024.12.05 10:58, Josh Steadmon wrote:
Show 36 quoted lines
> On 2024.11.22 13:28, Bence Ferdinandy wrote:
> > When cloning a repository remote/HEAD is created, but when the user
> > creates a repository with git init, and later adds a remote, remote/HEAD
> > is only created if the user explicitly runs a variant of "remote
> > set-head". Attempt to set remote/HEAD during fetch, if the user does not
> > have it already set. Silently ignore any errors.
> > 
> > Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
> 
> At $DAYJOB, we noticed that this breaks `git fetch --tags`, although I
> haven't had a chance to figure out what causes the error just yet.
> 
> I was able to bisect down to this commit using Jonathan Tan's
> reproduction script:
> 
> rm -rf test_tag_1 test_tag_2
> GIT=~/git/bin-wrappers/git
> mkdir test_tag_1 && cd test_tag_1
> REMOTE=$(pwd)
> $GIT init .
> touch foo.txt
> $GIT add foo.txt
> $GIT commit foo.txt -m "commit one"
> $GIT tag foo
> cd ..
> mkdir test_tag_2 && cd test_tag_2
> $GIT init .
> echo fetch --tags
> $GIT fetch --tags "file://$REMOTE"
> echo regular fetch
> $GIT fetch "file://$REMOTE" 'refs/tags/*:refs/tags/*'
> $GIT --version
> 
> 
> Prior to this change, the first `$GIT fetch --tags "file://$REMOTE"`
> fetches the `foo` tag; with this change, it does not.
FWIW, moving this:
Show changes to diff +2 −0
@@ -1643,6 +1703,8 @@ static int do_fetch(struct transport *transport,
                                    "refs/tags/");
        }

+       strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
+

to just above the prior `if` block fixes our issue and doesn't break any
tests. However, I'm not sure yet why the order of ref_prefixes should
matter here.
Bence Ferdinandy· Dec 5, 2024, 20:09 UTC · re: Josh Steadmon · lore

Re: [PATCH v15 09/10] fetch: set remote/HEAD if it does not exist

On Thu Dec 05, 2024 at 20:50, Josh Steadmon <steadmon@google.com> wrote:
Show 51 quoted lines
> On 2024.12.05 10:58, Josh Steadmon wrote:
>> On 2024.11.22 13:28, Bence Ferdinandy wrote:
>> > When cloning a repository remote/HEAD is created, but when the user
>> > creates a repository with git init, and later adds a remote, remote/HEAD
>> > is only created if the user explicitly runs a variant of "remote
>> > set-head". Attempt to set remote/HEAD during fetch, if the user does not
>> > have it already set. Silently ignore any errors.
>> > 
>> > Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
>> 
>> At $DAYJOB, we noticed that this breaks `git fetch --tags`, although I
>> haven't had a chance to figure out what causes the error just yet.
>> 
>> I was able to bisect down to this commit using Jonathan Tan's
>> reproduction script:
>> 
>> rm -rf test_tag_1 test_tag_2
>> GIT=~/git/bin-wrappers/git
>> mkdir test_tag_1 && cd test_tag_1
>> REMOTE=$(pwd)
>> $GIT init .
>> touch foo.txt
>> $GIT add foo.txt
>> $GIT commit foo.txt -m "commit one"
>> $GIT tag foo
>> cd ..
>> mkdir test_tag_2 && cd test_tag_2
>> $GIT init .
>> echo fetch --tags
>> $GIT fetch --tags "file://$REMOTE"
>> echo regular fetch
>> $GIT fetch "file://$REMOTE" 'refs/tags/*:refs/tags/*'
>> $GIT --version
>> 
>> 
>> Prior to this change, the first `$GIT fetch --tags "file://$REMOTE"`
>> fetches the `foo` tag; with this change, it does not.
>
>
> FWIW, moving this:
>
> @@ -1643,6 +1703,8 @@ static int do_fetch(struct transport *transport,
>                                     "refs/tags/");
>         }
>
> +       strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
> +
>
> to just above the prior `if` block fixes our issue and doesn't break any
> tests. However, I'm not sure yet why the order of ref_prefixes should
> matter here.
Thanks for looking into this! I think the issue is with 
 $GIT fetch --tags "file://$REMOTE"
instead of adding a proper remote. Tbh, I've never seen the above syntax before, so first I just ran your script, which reproduced the issue for me, but then I modified it to use a proper remote which works as expected:

rm -rf test_tag_1 test_tag_2 GIT=~/git/bin-wrappers/git mkdir test_tag_1 && cd test_tag_1 REMOTE=$(pwd) $GIT init . touch foo.txt $GIT add foo.txt $GIT commit foo.txt -m "commit one" $GIT tag foo cd .. mkdir test_tag_2 && cd test_tag_2 $GIT init . $GIT remote add origin $REMOTE echo fetch --tags $GIT fetch origin --tags echo regular fetch $GIT fetch origin 'refs/tags/*:refs/tags/*' $GIT --version

So I'm assuming this is why also the tests never caught this, since probably all of them are using `git remote add`.

Josh Steadmon· Dec 5, 2024, 20:11 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v15 09/10] fetch: set remote/HEAD if it does not exist

On 2024.12.05 21:09, Bence Ferdinandy wrote:
Show 82 quoted lines
> 
> On Thu Dec 05, 2024 at 20:50, Josh Steadmon <steadmon@google.com> wrote:
> > On 2024.12.05 10:58, Josh Steadmon wrote:
> >> On 2024.11.22 13:28, Bence Ferdinandy wrote:
> >> > When cloning a repository remote/HEAD is created, but when the user
> >> > creates a repository with git init, and later adds a remote, remote/HEAD
> >> > is only created if the user explicitly runs a variant of "remote
> >> > set-head". Attempt to set remote/HEAD during fetch, if the user does not
> >> > have it already set. Silently ignore any errors.
> >> > 
> >> > Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
> >> 
> >> At $DAYJOB, we noticed that this breaks `git fetch --tags`, although I
> >> haven't had a chance to figure out what causes the error just yet.
> >> 
> >> I was able to bisect down to this commit using Jonathan Tan's
> >> reproduction script:
> >> 
> >> rm -rf test_tag_1 test_tag_2
> >> GIT=~/git/bin-wrappers/git
> >> mkdir test_tag_1 && cd test_tag_1
> >> REMOTE=$(pwd)
> >> $GIT init .
> >> touch foo.txt
> >> $GIT add foo.txt
> >> $GIT commit foo.txt -m "commit one"
> >> $GIT tag foo
> >> cd ..
> >> mkdir test_tag_2 && cd test_tag_2
> >> $GIT init .
> >> echo fetch --tags
> >> $GIT fetch --tags "file://$REMOTE"
> >> echo regular fetch
> >> $GIT fetch "file://$REMOTE" 'refs/tags/*:refs/tags/*'
> >> $GIT --version
> >> 
> >> 
> >> Prior to this change, the first `$GIT fetch --tags "file://$REMOTE"`
> >> fetches the `foo` tag; with this change, it does not.
> >
> >
> > FWIW, moving this:
> >
> > @@ -1643,6 +1703,8 @@ static int do_fetch(struct transport *transport,
> >                                     "refs/tags/");
> >         }
> >
> > +       strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
> > +
> >
> > to just above the prior `if` block fixes our issue and doesn't break any
> > tests. However, I'm not sure yet why the order of ref_prefixes should
> > matter here.
> 
> Thanks for looking into this! I think the issue is with 
> 
>  $GIT fetch --tags "file://$REMOTE"
> 
> instead of adding a proper remote. Tbh, I've never seen the above syntax before, so first I just ran your script, which reproduced the issue for me, but then I modified it to use a proper remote which works as expected:
> 
> rm -rf test_tag_1 test_tag_2
> GIT=~/git/bin-wrappers/git
> mkdir test_tag_1 && cd test_tag_1
> REMOTE=$(pwd)
> $GIT init .
> touch foo.txt
> $GIT add foo.txt
> $GIT commit foo.txt -m "commit one"
> $GIT tag foo
> cd ..
> mkdir test_tag_2 && cd test_tag_2
> $GIT init .
> $GIT remote add origin $REMOTE
> echo fetch --tags
> $GIT fetch origin --tags 
> echo regular fetch
> $GIT fetch origin 'refs/tags/*:refs/tags/*'
> $GIT --version
> 
> So I'm assuming this is why also the tests never caught this, since probably
> all of them are using `git remote add`.
> 

Yeah, I think the issue is that we check the `--tags` flag first, but only add the ref_prefixes entry if it's not empty already. Then after that we unconditionally add HEAD. So that's why moving your strvec_push earlier fixes it. I'll send a fix + test patch in just a minute.

Josh Steadmon· Dec 5, 2024, 20:27 UTC · re: Josh Steadmon · lore

[PATCH] Fix `git fetch --tags` in repo with no configured remote

In 3f763ddf28 (fetch: set remote/HEAD if it does not exist, 2024-11-22), git-fetch learned to opportunistically set $REMOTE/HEAD when fetching. However, this broke the logic for the `--tags` flag. Specifically, we now unconditionally add HEAD to the ref_prefixes list, but we did this *after* deciding whether we also need to explicitly request tags.

Fix this by adding HEAD to the ref_prefixes list prior to handling the `--tags` flag, and removing the now obsolete check whether ref_prefixes is empty or not.

Signed-off-by: Josh Steadmon <steadmon@google.com>
---
 builtin/fetch.c  |  9 ++++-----
 t/t5510-fetch.sh | 17 +++++++++++++++++
 2 files changed, 21 insertions(+), 5 deletions(-)
Show changes to 2 files +21 −5

builtin/fetch.c, t/t5510-fetch.sh

diff --git a/builtin/fetch.c b/builtin/fetch.c
index b2a36a5d95..e7b0c79678 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1699,15 +1699,14 @@ static int do_fetch(struct transport *transport,
 		}
 	}
 
+	strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
+
 	if (tags == TAGS_SET || tags == TAGS_DEFAULT) {
 		must_list_refs = 1;
-		if (transport_ls_refs_options.ref_prefixes.nr)
-			strvec_push(&transport_ls_refs_options.ref_prefixes,
-				    "refs/tags/");
+		strvec_push(&transport_ls_refs_options.ref_prefixes,
+			    "refs/tags/");
 	}
 
-	strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
-
 	if (must_list_refs) {
 		trace2_region_enter("fetch", "remote_refs", the_repository);
 		remote_refs = transport_get_remote_refs(transport,
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index 87698341f5..d7602333ff 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -189,6 +189,23 @@ test_expect_success 'fetch --prune --tags with refspec prunes based on refspec'
 	git rev-parse sometag
 '
 
+test_expect_success 'fetch --tags gets tags even without a configured remote' '
+	REMOTE="$(pwd)/test_tag_1" &&
+	git init test_tag_1 &&
+	(
+		cd test_tag_1 &&
+		test_commit foo
+	) &&
+	git init test_tag_2 &&
+	(
+		cd test_tag_2 &&
+		git fetch --tags "file://$REMOTE" &&
+		echo "foo" >expect &&
+		git tag >actual &&
+		test_cmp expect actual
+	)
+'
+
 test_expect_success REFFILES 'fetch --prune fails to delete branches' '
 	cd "$D" &&
 	git clone . prune-fail &&

base-commit: 3f763ddf28d28fe63963991513c8db4045eabadc
-- 
2.47.0.338.g60cca15819-goog
Junio C Hamano· Dec 6, 2024, 03:07 UTC · re: Josh Steadmon · lore

Re: [PATCH] Fix `git fetch --tags` in repo with no configured remote

Josh Steadmon <steadmon@google.com> writes:
Show 15 quoted lines
> In 3f763ddf28 (fetch: set remote/HEAD if it does not exist, 2024-11-22),
> git-fetch learned to opportunistically set $REMOTE/HEAD when fetching.
> However, this broke the logic for the `--tags` flag. Specifically, we
> now unconditionally add HEAD to the ref_prefixes list, but we did this
> *after* deciding whether we also need to explicitly request tags.
>
> Fix this by adding HEAD to the ref_prefixes list prior to handling the
> `--tags` flag, and removing the now obsolete check whether ref_prefixes
> is empty or not.
>
> Signed-off-by: Josh Steadmon <steadmon@google.com>
> ---
>  builtin/fetch.c  |  9 ++++-----
>  t/t5510-fetch.sh | 17 +++++++++++++++++
>  2 files changed, 21 insertions(+), 5 deletions(-)

I see Bence is happy with the fix in a nearby message, so let me queue (and perhaps later amend it with Acked-by from Bence if we see one) the fix.

Thanks.
Junio C Hamano· Dec 6, 2024, 03:28 UTC · re: Josh Steadmon · lore

Re: [PATCH] Fix `git fetch --tags` in repo with no configured remote

Josh Steadmon <steadmon@google.com> writes:
Show 34 quoted lines
> In 3f763ddf28 (fetch: set remote/HEAD if it does not exist, 2024-11-22),
> git-fetch learned to opportunistically set $REMOTE/HEAD when fetching.
> However, this broke the logic for the `--tags` flag. Specifically, we
> now unconditionally add HEAD to the ref_prefixes list, but we did this
> *after* deciding whether we also need to explicitly request tags.
>
> Fix this by adding HEAD to the ref_prefixes list prior to handling the
> `--tags` flag, and removing the now obsolete check whether ref_prefixes
> is empty or not.
>
> Signed-off-by: Josh Steadmon <steadmon@google.com>
> ---
>  builtin/fetch.c  |  9 ++++-----
>  t/t5510-fetch.sh | 17 +++++++++++++++++
>  2 files changed, 21 insertions(+), 5 deletions(-)
>
> diff --git a/builtin/fetch.c b/builtin/fetch.c
> index b2a36a5d95..e7b0c79678 100644
> --- a/builtin/fetch.c
> +++ b/builtin/fetch.c
> @@ -1699,15 +1699,14 @@ static int do_fetch(struct transport *transport,
>  		}
>  	}
>  
> +	strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
> +
>  	if (tags == TAGS_SET || tags == TAGS_DEFAULT) {
>  		must_list_refs = 1;
> -		if (transport_ls_refs_options.ref_prefixes.nr)
> -			strvec_push(&transport_ls_refs_options.ref_prefixes,
> -				    "refs/tags/");
> +		strvec_push(&transport_ls_refs_options.ref_prefixes,
> +			    "refs/tags/");
>  	}

Stepping back a bit, do we even need to learn where HEAD points at in the remote, when we are not doing the "opportunistically set $REMOTE/HEAD"? Your example is "in repo with no configured remote", which by definition means that we do not use any refs/remotes/*/ ref hierarchy to keep track of the remote-tracking branches for the remote we are fetching from. There is no place we record what we learn by running ls-remote HEAD against them, so should we even push "HEAD" to the ls-remote prefixes in such a case?

While this change may hide the breakage you saw in your set-up, we may be now asking to ls-remote HEAD even in cases we do not need to.

> Fix this by adding HEAD to the ref_prefixes list prior to handling the
> `--tags` flag, and removing the now obsolete check whether ref_prefixes
> is empty or not.

And if we unconditionally add HEAD even when we do not need to, especially with the loss of the ref-prefixes condition that was there in order to implement "learn refs/tags/* hierarchy only when we are doing the default fetch", wouldn't it mean we may learn refs/tags/* even when we do not have to?

Show 60 quoted lines
> Signed-off-by: Josh Steadmon <steadmon@google.com>
> ---
>  builtin/fetch.c  |  9 ++++-----
>  t/t5510-fetch.sh | 17 +++++++++++++++++
>  2 files changed, 21 insertions(+), 5 deletions(-)
>
> diff --git a/builtin/fetch.c b/builtin/fetch.c
> index b2a36a5d95..e7b0c79678 100644
> --- a/builtin/fetch.c
> +++ b/builtin/fetch.c
> @@ -1699,15 +1699,14 @@ static int do_fetch(struct transport *transport,
>  		}
>  	}
>  
> +	strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
> +
>  	if (tags == TAGS_SET || tags == TAGS_DEFAULT) {
>  		must_list_refs = 1;
> -		if (transport_ls_refs_options.ref_prefixes.nr)
> -			strvec_push(&transport_ls_refs_options.ref_prefixes,
> -				    "refs/tags/");
> +		strvec_push(&transport_ls_refs_options.ref_prefixes,
> +			    "refs/tags/");
>  	}
>  
> -	strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
> -
>  	if (must_list_refs) {
>  		trace2_region_enter("fetch", "remote_refs", the_repository);
>  		remote_refs = transport_get_remote_refs(transport,
> diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
> index 87698341f5..d7602333ff 100755
> --- a/t/t5510-fetch.sh
> +++ b/t/t5510-fetch.sh
> @@ -189,6 +189,23 @@ test_expect_success 'fetch --prune --tags with refspec prunes based on refspec'
>  	git rev-parse sometag
>  '
>  
> +test_expect_success 'fetch --tags gets tags even without a configured remote' '
> +	REMOTE="$(pwd)/test_tag_1" &&
> +	git init test_tag_1 &&
> +	(
> +		cd test_tag_1 &&
> +		test_commit foo
> +	) &&
> +	git init test_tag_2 &&
> +	(
> +		cd test_tag_2 &&
> +		git fetch --tags "file://$REMOTE" &&
> +		echo "foo" >expect &&
> +		git tag >actual &&
> +		test_cmp expect actual
> +	)
> +'
> +
>  test_expect_success REFFILES 'fetch --prune fails to delete branches' '
>  	cd "$D" &&
>  	git clone . prune-fail &&
>
> base-commit: 3f763ddf28d28fe63963991513c8db4045eabadc
Junio C Hamano· Dec 6, 2024, 04:00 UTC · re: Junio C Hamano · lore

Re: [PATCH] Fix `git fetch --tags` in repo with no configured remote

Junio C Hamano <gitster@pobox.com> writes:
Show 5 quoted lines
> And if we unconditionally add HEAD even when we do not need to,
> especially with the loss of the ref-prefixes condition that was
> there in order to implement "learn refs/tags/* hierarchy only when
> we are doing the default fetch", wouldn't it mean we may learn
> refs/tags/* even when we do not have to?

Micro-correction. "We grab tags only when we are fetching something else as well" is what I should have said.

The general direction my comment leads to does not change, though. Because we ask for "HEAD" even when we do not need to, the additional change this patch names to unconditionally add "refs/tags/" would make us ask to list all the tags even when we do not need to see them.

Thanks.
Junio C Hamano· Dec 6, 2024, 08:07 UTC · re: Junio C Hamano · lore

Re* [PATCH] Fix `git fetch --tags` in repo with no configured remote

Junio C Hamano <gitster@pobox.com> writes:
Show 21 quoted lines
> Stepping back a bit, do we even need to learn where HEAD points at
> in the remote, when we are not doing the "opportunistically set
> $REMOTE/HEAD"?  Your example is "in repo with no configured remote",
> which by definition means that we do not use any refs/remotes/*/ ref
> hierarchy to keep track of the remote-tracking branches for the
> remote we are fetching from.  There is no place we record what we
> learn by running ls-remote HEAD against them, so should we even push
> "HEAD" to the ls-remote prefixes in such a case?
>
> While this change may hide the breakage you saw in your set-up, we
> may be now asking to ls-remote HEAD even in cases we do not need to.
>
>> Fix this by adding HEAD to the ref_prefixes list prior to handling the
>> `--tags` flag, and removing the now obsolete check whether ref_prefixes
>> is empty or not.
>
> And if we unconditionally add HEAD even when we do not need to,
> especially with the loss of the ref-prefixes condition that was
> there in order to implement "learn refs/tags/* hierarchy only when
> we are doing the default fetch", wouldn't it mean we may learn
> refs/tags/* even when we do not have to?

In other words, what I think the "fix" should look like is more like the attached. It seems to pass your test, as well as existing tests Bence added and other tests about "git fetch".

One thing I am not happy about is the abstraction violation that is needed to make the uses_remote_tracking() helper aware of the "use the rs, the refspec given from the command line, or if it is empty, use the configured 'fetch' refspec from the remote" rule, which is primarily used by get_ref_map() that is much later called, but the layering violation started when we started limiting the ls-remote request with narrowing common prefixes, and it would take a larger surgery to fix, I would think.

0000---- >8 ----
Subject: [PATCH] fetch: do not ask for HEAD unnecessarily

In 3f763ddf28 (fetch: set remote/HEAD if it does not exist, 2024-11-22), git-fetch learned to opportunistically set $REMOTE/HEAD when fetching by always asking for remote HEAD, in the hope that it will help setting refs/remotes/<name>/HEAD if missing.

But it is not needed to always ask for remote HEAD. When we are fetching from a remote, for which we have remote-tracking branches, we do need to know about HEAD. But if we are doing one-shot fetch, e.g.,

  $ git fetch --tags https://github.com/git/git

we do not even know what sub-hierarchy of refs/remotes/<remote>/ we need to adjust the remote HEAD for. There is no need to ask for HEAD in such a case.

Incidentally, because the unconditional request to list "HEAD" affected the number of ref-prefixes requested in the ls-remote request, this affected how the requests for tags are added to the same ls-remote request, breaking "git fetch --tags $URL" performed against a URL that is not configured as a remote.

Make sure we ask to list "HEAD" from the remote only when we are fetching with configured remote for which we use remote-tracking branches.

Reported-by: Josh Steadmon <steadmon@google.com>
[jc: tests are also borrowed from Josh's patch]
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 * Even though I borrowed some part of the commit log message from
   Josh's version, it not clear to me how "*after* deciding" led to
   whatever the observed breakage (which was not described in the
   log message), in the following part.
      Specifically, we now unconditionally add HEAD to the
      ref_prefixes list, but we did this *after* deciding whether we
      also need to explicitly request tags.
   Bence's change asks "HEAD" after "if we are fetching something,
   then also ask about refs/tags/" logic thought we are not fetching
   anything (i.e. ref_prefixes.nr == 0 at that point).  But before
   Bence's series, the same refs/tags/ logic saw that (ref_prefix.nr
   == 0), didn't it?  So that does not sound like a sufficient
   explanation on how the series regressed.
 builtin/fetch.c  | 20 +++++++++++++++++++-
 t/t5510-fetch.sh | 17 +++++++++++++++++
 2 files changed, 36 insertions(+), 1 deletion(-)
Show changes to diff +36 −1
diff --git c/builtin/fetch.c w/builtin/fetch.c
index a64de4485f..3eb6f3acc9 100644
--- c/builtin/fetch.c
+++ w/builtin/fetch.c
@@ -1643,6 +1643,21 @@ static int set_head(const struct ref *remote_refs)
 	return result;
 }
 
+static int uses_remote_tracking(struct transport *transport, struct refspec *rs)
+{
+	if (!remote_is_configured(transport->remote, 0))
+		return 0;
+
+	if (!rs->nr)
+		rs = &transport->remote->fetch;
+
+	for (int i = 0; i < rs->nr; i++)
+		if (rs->items[i].dst)
+			return 1;
+
+	return 0;
+}
+
 static int do_fetch(struct transport *transport,
 		    struct refspec *rs,
 		    const struct fetch_config *config)
@@ -1712,7 +1727,10 @@ static int do_fetch(struct transport *transport,
 				    "refs/tags/");
 	}
 
-	strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
+	if (uses_remote_tracking(transport, rs)) {
+		must_list_refs = 1;
+		strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
+	}
 
 	if (must_list_refs) {
 		trace2_region_enter("fetch", "remote_refs", the_repository);
diff --git c/t/t5510-fetch.sh w/t/t5510-fetch.sh
index 87698341f5..d7602333ff 100755
--- c/t/t5510-fetch.sh
+++ w/t/t5510-fetch.sh
@@ -189,6 +189,23 @@ test_expect_success 'fetch --prune --tags with refspec prunes based on refspec'
 	git rev-parse sometag
 '
 
+test_expect_success 'fetch --tags gets tags even without a configured remote' '
+	REMOTE="$(pwd)/test_tag_1" &&
+	git init test_tag_1 &&
+	(
+		cd test_tag_1 &&
+		test_commit foo
+	) &&
+	git init test_tag_2 &&
+	(
+		cd test_tag_2 &&
+		git fetch --tags "file://$REMOTE" &&
+		echo "foo" >expect &&
+		git tag >actual &&
+		test_cmp expect actual
+	)
+'
+
 test_expect_success REFFILES 'fetch --prune fails to delete branches' '
 	cd "$D" &&
 	git clone . prune-fail &&
Junio C Hamano· Dec 6, 2024, 08:08 UTC · re: Junio C Hamano · lore

Re* [PATCH] Fix `git fetch --tags` in repo with no configured remote

Junio C Hamano <gitster@pobox.com> writes:
Show 21 quoted lines
> Stepping back a bit, do we even need to learn where HEAD points at
> in the remote, when we are not doing the "opportunistically set
> $REMOTE/HEAD"?  Your example is "in repo with no configured remote",
> which by definition means that we do not use any refs/remotes/*/ ref
> hierarchy to keep track of the remote-tracking branches for the
> remote we are fetching from.  There is no place we record what we
> learn by running ls-remote HEAD against them, so should we even push
> "HEAD" to the ls-remote prefixes in such a case?
>
> While this change may hide the breakage you saw in your set-up, we
> may be now asking to ls-remote HEAD even in cases we do not need to.
>
>> Fix this by adding HEAD to the ref_prefixes list prior to handling the
>> `--tags` flag, and removing the now obsolete check whether ref_prefixes
>> is empty or not.
>
> And if we unconditionally add HEAD even when we do not need to,
> especially with the loss of the ref-prefixes condition that was
> there in order to implement "learn refs/tags/* hierarchy only when
> we are doing the default fetch", wouldn't it mean we may learn
> refs/tags/* even when we do not have to?

In other words, what I think the "fix" should look like is more like the attached. It seems to pass your test, as well as existing tests Bence added and other tests about "git fetch".

One thing I am not happy about is the abstraction violation that is needed to make the uses_remote_tracking() helper aware of the "use the rs, the refspec given from the command line, or if it is empty, use the configured 'fetch' refspec from the remote" rule, which is primarily used by get_ref_map() that is much later called, but the layering violation started when we started limiting the ls-remote request with narrowing common prefixes, and it would take a larger surgery to fix, I would think.

---- >8 ----
Subject: [PATCH] fetch: do not ask for HEAD unnecessarily

In 3f763ddf28 (fetch: set remote/HEAD if it does not exist, 2024-11-22), git-fetch learned to opportunistically set $REMOTE/HEAD when fetching by always asking for remote HEAD, in the hope that it will help setting refs/remotes/<name>/HEAD if missing.

But it is not needed to always ask for remote HEAD. When we are fetching from a remote, for which we have remote-tracking branches, we do need to know about HEAD. But if we are doing one-shot fetch, e.g.,

  $ git fetch --tags https://github.com/git/git

we do not even know what sub-hierarchy of refs/remotes/<remote>/ we need to adjust the remote HEAD for. There is no need to ask for HEAD in such a case.

Incidentally, because the unconditional request to list "HEAD" affected the number of ref-prefixes requested in the ls-remote request, this affected how the requests for tags are added to the same ls-remote request, breaking "git fetch --tags $URL" performed against a URL that is not configured as a remote.

Reported-by: Josh Steadmon <steadmon@google.com>
[jc: tests are also borrowed from Josh's patch]
Signed-off-by: Junio C Hamano <gitster@pobox.com>
---
 * Even though I borrowed some part of the commit log message from
   Josh's version, it not clear to me how "*after* deciding" led to
   whatever the observed breakage (which was not described in the
   log message), in the following part.
      Specifically, we now unconditionally add HEAD to the
      ref_prefixes list, but we did this *after* deciding whether we
      also need to explicitly request tags.
   Bence's change asks "HEAD" after "if we are fetching something,
   then also ask about refs/tags/" logic thought we are not fetching
   anything (i.e. ref_prefixes.nr == 0 at that point).  But before
   Bence's series, the same refs/tags/ logic saw that (ref_prefix.nr
   == 0), didn't it?  So that does not sound like a sufficient
   explanation on how the series regressed.
---
 builtin/fetch.c  | 20 +++++++++++++++++++-
 t/t5510-fetch.sh | 17 +++++++++++++++++
 2 files changed, 36 insertions(+), 1 deletion(-)
Show changes to 2 files +36 −1

builtin/fetch.c, t/t5510-fetch.sh

diff --git a/builtin/fetch.c b/builtin/fetch.c
index a64de4485f..3eb6f3acc9 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1643,6 +1643,21 @@ static int set_head(const struct ref *remote_refs)
 	return result;
 }
 
+static int uses_remote_tracking(struct transport *transport, struct refspec *rs)
+{
+	if (!remote_is_configured(transport->remote, 0))
+		return 0;
+
+	if (!rs->nr)
+		rs = &transport->remote->fetch;
+
+	for (int i = 0; i < rs->nr; i++)
+		if (rs->items[i].dst)
+			return 1;
+
+	return 0;
+}
+
 static int do_fetch(struct transport *transport,
 		    struct refspec *rs,
 		    const struct fetch_config *config)
@@ -1712,7 +1727,10 @@ static int do_fetch(struct transport *transport,
 				    "refs/tags/");
 	}
 
-	strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
+	if (uses_remote_tracking(transport, rs)) {
+		must_list_refs = 1;
+		strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
+	}
 
 	if (must_list_refs) {
 		trace2_region_enter("fetch", "remote_refs", the_repository);
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index 87698341f5..d7602333ff 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -189,6 +189,23 @@ test_expect_success 'fetch --prune --tags with refspec prunes based on refspec'
 	git rev-parse sometag
 '
 
+test_expect_success 'fetch --tags gets tags even without a configured remote' '
+	REMOTE="$(pwd)/test_tag_1" &&
+	git init test_tag_1 &&
+	(
+		cd test_tag_1 &&
+		test_commit foo
+	) &&
+	git init test_tag_2 &&
+	(
+		cd test_tag_2 &&
+		git fetch --tags "file://$REMOTE" &&
+		echo "foo" >expect &&
+		git tag >actual &&
+		test_cmp expect actual
+	)
+'
+
 test_expect_success REFFILES 'fetch --prune fails to delete branches' '
 	cd "$D" &&
 	git clone . prune-fail &&
-- 
2.47.1-576-g9f8c8eb655
Bence Ferdinandy· Dec 6, 2024, 11:23 UTC · re: Junio C Hamano · lore

Re: Re* [PATCH] Fix `git fetch --tags` in repo with no configured remote

On Fri Dec 06, 2024 at 09:08, Junio C Hamano <gitster@pobox.com> wrote:
Show 10 quoted lines
> Junio C Hamano <gitster@pobox.com> writes:
>
>> Stepping back a bit, do we even need to learn where HEAD points at
>> in the remote, when we are not doing the "opportunistically set
>> $REMOTE/HEAD"?  Your example is "in repo with no configured remote",
>> which by definition means that we do not use any refs/remotes/*/ ref
>> hierarchy to keep track of the remote-tracking branches for the
>> remote we are fetching from.  There is no place we record what we
>> learn by running ls-remote HEAD against them, so should we even push
>> "HEAD" to the ls-remote prefixes in such a case?
Yes, it probably doesn't make any sense to do that.
Show 72 quoted lines
>>
>> While this change may hide the breakage you saw in your set-up, we
>> may be now asking to ls-remote HEAD even in cases we do not need to.
>>
>>> Fix this by adding HEAD to the ref_prefixes list prior to handling the
>>> `--tags` flag, and removing the now obsolete check whether ref_prefixes
>>> is empty or not.
>>
>> And if we unconditionally add HEAD even when we do not need to,
>> especially with the loss of the ref-prefixes condition that was
>> there in order to implement "learn refs/tags/* hierarchy only when
>> we are doing the default fetch", wouldn't it mean we may learn
>> refs/tags/* even when we do not have to?
>
> In other words, what I think the "fix" should look like is more like
> the attached.  It seems to pass your test, as well as existing tests
> Bence added and other tests about "git fetch".
>
> One thing I am not happy about is the abstraction violation that is
> needed to make the uses_remote_tracking() helper aware of the "use
> the rs, the refspec given from the command line, or if it is empty,
> use the configured 'fetch' refspec from the remote" rule, which is
> primarily used by get_ref_map() that is much later called, but the
> layering violation started when we started limiting the ls-remote
> request with narrowing common prefixes, and it would take a larger
> surgery to fix, I would think.
>
> ---- >8 ----
> Subject: [PATCH] fetch: do not ask for HEAD unnecessarily
>
> In 3f763ddf28 (fetch: set remote/HEAD if it does not exist,
> 2024-11-22), git-fetch learned to opportunistically set $REMOTE/HEAD
> when fetching by always asking for remote HEAD, in the hope that it
> will help setting refs/remotes/<name>/HEAD if missing.
>
> But it is not needed to always ask for remote HEAD.  When we are
> fetching from a remote, for which we have remote-tracking branches,
> we do need to know about HEAD.  But if we are doing one-shot fetch,
> e.g.,
>
>   $ git fetch --tags https://github.com/git/git
>
> we do not even know what sub-hierarchy of refs/remotes/<remote>/
> we need to adjust the remote HEAD for.  There is no need to ask for
> HEAD in such a case.
>
> Incidentally, because the unconditional request to list "HEAD"
> affected the number of ref-prefixes requested in the ls-remote
> request, this affected how the requests for tags are added to the
> same ls-remote request, breaking "git fetch --tags $URL" performed
> against a URL that is not configured as a remote.
>
> Reported-by: Josh Steadmon <steadmon@google.com>
> [jc: tests are also borrowed from Josh's patch]
> Signed-off-by: Junio C Hamano <gitster@pobox.com>
> ---
>
>  * Even though I borrowed some part of the commit log message from
>    Josh's version, it not clear to me how "*after* deciding" led to
>    whatever the observed breakage (which was not described in the
>    log message), in the following part.
>
>       Specifically, we now unconditionally add HEAD to the
>       ref_prefixes list, but we did this *after* deciding whether we
>       also need to explicitly request tags.
>
>    Bence's change asks "HEAD" after "if we are fetching something,
>    then also ask about refs/tags/" logic thought we are not fetching
>    anything (i.e. ref_prefixes.nr == 0 at that point).  But before
>    Bence's series, the same refs/tags/ logic saw that (ref_prefix.nr
>    == 0), didn't it?  So that does not sound like a sufficient
>    explanation on how the series regressed.

I did a bit of poking around on what is happening. For one I can confirm, that both before and after the set_head series `transport_ls_refs_options.ref_prefixes.nr` is 0. So the difference must be happening somewhere after that, and is not a side effect of calling set_head either, but I didn't manage to pin it down further.

I also checked what happens in set_head, just to be on the safe side: `heads` is empty so we reach the if where we check `heads.nr` which is 0. So at least no strange refs are created :)

Show 71 quoted lines
> ---
>  builtin/fetch.c  | 20 +++++++++++++++++++-
>  t/t5510-fetch.sh | 17 +++++++++++++++++
>  2 files changed, 36 insertions(+), 1 deletion(-)
>
> diff --git a/builtin/fetch.c b/builtin/fetch.c
> index a64de4485f..3eb6f3acc9 100644
> --- a/builtin/fetch.c
> +++ b/builtin/fetch.c
> @@ -1643,6 +1643,21 @@ static int set_head(const struct ref *remote_refs)
>  	return result;
>  }
>  
> +static int uses_remote_tracking(struct transport *transport, struct refspec *rs)
> +{
> +	if (!remote_is_configured(transport->remote, 0))
> +		return 0;
> +
> +	if (!rs->nr)
> +		rs = &transport->remote->fetch;
> +
> +	for (int i = 0; i < rs->nr; i++)
> +		if (rs->items[i].dst)
> +			return 1;
> +
> +	return 0;
> +}
> +
>  static int do_fetch(struct transport *transport,
>  		    struct refspec *rs,
>  		    const struct fetch_config *config)
> @@ -1712,7 +1727,10 @@ static int do_fetch(struct transport *transport,
>  				    "refs/tags/");
>  	}
>  
> -	strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
> +	if (uses_remote_tracking(transport, rs)) {
> +		must_list_refs = 1;
> +		strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD");
> +	}
>  
>  	if (must_list_refs) {
>  		trace2_region_enter("fetch", "remote_refs", the_repository);
> diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
> index 87698341f5..d7602333ff 100755
> --- a/t/t5510-fetch.sh
> +++ b/t/t5510-fetch.sh
> @@ -189,6 +189,23 @@ test_expect_success 'fetch --prune --tags with refspec prunes based on refspec'
>  	git rev-parse sometag
>  '
>  
> +test_expect_success 'fetch --tags gets tags even without a configured remote' '
> +	REMOTE="$(pwd)/test_tag_1" &&
> +	git init test_tag_1 &&
> +	(
> +		cd test_tag_1 &&
> +		test_commit foo
> +	) &&
> +	git init test_tag_2 &&
> +	(
> +		cd test_tag_2 &&
> +		git fetch --tags "file://$REMOTE" &&
> +		echo "foo" >expect &&
> +		git tag >actual &&
> +		test_cmp expect actual
> +	)
> +'
> +
>  test_expect_success REFFILES 'fetch --prune fails to delete branches' '
>  	cd "$D" &&
>  	git clone . prune-fail &&
Junio C Hamano· Dec 6, 2024, 11:30 UTC · re: Junio C Hamano · lore

Re: Re* [PATCH] Fix `git fetch --tags` in repo with no configured remote

Junio C Hamano <gitster@pobox.com> writes:
Show 14 quoted lines
> +static int uses_remote_tracking(struct transport *transport, struct refspec *rs)
> +{
> +	if (!remote_is_configured(transport->remote, 0))
> +		return 0;
> +
> +	if (!rs->nr)
> +		rs = &transport->remote->fetch;
> +
> +	for (int i = 0; i < rs->nr; i++)
> +		if (rs->items[i].dst)
> +			return 1;
> +
> +	return 0;
> +}

For the purpose of adjusting refs/remotes/<name>/HEAD, which is expected to point at something that we copy from their refs/heads/, it may be tempting to tighten the above logic to say, instead of "we have a refspec item that stores to somewhere", do something like

	for (int i = 0; i < rs->nr; i++) {
		struct refspec_item *item = &rs->items[i];
		if (item->dst &&
		    item->src && starts_with(item->src, "refs/heads/"))
			return 1;
	}

to make sure that we are tracking their branch refs, not some random hierarchy for which refs/remotes/<name>/HEAD does not matter.

I will leave the simplest "just check we have .dst" version (in other words, without any additional check on the .src side) in my tree, but if anybody wants to pursue "let's make sure that .src copies from somewhere in refs/heads/", please make sure that you consider "--mirror", where an equivalent of "refs/*:refs/*" is used as the refspec, i.e. there is a case where .src does not begin with "refs/heads/" but does cover the hierarchy and wants to learn about "HEAD".

Thanks.
Bence Ferdinandy· Dec 5, 2024, 20:57 UTC · re: Josh Steadmon · lore

Re: [PATCH v15 09/10] fetch: set remote/HEAD if it does not exist

On Thu Dec 05, 2024 at 21:11, Josh Steadmon <steadmon@google.com> wrote: [snip]

Show 34 quoted lines
>> 
>> Thanks for looking into this! I think the issue is with 
>> 
>>  $GIT fetch --tags "file://$REMOTE"
>> 
>> instead of adding a proper remote. Tbh, I've never seen the above syntax before, so first I just ran your script, which reproduced the issue for me, but then I modified it to use a proper remote which works as expected:
>> 
>> rm -rf test_tag_1 test_tag_2
>> GIT=~/git/bin-wrappers/git
>> mkdir test_tag_1 && cd test_tag_1
>> REMOTE=$(pwd)
>> $GIT init .
>> touch foo.txt
>> $GIT add foo.txt
>> $GIT commit foo.txt -m "commit one"
>> $GIT tag foo
>> cd ..
>> mkdir test_tag_2 && cd test_tag_2
>> $GIT init .
>> $GIT remote add origin $REMOTE
>> echo fetch --tags
>> $GIT fetch origin --tags 
>> echo regular fetch
>> $GIT fetch origin 'refs/tags/*:refs/tags/*'
>> $GIT --version
>> 
>> So I'm assuming this is why also the tests never caught this, since probably
>> all of them are using `git remote add`.
>> 
>
> Yeah, I think the issue is that we check the `--tags` flag first, but
> only add the ref_prefixes entry if it's not empty already. Then after
> that we unconditionally add HEAD. So that's why moving your strvec_push
> earlier fixes it. I'll send a fix + test patch in just a minute.
Awesome, thanks!
Bence Ferdinandy· Nov 27, 2024, 09:16 UTC · re: Bence Ferdinandy · lore

[PATCH v1] fetch: add configuration for set_head behaviour

In the current implementatio, if refs/remotes/$remote/HEAD does not exist, running fetch will create it, but if it does exist it will not do anything, which is a somewhat safe and minimal approach. Unfortunately, for users who wish to NOT have refs/remotes/$remote/HEAD set for any reason (e.g. so that `git rev-parse origin` doesn't accidentally point them somewhere they do not want to), there is no way to remove this behaviour. On the other side of the spectrum, users may want fetch to automatically update HEAD or at least give them a warning if something changed on the remote.

Introduce a new setting, remote.$remote.followRemoteHEAD with four options:

    - "never": do not ever do anything, not even create
    - "create": the current behaviour, now the default behaviour
    - "warn": print a message if remote and local HEAD is different
    - "always": silently update HEAD on every change
Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    This patch is built off v15 of the set-head/fetch updates series:
    https://lore.kernel.org/git/D5WEJJBMSO1K.2TPXDI1K08SHT@ferdinandy.com/T/#m13a0c3e8919872188ef07fd9fd984c78e8aa35ba
    
    The patch is sent separately, because the rest of the series building up
    to here seems ready, while this patch will likely trigger some
    discussion.
 Documentation/config/remote.txt |  11 ++++
 builtin/fetch.c                 |  46 ++++++++++++--
 remote.c                        |   9 +++
 remote.h                        |   9 +++
 t/t5510-fetch.sh                | 102 ++++++++++++++++++++++++++++++++
 5 files changed, 171 insertions(+), 6 deletions(-)
Show changes to 5 files +171 −6

Documentation/config/remote.txt, builtin/fetch.c, remote.c, remote.h, t/t5510-fetch.sh

diff --git a/Documentation/config/remote.txt b/Documentation/config/remote.txt
index 6d8b7d6c63..024f92befc 100644
--- a/Documentation/config/remote.txt
+++ b/Documentation/config/remote.txt
@@ -101,6 +101,17 @@ remote.<name>.serverOption::
 	The default set of server options used when fetching from this remote.
 	These server options can be overridden by the `--server-option=` command
 	line arguments.
+
+remote.<name>.followRemoteHEAD::
+	How linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`.
+	The default value is "create", which will create `remotes/<name>/HEAD`
+	if it exists on the remote, but not locally, but will not touch an
+	already existing local reference.  Setting to "warn" will print
+	a message if the remote has a different value, than the local one and
+	in case there is no local reference, it behaves like "create". Setting
+	to "always" will silently update it to the value on the remote.
+	Finally, setting it to "never" will never change or create the local
+	reference.
 +
 This is a multi-valued variable, and an empty value can be used in a higher
 priority configuration file (e.g. `.git/config` in a repository) to clear
diff --git a/builtin/fetch.c b/builtin/fetch.c
index 2f416cf867..b619bddd7a 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1579,10 +1579,35 @@ static const char *strip_refshead(const char *name){
 	return name;
 }
 
-static int set_head(const struct ref *remote_refs)
+static void report_set_head(const char *remote, const char *head_name,
+			struct strbuf *buf_prev, int updateres) {
+	struct strbuf buf_prefix = STRBUF_INIT;
+	const char *prev_head = NULL;
+
+	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
+	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
+
+	if (prev_head && strcmp(prev_head, head_name)) {
+		printf("'HEAD' at '%s' is '%s', but we have '%s' locally.\n",
+			remote, head_name, prev_head);
+		printf("Run 'git remote set-head %s %s' to follow the change.\n",
+			remote, head_name);
+	}
+	else if (updateres && buf_prev->len) {
+		printf("'HEAD' at '%s' is '%s', "
+			"but we have a detached HEAD pointing to '%s' locally.\n",
+			remote, head_name, buf_prev->buf);
+		printf("Run 'git remote set-head %s %s' to follow the change.\n",
+			remote, head_name);
+	}
+	strbuf_release(&buf_prefix);
+}
+
+static int set_head(const struct ref *remote_refs, int follow_remote_head)
 {
-	int result = 0, is_bare;
-	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT;
+	int result = 0, create_only, is_bare, was_detached;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
+		      b_local_head = STRBUF_INIT;
 	const char *remote = gtransport->remote->name;
 	char *head_name = NULL;
 	struct ref *ref, *matches;
@@ -1603,6 +1628,8 @@ static int set_head(const struct ref *remote_refs)
 		string_list_append(&heads, strip_refshead(ref->name));
 	}
 
+	if (follow_remote_head < 0)
+		goto cleanup;
 
 	if (!heads.nr)
 		result = 1;
@@ -1614,6 +1641,7 @@ static int set_head(const struct ref *remote_refs)
 	if (!head_name)
 		goto cleanup;
 	is_bare = is_bare_repository();
+	create_only = follow_remote_head == 2 ? 0 : !is_bare;
 	if (is_bare) {
 		strbuf_addstr(&b_head, "HEAD");
 		strbuf_addf(&b_remote_head, "refs/heads/%s", head_name);
@@ -1626,9 +1654,14 @@ static int set_head(const struct ref *remote_refs)
 		result = 1;
 		goto cleanup;
 	}
-	if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
-					"fetch", NULL, !is_bare))
+	was_detached = refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
+					"fetch", &b_local_head, create_only);
+	if (was_detached == -1) {
 		result = 1;
+		goto cleanup;
+	}
+	if (follow_remote_head == 1 && verbosity >= 0)
+		report_set_head(remote, head_name, &b_local_head, was_detached);
 
 cleanup:
 	free(head_name);
@@ -1636,6 +1669,7 @@ static int set_head(const struct ref *remote_refs)
 	free_refs(matches);
 	string_list_clear(&heads, 0);
 	strbuf_release(&b_head);
+	strbuf_release(&b_local_head);
 	strbuf_release(&b_remote_head);
 	return result;
 }
@@ -1855,7 +1889,7 @@ static int do_fetch(struct transport *transport,
 				  "you need to specify exactly one branch with the --set-upstream option"));
 		}
 	}
-	if (set_head(remote_refs))
+	if (set_head(remote_refs, transport->remote->follow_remote_head))
 		;
 		/*
 		 * Way too many cases where this can go wrong
diff --git a/remote.c b/remote.c
index 10104d11e3..5a768ddac2 100644
--- a/remote.c
+++ b/remote.c
@@ -514,6 +514,15 @@ static int handle_config(const char *key, const char *value,
 	} else if (!strcmp(subkey, "serveroption")) {
 		return parse_transport_option(key, value,
 					      &remote->server_options);
+	} else if (!strcmp(subkey, "followremotehead")) {
+		if (!strcmp(value, "never"))
+			remote->follow_remote_head = -1;
+		else if (!strcmp(value, "create"))
+			remote->follow_remote_head = 0;
+		else if (!strcmp(value, "warn"))
+			remote->follow_remote_head = 1;
+		else if (!strcmp(value, "always"))
+			remote->follow_remote_head = 2;
 	}
 	return 0;
 }
diff --git a/remote.h b/remote.h
index a7e5c4e07c..3ceadac820 100644
--- a/remote.h
+++ b/remote.h
@@ -107,6 +107,15 @@ struct remote {
 	char *http_proxy_authmethod;
 
 	struct string_list server_options;
+
+	/*
+	 * The setting for whether to update HEAD for the remote.
+	 * -1 never update
+	 * 0 create only (default)
+	 * 1 warn on change
+	 * 2 always update
+	 */
+	int follow_remote_head;
 };
 
 /**
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index 87698341f5..2467027d34 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -99,6 +99,108 @@ test_expect_success "fetch test remote HEAD change" '
 	branch=$(git rev-parse refs/remotes/origin/other) &&
 	test "z$head" = "z$branch"'
 
+test_expect_success "fetch test followRemoteHEAD never" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
+		git config set remote.origin.followRemoteHEAD "never" &&
+		git fetch &&
+		test_must_fail git rev-parse --verify refs/remotes/origin/HEAD
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD warn no change" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git rev-parse --verify refs/remotes/origin/other &&
+		git remote set-head origin other &&
+		git rev-parse --verify refs/remotes/origin/HEAD &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		git config set remote.origin.followRemoteHEAD "warn" &&
+		git fetch >output &&
+		echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
+			"but we have ${SQ}other${SQ} locally." >expect &&
+		echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change." >>expect &&
+		test_cmp expect output &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/other) &&
+		test "z$head" = "z$branch"
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD warn create" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
+		git config set remote.origin.followRemoteHEAD "warn" &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		output=$(git fetch) &&
+		test "z" = "z$output" &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/main) &&
+		test "z$head" = "z$branch"
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD warn detached" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
+		git update-ref refs/remotes/origin/HEAD HEAD &&
+		HEAD=$(git log --pretty="%H") &&
+		git config set remote.origin.followRemoteHEAD "warn" &&
+		git fetch >output &&
+		echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
+			"but we have a detached HEAD pointing to" \
+			"${SQ}${HEAD}${SQ} locally." >expect &&
+		echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change." >>expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD warn quiet" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git rev-parse --verify refs/remotes/origin/other &&
+		git remote set-head origin other &&
+		git rev-parse --verify refs/remotes/origin/HEAD &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		git config set remote.origin.followRemoteHEAD "warn" &&
+		output=$(git fetch --quiet) &&
+		test "z" = "z$output" &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/other) &&
+		test "z$head" = "z$branch"
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD always" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git rev-parse --verify refs/remotes/origin/other &&
+		git remote set-head origin other &&
+		git rev-parse --verify refs/remotes/origin/HEAD &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		git config set remote.origin.followRemoteHEAD "always" &&
+		git fetch &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/main) &&
+		test "z$head" = "z$branch"
+	)
+'
+
 test_expect_success 'fetch --prune on its own works as expected' '
 	cd "$D" &&
 	git clone . prune &&
-- 
2.47.1.295.ge630f003e1.dirty
Junio C Hamano· Nov 27, 2024, 13:46 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v1] fetch: add configuration for set_head behaviour

Bence Ferdinandy <bence@ferdinandy.com> writes:
Show 7 quoted lines
> Introduce a new setting, remote.$remote.followRemoteHEAD with four
> options:
>
>     - "never": do not ever do anything, not even create
>     - "create": the current behaviour, now the default behaviour
>     - "warn": print a message if remote and local HEAD is different
>     - "always": silently update HEAD on every change
That seems to be plenty of choices to please many classes of users.

Except for the one that I would want to use myself, which is "I understand their HEAD points at branch X right now; please warn when they flip their HEAD to a different branch, but until then please do nothing". That's somewhere between "never" and "warn".

Show 6 quoted lines
> @@ -1603,6 +1628,8 @@ static int set_head(const struct ref *remote_refs)
>  		string_list_append(&heads, strip_refshead(ref->name));
>  	}
>  
> +	if (follow_remote_head < 0)
> +		goto cleanup;

There is some "magical" value(s) that is/are negative; we will find out what they are later.

Show 5 quoted lines
> @@ -1614,6 +1641,7 @@ static int set_head(const struct ref *remote_refs)
>  	if (!head_name)
>  		goto cleanup;
>  	is_bare = is_bare_repository();
> +	create_only = follow_remote_head == 2 ? 0 : !is_bare;
There is one more "magical" value that follow_remote_head can take.
Show 13 quoted lines
> @@ -1626,9 +1654,14 @@ static int set_head(const struct ref *remote_refs)
>  		result = 1;
>  		goto cleanup;
>  	}
> -	if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
> -					"fetch", NULL, !is_bare))
> +	was_detached = refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
> +					"fetch", &b_local_head, create_only);
> +	if (was_detached == -1) {
>  		result = 1;
> +		goto cleanup;
> +	}
> +	if (follow_remote_head == 1 && verbosity >= 0)
And there is one more.
Show 17 quoted lines
> diff --git a/remote.c b/remote.c
> index 10104d11e3..5a768ddac2 100644
> --- a/remote.c
> +++ b/remote.c
> @@ -514,6 +514,15 @@ static int handle_config(const char *key, const char *value,
>  	} else if (!strcmp(subkey, "serveroption")) {
>  		return parse_transport_option(key, value,
>  					      &remote->server_options);
> +	} else if (!strcmp(subkey, "followremotehead")) {
> +		if (!strcmp(value, "never"))
> +			remote->follow_remote_head = -1;
> +		else if (!strcmp(value, "create"))
> +			remote->follow_remote_head = 0;
> +		else if (!strcmp(value, "warn"))
> +			remote->follow_remote_head = 1;
> +		else if (!strcmp(value, "always"))
> +			remote->follow_remote_head = 2;
Use something like
	/* The setting for whether to update HEAD for the remote. */
	enum follow_remote_head {
		FOLLOW_REMOTE_NEVER = -1,
		FOLLOW_REMOTE_CREATE = 0,
		FOLLOW_REMOTE_WARN = 1,
		FOLLOW_REMOTE_ALWAYS = 2,
	};

or something? I have no strong preference between "enum" and "#define" myself, but moderately strong preference for anything symbolic over magic numbers.

Show 18 quoted lines
> diff --git a/remote.h b/remote.h
> index a7e5c4e07c..3ceadac820 100644
> --- a/remote.h
> +++ b/remote.h
> @@ -107,6 +107,15 @@ struct remote {
>  	char *http_proxy_authmethod;
>  
>  	struct string_list server_options;
> +
> +	/*
> +	 * The setting for whether to update HEAD for the remote.
> +	 * -1 never update
> +	 * 0 create only (default)
> +	 * 1 warn on change
> +	 * 2 always update
> +	 */
> +	int follow_remote_head;
>  };
Other than that, looking good from a cursory read.
Thanks.
Bence Ferdinandy· Nov 27, 2024, 19:20 UTC · re: Junio C Hamano · lore

Re: [PATCH v1] fetch: add configuration for set_head behaviour

On Wed Nov 27, 2024 at 14:46, Junio C Hamano <gitster@pobox.com> wrote:
Show 16 quoted lines
> Bence Ferdinandy <bence@ferdinandy.com> writes:
>
>> Introduce a new setting, remote.$remote.followRemoteHEAD with four
>> options:
>>
>>     - "never": do not ever do anything, not even create
>>     - "create": the current behaviour, now the default behaviour
>>     - "warn": print a message if remote and local HEAD is different
>>     - "always": silently update HEAD on every change
>
> That seems to be plenty of choices to please many classes of users.
>
> Except for the one that I would want to use myself, which is "I
> understand their HEAD points at branch X right now; please warn when
> they flip their HEAD to a different branch, but until then please do
> nothing".  That's somewhere between "never" and "warn".
Just to clarify, an example: the remote's HEAD is set to "master", and you have
- git remote set-head origin next
- git config set remote.origin.followRemoteHead "manual"
- git config set remote.origin.followRemoteHeadManual "master"

you manually set these explicitly. As long as the remote's HEAD is still "master" you do not get a warning when running fetch, but if it changes to something else (even "next"), you _do_ get a warning, that is not silenced until you set followRemoteHeadManual to "next".

Would you expect `git remote set-head origin next` to set followRemoteHead to "manual" and to set the correct value for the followRemoteHeadManual to "master" if it actually changed the current refs/remote/origin/HEAD from master to next? Or is having this completely manual fine?

I toyed with the thought of rolling the two settings together (an unrecognized string would mean the reference for which we must be silent), but then you couldn't have a remote/HEAD called "create" for example, so I think we need to store that separately. I'm also not quite happy with "followRemoteHeadManual" ...

Show 66 quoted lines
>
>> @@ -1603,6 +1628,8 @@ static int set_head(const struct ref *remote_refs)
>>  		string_list_append(&heads, strip_refshead(ref->name));
>>  	}
>>  
>> +	if (follow_remote_head < 0)
>> +		goto cleanup;
>
> There is some "magical" value(s) that is/are negative; we will find
> out what they are later.
>
>> @@ -1614,6 +1641,7 @@ static int set_head(const struct ref *remote_refs)
>>  	if (!head_name)
>>  		goto cleanup;
>>  	is_bare = is_bare_repository();
>> +	create_only = follow_remote_head == 2 ? 0 : !is_bare;
>
> There is one more "magical" value that follow_remote_head can take.
>
>> @@ -1626,9 +1654,14 @@ static int set_head(const struct ref *remote_refs)
>>  		result = 1;
>>  		goto cleanup;
>>  	}
>> -	if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
>> -					"fetch", NULL, !is_bare))
>> +	was_detached = refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
>> +					"fetch", &b_local_head, create_only);
>> +	if (was_detached == -1) {
>>  		result = 1;
>> +		goto cleanup;
>> +	}
>> +	if (follow_remote_head == 1 && verbosity >= 0)
>
> And there is one more.
>
>> diff --git a/remote.c b/remote.c
>> index 10104d11e3..5a768ddac2 100644
>> --- a/remote.c
>> +++ b/remote.c
>> @@ -514,6 +514,15 @@ static int handle_config(const char *key, const char *value,
>>  	} else if (!strcmp(subkey, "serveroption")) {
>>  		return parse_transport_option(key, value,
>>  					      &remote->server_options);
>> +	} else if (!strcmp(subkey, "followremotehead")) {
>> +		if (!strcmp(value, "never"))
>> +			remote->follow_remote_head = -1;
>> +		else if (!strcmp(value, "create"))
>> +			remote->follow_remote_head = 0;
>> +		else if (!strcmp(value, "warn"))
>> +			remote->follow_remote_head = 1;
>> +		else if (!strcmp(value, "always"))
>> +			remote->follow_remote_head = 2;
>
> Use something like
>
> 	/* The setting for whether to update HEAD for the remote. */
> 	enum follow_remote_head {
> 		FOLLOW_REMOTE_NEVER = -1,
> 		FOLLOW_REMOTE_CREATE = 0,
> 		FOLLOW_REMOTE_WARN = 1,
> 		FOLLOW_REMOTE_ALWAYS = 2,
> 	};
>
> or something?  I have no strong preference between "enum" and
> "#define" myself, but moderately strong preference for anything
> symbolic over magic numbers.
Ah, my mistake sorry. Magic number bad.

Best, Bence

Junio C Hamano· Nov 28, 2024, 00:12 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v1] fetch: add configuration for set_head behaviour

"Bence Ferdinandy" <bence@ferdinandy.com> writes:
Show 9 quoted lines
> Just to clarify, an example: the remote's HEAD is set to "master", and you have
> - git remote set-head origin next
> - git config set remote.origin.followRemoteHead "manual"
> - git config set remote.origin.followRemoteHeadManual "master"
>
> you manually set these explicitly. As long as the remote's HEAD is still
> "master" you do not get a warning when running fetch, but if it changes to
> something else (even "next"), you _do_ get a warning, that is not silenced
> until you set followRemoteHeadManual to "next".
Yup.
> Would you expect `git remote set-head origin next` to set followRemoteHead to
> "manual" and to set the correct value for the followRemoteHeadManual to
> "master" if it actually changed the current refs/remote/origin/HEAD from master
> to next?
What I found missing is:
    "I know this is the value I expect them to have, because it was
    the value I last observed there. Please let me know when they
    changed their mind; I want to reconsider my position when it
    happens, and your warning me would help me to do so."

My running "remote set-head" to manually change which of their branches I am interested in does not tell Git anything about what branch I expect them to be pointing at with HEAD. It may be the action after such "reconsideration" of my position, but there is 0 bit of information on what I expect their HEAD to be pointing at.

> Or is having this completely manual fine?

If it can be automated, that would be nicer, but I do not think a manual "remote set-head origin next" gives any information to help automating it.

Show 5 quoted lines
> I toyed with the thought of rolling the two settings together (an unrecognized
> string would mean the reference for which we must be silent), but then you
> couldn't have a remote/HEAD called "create" for example, so I think we need to
> store that separately. I'm also not quite happy with "followRemoteHeadManual"
> ...

Yeah, I was thinking a value like "warn-if-not-pointing-at-$branch" where $branch part would be a token like 'bc/set-head-symref', 'master', etc.

I do not think any of the above should block this topic. It can be added later without disrupting all other modes implemented there.

Thanks.
Bence Ferdinandy· Nov 28, 2024, 05:49 UTC · re: Junio C Hamano · lore

Re: [PATCH v1] fetch: add configuration for set_head behaviour

On Thu Nov 28, 2024 at 01:12, Junio C Hamano <gitster@pobox.com> wrote:
Show 31 quoted lines
> "Bence Ferdinandy" <bence@ferdinandy.com> writes:
>
>> Just to clarify, an example: the remote's HEAD is set to "master", and you have
>> - git remote set-head origin next
>> - git config set remote.origin.followRemoteHead "manual"
>> - git config set remote.origin.followRemoteHeadManual "master"
>>
>> you manually set these explicitly. As long as the remote's HEAD is still
>> "master" you do not get a warning when running fetch, but if it changes to
>> something else (even "next"), you _do_ get a warning, that is not silenced
>> until you set followRemoteHeadManual to "next".
>
> Yup.
>
>> Would you expect `git remote set-head origin next` to set followRemoteHead to
>> "manual" and to set the correct value for the followRemoteHeadManual to
>> "master" if it actually changed the current refs/remote/origin/HEAD from master
>> to next?
>
> What I found missing is:
>
>     "I know this is the value I expect them to have, because it was
>     the value I last observed there. Please let me know when they
>     changed their mind; I want to reconsider my position when it
>     happens, and your warning me would help me to do so."
>
> My running "remote set-head" to manually change which of their
> branches I am interested in does not tell Git anything about what
> branch I expect them to be pointing at with HEAD.  It may be the
> action after such "reconsideration" of my position, but there is 0
> bit of information on what I expect their HEAD to be pointing at.

Hmm. After a bit more thought: running `remote set-head` doesn't make much sense if you have "always" and makes a bit of sense if you have "warn". So maybe one thing set-head could do is _if_ you have always it could drop you to "warn" and "warn" could also include a line not just about following with "remote set-head" but something like "You can either follow with set-head, or you can turn this off but still get a warning if origin changes by setting ...".

Show 16 quoted lines
>
>> Or is having this completely manual fine?
>
> If it can be automated, that would be nicer, but I do not think a
> manual "remote set-head origin next" gives any information to help
> automating it.
>
>> I toyed with the thought of rolling the two settings together (an unrecognized
>> string would mean the reference for which we must be silent), but then you
>> couldn't have a remote/HEAD called "create" for example, so I think we need to
>> store that separately. I'm also not quite happy with "followRemoteHeadManual"
>> ...
>
> Yeah, I was thinking a value like "warn-if-not-pointing-at-$branch"
> where $branch part would be a token like 'bc/set-head-symref',
> 'master', etc.
Good idea. Probably shorter though like "warn-if-not-$branch".
>
> I do not think any of the above should block this topic.  It can be
> added later without disrupting all other modes implemented there.
Ack, I'll send a respin with just enum fixed for now.

Thanks, Bence

Junio C Hamano· Nov 28, 2024, 06:06 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v1] fetch: add configuration for set_head behaviour

"Bence Ferdinandy" <bence@ferdinandy.com> writes:
> Hmm. After a bit more thought: running `remote set-head` doesn't make much
> sense if you have "always" and makes a bit of sense if you have "warn".

Yeah, after "warn" tells the user what branch the remote points at, the user may decide to match. Or the user may decide to flip the remote-tracking HEAD to something unrelated. In the former case, the next and subsequent fetch will not warn, until the remote flips the HEAD again. In the latter case, the next and subsequent fetch will warn, until they happen to change their HEAD again and their choice happens to match ours.

Show 6 quoted lines
> So
> maybe one thing set-head could do is _if_ you have always it could drop you to
> "warn" and "warn" could also include a line not just about following with
> "remote set-head" but something like "You can either follow with set-head, or
> you can turn this off but still get a warning if origin changes by setting
> ...".

Hmph, if you've been happy with "always", if we notice that the remote flipped its HEAD, we switch to "warn" and the next and subsequent fetch will not warn, until they flip again, and then we will keep warning.

If you do not want to follow (but still monitor origin), the above would not be sufficient, unless "by setting..." part gives a new choice via your .followRemoteHEADManual setting.

Show 5 quoted lines
>> Yeah, I was thinking a value like "warn-if-not-pointing-at-$branch"
>> where $branch part would be a token like 'bc/set-head-symref',
>> 'master', etc.
>
> Good idea. Probably shorter though like "warn-if-not-$branch".

Sorry, I wasn't offering an alternative I thought was better. It was "we could do this, if we really want to avoid two variables". But thinking about it further, not having to worry about two variables may be a huge benefit. If you set .followRemoteHEAD to "manual" and .followRemoteHEADManual to some value, and then changed .followRemoteHEAD to something else, you may be very likely to forget unsetting .followRemoteHEADManual that is no longer in effect.

>> I do not think any of the above should block this topic.  It can be
>> added later without disrupting all other modes implemented there.
>
> Ack, I'll send a respin with just enum fixed for now.
Thanks.
Bence Ferdinandy· Dec 3, 2024, 21:56 UTC · re: Junio C Hamano · lore

[RFC PATCH v1 0/2] set_head finishing touches

This series is intended to be the finishing touches on the fetch/remote set_head changes as in built off of bf/fetch-set-head-config <20241128230806.2058962-1-bence@ferdinandy.com>.

Patch 1/2 contains the missing setting Junio was missing, while 2/2 is an attempt to do the right thing.

I've marked them as RFC because I'm not sure about a couple of things:
- 1/2 contains a slight refactor by extracting the hints into a separate
  function to avoid writing it twice. It could be argued that this
  refactor could be in a separate patch.
- I'm not exactly sure what should be in this hint, I added the new
  "warn-if-not-$branch", but arguments could be made for adding
  "always", adding a generic "or see the other possible settings in the
  manual" or any other combination
- I've seen some examples of advice stuff in yellow that can be
  disabled, so maybe a complete explanation of possibilites could be
  done with that mechanism (I haven't yet looked into how that works)
- I'm also not entirely sure about 2/2. I think it's a safe move, that
  is very unlikely to be annoying, but it's hard to judge from my
  limited perspective on all the different possible workflows.
Bence Ferdinandy (2):
  fetch set_head: add warn-if-not-$branch option
  remote set-head: set followRemoteHEAD to "warn" if "always"
 builtin/fetch.c   | 26 ++++++++++++++++++-------
 builtin/remote.c  | 12 +++++++++++-
 remote.c          |  5 +++++
 remote.h          |  6 ++++--
 t/t5505-remote.sh | 11 +++++++++++
 t/t5510-fetch.sh  | 49 +++++++++++++++++++++++++++++++++++++++++++++--
 6 files changed, 97 insertions(+), 12 deletions(-)
-- 
2.47.1.297.g6455f89743
Bence Ferdinandy· Dec 3, 2024, 21:56 UTC · re: Bence Ferdinandy · lore

[RFC PATCH v1 1/2] fetch set_head: add warn-if-not-$branch option

Currently if we want to have a remote/HEAD locally that is different from the one on the remote, but we still want to get a warning if remote changes HEAD, our only option is to have an indiscriminate warning with "follow_remote_head" set to "warn". Add a new option "warn-if-not-$branch", where $branch is a branch name we do not wish to get a warning about. If the remote HEAD is $branch do not warn, otherwise, behave as "warn".

E.g. let's assume, that our remote origin has HEAD set to "master", but locally we have "git remote set-head origin seen". Setting 'remote.origin.followRemoteHEAD = "warn"' will always print a warning, even though the remote has not changed HEAD from "master". Setting 'remote.origin.followRemoteHEAD = "warn-if-not-master" will squelch the warning message, unless the remote changes HEAD from "master". Note, that should the remote change HEAD to "seen" (which we have locally), there will still be no warning.

Improve the advice message in report_set_head to also include silencing the warning message with "warn-if-not-$branch".

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
 builtin/fetch.c  | 26 ++++++++++++++++++-------
 remote.c         |  5 +++++
 remote.h         |  6 ++++--
 t/t5510-fetch.sh | 49 ++++++++++++++++++++++++++++++++++++++++++++++--
 4 files changed, 75 insertions(+), 11 deletions(-)
Show changes to 4 files +75 −11

builtin/fetch.c, remote.c, remote.h, t/t5510-fetch.sh

diff --git a/builtin/fetch.c b/builtin/fetch.c
index 88c5c5d781..fd7f3694cc 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1579,6 +1579,15 @@ static const char *strip_refshead(const char *name){
 	return name;
 }
 
+static void set_head_advice_msg(const char *remote, const char *head_name) {
+
+	printf("Run 'git remote set-head %s %s' to follow the change, or\n"
+		"'git config set remote.%s.warn-if-not-%s' to disable this warning\n"
+		"until the remote changes HEAD again.\n",
+		remote, head_name, remote, head_name);
+
+}
+
 static void report_set_head(const char *remote, const char *head_name,
 			struct strbuf *buf_prev, int updateres) {
 	struct strbuf buf_prefix = STRBUF_INIT;
@@ -1590,20 +1599,19 @@ static void report_set_head(const char *remote, const char *head_name,
 	if (prev_head && strcmp(prev_head, head_name)) {
 		printf("'HEAD' at '%s' is '%s', but we have '%s' locally.\n",
 			remote, head_name, prev_head);
-		printf("Run 'git remote set-head %s %s' to follow the change.\n",
-			remote, head_name);
+		set_head_advice_msg(remote, head_name);
 	}
 	else if (updateres && buf_prev->len) {
 		printf("'HEAD' at '%s' is '%s', "
 			"but we have a detached HEAD pointing to '%s' locally.\n",
 			remote, head_name, buf_prev->buf);
-		printf("Run 'git remote set-head %s %s' to follow the change.\n",
-			remote, head_name);
+		set_head_advice_msg(remote, head_name);
 	}
 	strbuf_release(&buf_prefix);
 }
 
-static int set_head(const struct ref *remote_refs, int follow_remote_head)
+static int set_head(const struct ref *remote_refs, int follow_remote_head,
+		const char *no_warn_branch)
 {
 	int result = 0, create_only, is_bare, was_detached;
 	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
@@ -1660,7 +1668,10 @@ static int set_head(const struct ref *remote_refs, int follow_remote_head)
 		result = 1;
 		goto cleanup;
 	}
-	if (follow_remote_head == FOLLOW_REMOTE_WARN && verbosity >= 0)
+	if ((follow_remote_head == FOLLOW_REMOTE_WARN ||
+		(follow_remote_head == FOLLOW_REMOTE_WARN_IF_NOT_BRANCH &&
+		strcmp(no_warn_branch, head_name))
+		) && verbosity >= 0)
 		report_set_head(remote, head_name, &b_local_head, was_detached);
 
 cleanup:
@@ -1889,7 +1900,8 @@ static int do_fetch(struct transport *transport,
 				  "you need to specify exactly one branch with the --set-upstream option"));
 		}
 	}
-	if (set_head(remote_refs, transport->remote->follow_remote_head))
+	if (set_head(remote_refs, transport->remote->follow_remote_head,
+		transport->remote->no_warn_branch))
 		;
 		/*
 		 * Way too many cases where this can go wrong
diff --git a/remote.c b/remote.c
index 0b18840d43..f0e1b1b76a 100644
--- a/remote.c
+++ b/remote.c
@@ -515,6 +515,7 @@ static int handle_config(const char *key, const char *value,
 		return parse_transport_option(key, value,
 					      &remote->server_options);
 	} else if (!strcmp(subkey, "followremotehead")) {
+		const char *no_warn_branch;
 		if (!strcmp(value, "never"))
 			remote->follow_remote_head = FOLLOW_REMOTE_NEVER;
 		else if (!strcmp(value, "create"))
@@ -523,6 +524,10 @@ static int handle_config(const char *key, const char *value,
 			remote->follow_remote_head = FOLLOW_REMOTE_WARN;
 		else if (!strcmp(value, "always"))
 			remote->follow_remote_head = FOLLOW_REMOTE_ALWAYS;
+		else if (skip_prefix(value, "warn-if-not-", &no_warn_branch)) {
+			remote->follow_remote_head = FOLLOW_REMOTE_WARN_IF_NOT_BRANCH;
+			remote->no_warn_branch = no_warn_branch;
+		}
 	}
 	return 0;
 }
diff --git a/remote.h b/remote.h
index 184b35653d..75be3977ba 100644
--- a/remote.h
+++ b/remote.h
@@ -62,8 +62,9 @@ struct remote_state *remote_state_new(void);
 	enum follow_remote_head_settings {
 		FOLLOW_REMOTE_NEVER = -1,
 		FOLLOW_REMOTE_CREATE = 0,
-		FOLLOW_REMOTE_WARN = 1,
-		FOLLOW_REMOTE_ALWAYS = 2,
+		FOLLOW_REMOTE_WARN_IF_NOT_BRANCH = 1,
+		FOLLOW_REMOTE_WARN = 2,
+		FOLLOW_REMOTE_ALWAYS = 3,
 	};
 
 struct remote {
@@ -116,6 +117,7 @@ struct remote {
 	struct string_list server_options;
 
 	enum follow_remote_head_settings follow_remote_head;
+	const char *no_warn_branch;
 };
 
 /**
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index 2467027d34..be0c60be2c 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -124,7 +124,9 @@ test_expect_success "fetch test followRemoteHEAD warn no change" '
 		git fetch >output &&
 		echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
 			"but we have ${SQ}other${SQ} locally." >expect &&
-		echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change." >>expect &&
+		echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change, or" >>expect &&
+		echo "${SQ}git config set remote.origin.warn-if-not-main${SQ} to disable this warning" >>expect &&
+		echo "until the remote changes HEAD again." >>expect &&
 		test_cmp expect output &&
 		head=$(git rev-parse refs/remotes/origin/HEAD) &&
 		branch=$(git rev-parse refs/remotes/origin/other) &&
@@ -161,7 +163,9 @@ test_expect_success "fetch test followRemoteHEAD warn detached" '
 		echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
 			"but we have a detached HEAD pointing to" \
 			"${SQ}${HEAD}${SQ} locally." >expect &&
-		echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change." >>expect &&
+		echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change, or" >>expect &&
+		echo "${SQ}git config set remote.origin.warn-if-not-main${SQ} to disable this warning" >>expect &&
+		echo "until the remote changes HEAD again." >>expect &&
 		test_cmp expect output
 	)
 '
@@ -184,6 +188,47 @@ test_expect_success "fetch test followRemoteHEAD warn quiet" '
 	)
 '
 
+test_expect_success "fetch test followRemoteHEAD warn-if-not-branch branch is same" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git rev-parse --verify refs/remotes/origin/other &&
+		git remote set-head origin other &&
+		git rev-parse --verify refs/remotes/origin/HEAD &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		git config set remote.origin.followRemoteHEAD "warn-if-not-main" &&
+		output=$(git fetch) &&
+		test "z" = "z$output" &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/other) &&
+		test "z$head" = "z$branch"
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD warn-if-not-branch branch is different" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git rev-parse --verify refs/remotes/origin/other &&
+		git remote set-head origin other &&
+		git rev-parse --verify refs/remotes/origin/HEAD &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		git config set remote.origin.followRemoteHEAD "warn-if-not-some/different-branch" &&
+		git fetch >output &&
+		echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
+			"but we have ${SQ}other${SQ} locally." >expect &&
+		echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change, or" >>expect &&
+		echo "${SQ}git config set remote.origin.warn-if-not-main${SQ} to disable this warning" >>expect &&
+		echo "until the remote changes HEAD again." >>expect &&
+		test_cmp expect output &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/other) &&
+		test "z$head" = "z$branch"
+	)
+'
+
 test_expect_success "fetch test followRemoteHEAD always" '
 	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
 	(
-- 
2.47.1.297.g6455f89743
Junio C Hamano· Dec 4, 2024, 02:20 UTC · re: Bence Ferdinandy · lore

Re: [RFC PATCH v1 1/2] fetch set_head: add warn-if-not-$branch option

Bence Ferdinandy <bence@ferdinandy.com> writes:
Show 8 quoted lines
> +static void set_head_advice_msg(const char *remote, const char *head_name) {
> +
> +	printf("Run 'git remote set-head %s %s' to follow the change, or\n"
> +		"'git config set remote.%s.warn-if-not-%s' to disable this warning\n"
> +		"until the remote changes HEAD again.\n",
> +		remote, head_name, remote, head_name);
> +
> +}

Style. "{" that encloses the function body sits on a line of its own.

Perhaps use the advise_if_enabled(), so that those who already learned how to deal with the situation can squelch the "how to fix" message.

Show 15 quoted lines
> -static int set_head(const struct ref *remote_refs, int follow_remote_head)
> +static int set_head(const struct ref *remote_refs, int follow_remote_head,
> +		const char *no_warn_branch)
>  {
>  	int result = 0, create_only, is_bare, was_detached;
>  	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
> @@ -1660,7 +1668,10 @@ static int set_head(const struct ref *remote_refs, int follow_remote_head)
>  		result = 1;
>  		goto cleanup;
>  	}
> -	if (follow_remote_head == FOLLOW_REMOTE_WARN && verbosity >= 0)
> +	if ((follow_remote_head == FOLLOW_REMOTE_WARN ||
> +		(follow_remote_head == FOLLOW_REMOTE_WARN_IF_NOT_BRANCH &&
> +		strcmp(no_warn_branch, head_name))
> +		) && verbosity >= 0)

Reorder conditions combined with &&- to have more expensive ones later.

	if (verbosity >= 0 &&
            (follow_remote_head == FOLLOW_REMOTE_WARN ||
	    (follow_remote_head == FOLLOW_REMOTE_WARN_IF_NOT_BRANCH &&
	     strcmp(no_warn_branch, head_name)))

As human readers, we may know that no_warn_branch is always valid when (follow_remote_head == FOLLOW_REMOTE_WARN_IF_NOT_BRANCH), but semi clever compilers may not realize it and give a false warning about using no_warn_branch potentially uninitialized.

We could do without adding FOLLOW_REMOTE_WARN_IF_NOT_BRANCH and reuse FOLLOW_REMOTE_WARN, like so:

	if (verbosity >= 0 &&
            follow_remote_head == FOLLOW_REMOTE_WARN &&
	    (!no_warn_branch || strcmp(no_warn_branch, head_name)))

That is, "if set to remote-warn, always warn, or no_warn_branch is not NULL, only warn if the current head is different from it".

Show 20 quoted lines
> diff --git a/remote.c b/remote.c
> index 0b18840d43..f0e1b1b76a 100644
> --- a/remote.c
> +++ b/remote.c
> @@ -515,6 +515,7 @@ static int handle_config(const char *key, const char *value,
>  		return parse_transport_option(key, value,
>  					      &remote->server_options);
>  	} else if (!strcmp(subkey, "followremotehead")) {
> +		const char *no_warn_branch;
>  		if (!strcmp(value, "never"))
>  			remote->follow_remote_head = FOLLOW_REMOTE_NEVER;
>  		else if (!strcmp(value, "create"))
> @@ -523,6 +524,10 @@ static int handle_config(const char *key, const char *value,
>  			remote->follow_remote_head = FOLLOW_REMOTE_WARN;
>  		else if (!strcmp(value, "always"))
>  			remote->follow_remote_head = FOLLOW_REMOTE_ALWAYS;
> +		else if (skip_prefix(value, "warn-if-not-", &no_warn_branch)) {
> +			remote->follow_remote_head = FOLLOW_REMOTE_WARN_IF_NOT_BRANCH;
> +			remote->no_warn_branch = no_warn_branch;
> +		}

If we were to do without FOLLOW_REMOTE_WARN_IF_NOT_BRANCH, then the above becomes

			remote->follow_remote_head = FOLLOW_REMOTE_WARN;
			remote->no_warn_branch = NULL;
		} else if (skip_prefix(value, "warn-if-not-", &no_warn_branch)) {
			remote->follow_remote_head = FOLLOW_REMOTE_WARN;
			remote->no_warn_branch = no_warn_branch;
		} else if (!strcmp(value, "always")) {
			remote->follow_remote_head = FOLLOW_REMOTE_ALWAYS;
		} else {
			warn(_("unrecognized followRemoteHEAD value '%s' ignored"),
			     value);
		}

We'd want the new choice documented before we graduate this topic out of the RFC status.

Thanks.
Bence Ferdinandy· Dec 4, 2024, 08:15 UTC · re: Junio C Hamano · lore

Re: [RFC PATCH v1 1/2] fetch set_head: add warn-if-not-$branch option

On Wed Dec 04, 2024 at 03:20, Junio C Hamano <gitster@pobox.com> wrote:
Show 56 quoted lines
> Bence Ferdinandy <bence@ferdinandy.com> writes:
>
>> +static void set_head_advice_msg(const char *remote, const char *head_name) {
>> +
>> +	printf("Run 'git remote set-head %s %s' to follow the change, or\n"
>> +		"'git config set remote.%s.warn-if-not-%s' to disable this warning\n"
>> +		"until the remote changes HEAD again.\n",
>> +		remote, head_name, remote, head_name);
>> +
>> +}
>
> Style. "{" that encloses the function body sits on a line of its
> own.
>
> Perhaps use the advise_if_enabled(), so that those who already
> learned how to deal with the situation can squelch the "how to fix"
> message.
>
>> -static int set_head(const struct ref *remote_refs, int follow_remote_head)
>> +static int set_head(const struct ref *remote_refs, int follow_remote_head,
>> +		const char *no_warn_branch)
>>  {
>>  	int result = 0, create_only, is_bare, was_detached;
>>  	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
>> @@ -1660,7 +1668,10 @@ static int set_head(const struct ref *remote_refs, int follow_remote_head)
>>  		result = 1;
>>  		goto cleanup;
>>  	}
>> -	if (follow_remote_head == FOLLOW_REMOTE_WARN && verbosity >= 0)
>> +	if ((follow_remote_head == FOLLOW_REMOTE_WARN ||
>> +		(follow_remote_head == FOLLOW_REMOTE_WARN_IF_NOT_BRANCH &&
>> +		strcmp(no_warn_branch, head_name))
>> +		) && verbosity >= 0)
>
> Reorder conditions combined with &&- to have more expensive ones
> later.
>
> 	if (verbosity >= 0 &&
>             (follow_remote_head == FOLLOW_REMOTE_WARN ||
> 	    (follow_remote_head == FOLLOW_REMOTE_WARN_IF_NOT_BRANCH &&
> 	     strcmp(no_warn_branch, head_name)))
>
> As human readers, we may know that no_warn_branch is always valid
> when (follow_remote_head == FOLLOW_REMOTE_WARN_IF_NOT_BRANCH), but
> semi clever compilers may not realize it and give a false warning
> about using no_warn_branch potentially uninitialized.
>
> We could do without adding FOLLOW_REMOTE_WARN_IF_NOT_BRANCH and reuse
> FOLLOW_REMOTE_WARN, like so:
>
> 	if (verbosity >= 0 &&
>             follow_remote_head == FOLLOW_REMOTE_WARN &&
> 	    (!no_warn_branch || strcmp(no_warn_branch, head_name)))
>
> That is, "if set to remote-warn, always warn, or no_warn_branch is
> not NULL, only warn if the current head is different from it".

Ah, nice, this has the added benefit of a bad configuration where "warn-if-not-" doesn't actually specify a branch to fall back to just "warn".

Show 41 quoted lines
>
>> diff --git a/remote.c b/remote.c
>> index 0b18840d43..f0e1b1b76a 100644
>> --- a/remote.c
>> +++ b/remote.c
>> @@ -515,6 +515,7 @@ static int handle_config(const char *key, const char *value,
>>  		return parse_transport_option(key, value,
>>  					      &remote->server_options);
>>  	} else if (!strcmp(subkey, "followremotehead")) {
>> +		const char *no_warn_branch;
>>  		if (!strcmp(value, "never"))
>>  			remote->follow_remote_head = FOLLOW_REMOTE_NEVER;
>>  		else if (!strcmp(value, "create"))
>> @@ -523,6 +524,10 @@ static int handle_config(const char *key, const char *value,
>>  			remote->follow_remote_head = FOLLOW_REMOTE_WARN;
>>  		else if (!strcmp(value, "always"))
>>  			remote->follow_remote_head = FOLLOW_REMOTE_ALWAYS;
>> +		else if (skip_prefix(value, "warn-if-not-", &no_warn_branch)) {
>> +			remote->follow_remote_head = FOLLOW_REMOTE_WARN_IF_NOT_BRANCH;
>> +			remote->no_warn_branch = no_warn_branch;
>> +		}
>
> If we were to do without FOLLOW_REMOTE_WARN_IF_NOT_BRANCH, then the
> above becomes
>
> 			remote->follow_remote_head = FOLLOW_REMOTE_WARN;
> 			remote->no_warn_branch = NULL;
> 		} else if (skip_prefix(value, "warn-if-not-", &no_warn_branch)) {
> 			remote->follow_remote_head = FOLLOW_REMOTE_WARN;
> 			remote->no_warn_branch = no_warn_branch;
> 		} else if (!strcmp(value, "always")) {
> 			remote->follow_remote_head = FOLLOW_REMOTE_ALWAYS;
> 		} else {
> 			warn(_("unrecognized followRemoteHEAD value '%s' ignored"),
> 			     value);
> 		}
>
> We'd want the new choice documented before we graduate this topic
> out of the RFC status.
>
> Thanks.
-- 
bence.ferdinandy.com
Bence Ferdinandy· Dec 3, 2024, 21:56 UTC · re: Bence Ferdinandy · lore

[RFC PATCH v1 2/2] remote set-head: set followRemoteHEAD to "warn" if "always"

When running "remote set-head" manually it is unlikely, that the user would actually like to have "fetch" always update the remote/HEAD. On the contrary, it is more likely, that the user would expect remote/HEAD to stay the way they manually set it, and just forgot about having "followRemoteHEAD" set to "always".

When "followRemoteHEAD" is set to "always" make running "remote set-head" change the config to "warn".

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
 builtin/remote.c  | 12 +++++++++++-
 t/t5505-remote.sh | 11 +++++++++++
 2 files changed, 22 insertions(+), 1 deletion(-)
Show changes to 2 files +22 −1

builtin/remote.c, t/t5505-remote.sh

diff --git a/builtin/remote.c b/builtin/remote.c
index 4a8b2ef678..9a30c17724 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1433,6 +1433,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		b_local_head = STRBUF_INIT;
 	char *head_name = NULL;
 	struct ref_store *refs = get_main_ref_store(the_repository);
+	struct remote *remote;
 
 	struct option options[] = {
 		OPT_BOOL('a', "auto", &opt_a,
@@ -1443,8 +1444,10 @@ static int set_head(int argc, const char **argv, const char *prefix)
 	};
 	argc = parse_options(argc, argv, prefix, options,
 			     builtin_remote_sethead_usage, 0);
-	if (argc)
+	if (argc) {
 		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", argv[0]);
+		remote = remote_get(argv[0]);
+	}
 
 	if (!opt_a && !opt_d && argc == 2) {
 		head_name = xstrdup(argv[1]);
@@ -1483,6 +1486,13 @@ static int set_head(int argc, const char **argv, const char *prefix)
 	}
 	if (opt_a)
 		report_set_head_auto(argv[0], head_name, &b_local_head, was_detached);
+	if (remote->follow_remote_head == FOLLOW_REMOTE_ALWAYS) {
+		struct strbuf config_name = STRBUF_INIT;
+		strbuf_addf(&config_name,
+			"remote.%s.followremotehead", remote->name);
+		git_config_set(config_name.buf, "warn");
+		strbuf_release(&config_name);
+	}
 
 cleanup:
 	free(head_name);
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 2600add82a..76ffdb2ec9 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -505,6 +505,17 @@ test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
 	)
 '
 
+test_expect_success 'set-head changes followRemoteHEAD always to warn' '
+	(
+		cd test &&
+		git config set remote.origin.followRemoteHEAD "always" &&
+		git remote set-head --auto origin &&
+		git config get remote.origin.followRemoteHEAD >output &&
+		echo "warn" >expect &&
+		test_cmp expect output
+	)
+'
+
 cat >test/expect <<\EOF
 refs/remotes/origin/side2
 EOF
-- 
2.47.1.297.g6455f89743
Junio C Hamano· Dec 4, 2024, 02:22 UTC · re: Bence Ferdinandy · lore

Re: [RFC PATCH v1 2/2] remote set-head: set followRemoteHEAD to "warn" if "always"

Bence Ferdinandy <bence@ferdinandy.com> writes:
Show 8 quoted lines
> When running "remote set-head" manually it is unlikely, that the user
> would actually like to have "fetch" always update the remote/HEAD. On
> the contrary, it is more likely, that the user would expect remote/HEAD
> to stay the way they manually set it, and just forgot about having
> "followRemoteHEAD" set to "always".
>
> When "followRemoteHEAD" is set to "always" make running "remote
> set-head" change the config to "warn".

If we were to take [1/2] of this series, then another plausible option is to set it to whatever their current value is, but that would require an extra probing of the remote state, so "warn" is probably a reasonable choice here.

Thanks.
Bence Ferdinandy· Dec 4, 2024, 10:39 UTC · re: Bence Ferdinandy · lore

[PATCH v2 1/3] fetch set_head: move warn advice into advise_if_enabled

Advice about what to do when getting a warning is typed out explicitly twice and is printed as regular output. The output is also tested for. Extract the advice message into a single place and use a wrapper function, so if later the advice is made more chatty the signature only needs to be changed in once place. Remove the testing for the advice output in the tests.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v2: new patch, with the refactor moved out from the following one
 advice.c         |  1 +
 advice.h         |  1 +
 builtin/fetch.c  | 17 +++++++++++++----
 t/t5510-fetch.sh |  2 --
 4 files changed, 15 insertions(+), 6 deletions(-)
Show changes to 4 files +15 −6

advice.c, advice.h, builtin/fetch.c, t/t5510-fetch.sh

diff --git a/advice.c b/advice.c
index 6b879d805c..66461fdce9 100644
--- a/advice.c
+++ b/advice.c
@@ -53,6 +53,7 @@ static struct {
 	[ADVICE_COMMIT_BEFORE_MERGE]			= { "commitBeforeMerge" },
 	[ADVICE_DETACHED_HEAD]				= { "detachedHead" },
 	[ADVICE_DIVERGING]				= { "diverging" },
+	[ADVICE_FETCH_SET_HEAD_WARN]			= { "fetchRemoteHEADWarn" },
 	[ADVICE_FETCH_SHOW_FORCED_UPDATES]		= { "fetchShowForcedUpdates" },
 	[ADVICE_FORCE_DELETE_BRANCH]			= { "forceDeleteBranch" },
 	[ADVICE_GRAFT_FILE_DEPRECATED]			= { "graftFileDeprecated" },
diff --git a/advice.h b/advice.h
index d7466bc0ef..cf2284ec43 100644
--- a/advice.h
+++ b/advice.h
@@ -20,6 +20,7 @@ enum advice_type {
 	ADVICE_COMMIT_BEFORE_MERGE,
 	ADVICE_DETACHED_HEAD,
 	ADVICE_DIVERGING,
+	ADVICE_FETCH_SET_HEAD_WARN,
 	ADVICE_FETCH_SHOW_FORCED_UPDATES,
 	ADVICE_FORCE_DELETE_BRANCH,
 	ADVICE_GRAFT_FILE_DEPRECATED,
diff --git a/builtin/fetch.c b/builtin/fetch.c
index 62769d1686..087beb4c92 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1579,6 +1579,17 @@ static const char *strip_refshead(const char *name){
 	return name;
 }
 
+static void set_head_advice_msg(const char *remote, const char *head_name)
+{
+	const char message_advice_set_head[] =
+	N_("Run 'git remote set-head %s %s' to follow the change, or set\n"
+	   "'remote.%s.followRemoteHEAD' configuration option to a different value\n"
+	   "if you do not want to see this message.");
+
+	advise_if_enabled(ADVICE_FETCH_SET_HEAD_WARN, _(message_advice_set_head),
+			remote, head_name, remote);
+}
+
 static void report_set_head(const char *remote, const char *head_name,
 			struct strbuf *buf_prev, int updateres) {
 	struct strbuf buf_prefix = STRBUF_INIT;
@@ -1590,15 +1601,13 @@ static void report_set_head(const char *remote, const char *head_name,
 	if (prev_head && strcmp(prev_head, head_name)) {
 		printf("'HEAD' at '%s' is '%s', but we have '%s' locally.\n",
 			remote, head_name, prev_head);
-		printf("Run 'git remote set-head %s %s' to follow the change.\n",
-			remote, head_name);
+		set_head_advice_msg(remote, head_name);
 	}
 	else if (updateres && buf_prev->len) {
 		printf("'HEAD' at '%s' is '%s', "
 			"but we have a detached HEAD pointing to '%s' locally.\n",
 			remote, head_name, buf_prev->buf);
-		printf("Run 'git remote set-head %s %s' to follow the change.\n",
-			remote, head_name);
+		set_head_advice_msg(remote, head_name);
 	}
 	strbuf_release(&buf_prefix);
 }
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index ccb74428bc..5d7ee1b550 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -123,7 +123,6 @@ test_expect_success "fetch test followRemoteHEAD warn no change" '
 		git fetch >output &&
 		echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
 			"but we have ${SQ}other${SQ} locally." >expect &&
-		echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change." >>expect &&
 		test_cmp expect output &&
 		head=$(git rev-parse refs/remotes/origin/HEAD) &&
 		branch=$(git rev-parse refs/remotes/origin/other) &&
@@ -160,7 +159,6 @@ test_expect_success "fetch test followRemoteHEAD warn detached" '
 		echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
 			"but we have a detached HEAD pointing to" \
 			"${SQ}${HEAD}${SQ} locally." >expect &&
-		echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change." >>expect &&
 		test_cmp expect output
 	)
 '
-- 
2.47.1.398.g18e7475ebe
Bence Ferdinandy· Dec 4, 2024, 10:39 UTC · re: Bence Ferdinandy · lore

[PATCH v2 2/3] fetch set_head: add warn-if-not-$branch option

Currently if we want to have a remote/HEAD locally that is different from the one on the remote, but we still want to get a warning if remote changes HEAD, our only option is to have an indiscriminate warning with "follow_remote_head" set to "warn". Add a new option "warn-if-not-$branch", where $branch is a branch name we do not wish to get a warning about. If the remote HEAD is $branch do not warn, otherwise, behave as "warn".

E.g. let's assume, that our remote origin has HEAD set to "master", but locally we have "git remote set-head origin seen". Setting 'remote.origin.followRemoteHEAD = "warn"' will always print a warning, even though the remote has not changed HEAD from "master". Setting 'remote.origin.followRemoteHEAD = "warn-if-not-master" will squelch the warning message, unless the remote changes HEAD from "master". Note, that should the remote change HEAD to "seen" (which we have locally), there will still be no warning.

Improve the advice message in report_set_head to also include silencing the warning message with "warn-if-not-$branch".

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v2: reuse FOLLOW_REMOTE_WARN
        add documentation
 Documentation/config/remote.txt |  8 ++++---
 builtin/fetch.c                 | 16 +++++++++-----
 remote.c                        | 13 +++++++++--
 remote.h                        |  1 +
 t/t5510-fetch.sh                | 38 +++++++++++++++++++++++++++++++++
 5 files changed, 66 insertions(+), 10 deletions(-)
Show changes to 5 files +66 −10

Documentation/config/remote.txt, builtin/fetch.c, remote.c, remote.h, t/t5510-fetch.sh

diff --git a/Documentation/config/remote.txt b/Documentation/config/remote.txt
index 024f92befc..4118c219c1 100644
--- a/Documentation/config/remote.txt
+++ b/Documentation/config/remote.txt
@@ -106,10 +106,12 @@ remote.<name>.followRemoteHEAD::
 	How linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`.
 	The default value is "create", which will create `remotes/<name>/HEAD`
 	if it exists on the remote, but not locally, but will not touch an
-	already existing local reference.  Setting to "warn" will print
+	already existing local reference. Setting to "warn" will print
 	a message if the remote has a different value, than the local one and
-	in case there is no local reference, it behaves like "create". Setting
-	to "always" will silently update it to the value on the remote.
+	in case there is no local reference, it behaves like "create".
+	A variant on "warn" is "warn-if-not-$branch", which behaves like
+	"warn", but if `HEAD` on the remote is `$branch` it will be silent.
+	Setting to "always" will silently update it to the value on the remote.
 	Finally, setting it to "never" will never change or create the local
 	reference.
 +
diff --git a/builtin/fetch.c b/builtin/fetch.c
index 087beb4c92..b3f6793026 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1584,10 +1584,12 @@ static void set_head_advice_msg(const char *remote, const char *head_name)
 	const char message_advice_set_head[] =
 	N_("Run 'git remote set-head %s %s' to follow the change, or set\n"
 	   "'remote.%s.followRemoteHEAD' configuration option to a different value\n"
-	   "if you do not want to see this message.");
+	   "if you do not want to see this message. Specifically running\n"
+	   "'git config set remote.%s.followRemoteHEAD %s' will disable the warning\n"
+	   "until the remote changes HEAD to something else.");
 
 	advise_if_enabled(ADVICE_FETCH_SET_HEAD_WARN, _(message_advice_set_head),
-			remote, head_name, remote);
+			remote, head_name, remote, remote, head_name);
 }
 
 static void report_set_head(const char *remote, const char *head_name,
@@ -1612,7 +1614,8 @@ static void report_set_head(const char *remote, const char *head_name,
 	strbuf_release(&buf_prefix);
 }
 
-static int set_head(const struct ref *remote_refs, int follow_remote_head)
+static int set_head(const struct ref *remote_refs, int follow_remote_head,
+		const char *no_warn_branch)
 {
 	int result = 0, create_only, is_bare, was_detached;
 	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
@@ -1669,7 +1672,9 @@ static int set_head(const struct ref *remote_refs, int follow_remote_head)
 		result = 1;
 		goto cleanup;
 	}
-	if (follow_remote_head == FOLLOW_REMOTE_WARN && verbosity >= 0)
+	if (verbosity >= 0 &&
+		follow_remote_head == FOLLOW_REMOTE_WARN &&
+		(!no_warn_branch || strcmp(no_warn_branch, head_name)))
 		report_set_head(remote, head_name, &b_local_head, was_detached);
 
 cleanup:
@@ -1898,7 +1903,8 @@ static int do_fetch(struct transport *transport,
 				  "you need to specify exactly one branch with the --set-upstream option"));
 		}
 	}
-	if (set_head(remote_refs, transport->remote->follow_remote_head))
+	if (set_head(remote_refs, transport->remote->follow_remote_head,
+		transport->remote->no_warn_branch))
 		;
 		/*
 		 * Way too many cases where this can go wrong
diff --git a/remote.c b/remote.c
index 0b18840d43..4ec5d3f47b 100644
--- a/remote.c
+++ b/remote.c
@@ -515,14 +515,23 @@ static int handle_config(const char *key, const char *value,
 		return parse_transport_option(key, value,
 					      &remote->server_options);
 	} else if (!strcmp(subkey, "followremotehead")) {
+		const char *no_warn_branch;
 		if (!strcmp(value, "never"))
 			remote->follow_remote_head = FOLLOW_REMOTE_NEVER;
 		else if (!strcmp(value, "create"))
 			remote->follow_remote_head = FOLLOW_REMOTE_CREATE;
-		else if (!strcmp(value, "warn"))
+		else if (!strcmp(value, "warn")) {
 			remote->follow_remote_head = FOLLOW_REMOTE_WARN;
-		else if (!strcmp(value, "always"))
+			remote->no_warn_branch = NULL;
+		} else if (skip_prefix(value, "warn-if-not-", &no_warn_branch)) {
+			remote->follow_remote_head = FOLLOW_REMOTE_WARN;
+			remote->no_warn_branch = no_warn_branch;
+		} else if (!strcmp(value, "always")) {
 			remote->follow_remote_head = FOLLOW_REMOTE_ALWAYS;
+		} else {
+			warning(_("unrecognized followRemoteHEAD value '%s' ignored"),
+				value);
+		}
 	}
 	return 0;
 }
diff --git a/remote.h b/remote.h
index 184b35653d..bda10dd5c8 100644
--- a/remote.h
+++ b/remote.h
@@ -116,6 +116,7 @@ struct remote {
 	struct string_list server_options;
 
 	enum follow_remote_head_settings follow_remote_head;
+	const char *no_warn_branch;
 };
 
 /**
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index 5d7ee1b550..5c9915b836 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -181,6 +181,44 @@ test_expect_success "fetch test followRemoteHEAD warn quiet" '
 	)
 '
 
+test_expect_success "fetch test followRemoteHEAD warn-if-not-branch branch is same" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git rev-parse --verify refs/remotes/origin/other &&
+		git remote set-head origin other &&
+		git rev-parse --verify refs/remotes/origin/HEAD &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		git config set remote.origin.followRemoteHEAD "warn-if-not-main" &&
+		output=$(git fetch) &&
+		test "z" = "z$output" &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/other) &&
+		test "z$head" = "z$branch"
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD warn-if-not-branch branch is different" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git rev-parse --verify refs/remotes/origin/other &&
+		git remote set-head origin other &&
+		git rev-parse --verify refs/remotes/origin/HEAD &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		git config set remote.origin.followRemoteHEAD "warn-if-not-some/different-branch" &&
+		git fetch >output &&
+		echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
+			"but we have ${SQ}other${SQ} locally." >expect &&
+		test_cmp expect output &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/other) &&
+		test "z$head" = "z$branch"
+	)
+'
+
 test_expect_success "fetch test followRemoteHEAD always" '
 	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
 	(
-- 
2.47.1.398.g18e7475ebe
Bence Ferdinandy· Dec 4, 2024, 10:39 UTC · re: Bence Ferdinandy · lore

[PATCH v2 3/3] remote set-head: set followRemoteHEAD to "warn" if "always"

When running "remote set-head" manually it is unlikely, that the user would actually like to have "fetch" always update the remote/HEAD. On the contrary, it is more likely, that the user would expect remote/HEAD to stay the way they manually set it, and just forgot about having "followRemoteHEAD" set to "always".

When "followRemoteHEAD" is set to "always" make running "remote set-head" change the config to "warn".

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
 builtin/remote.c  | 12 +++++++++++-
 t/t5505-remote.sh | 11 +++++++++++
 2 files changed, 22 insertions(+), 1 deletion(-)
Show changes to 2 files +22 −1

builtin/remote.c, t/t5505-remote.sh

diff --git a/builtin/remote.c b/builtin/remote.c
index d5b81445f2..32d02ca4a0 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1438,6 +1438,7 @@ static int set_head(int argc, const char **argv, const char *prefix,
 		b_local_head = STRBUF_INIT;
 	char *head_name = NULL;
 	struct ref_store *refs = get_main_ref_store(the_repository);
+	struct remote *remote;
 
 	struct option options[] = {
 		OPT_BOOL('a', "auto", &opt_a,
@@ -1448,8 +1449,10 @@ static int set_head(int argc, const char **argv, const char *prefix,
 	};
 	argc = parse_options(argc, argv, prefix, options,
 			     builtin_remote_sethead_usage, 0);
-	if (argc)
+	if (argc) {
 		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", argv[0]);
+		remote = remote_get(argv[0]);
+	}
 
 	if (!opt_a && !opt_d && argc == 2) {
 		head_name = xstrdup(argv[1]);
@@ -1488,6 +1491,13 @@ static int set_head(int argc, const char **argv, const char *prefix,
 	}
 	if (opt_a)
 		report_set_head_auto(argv[0], head_name, &b_local_head, was_detached);
+	if (remote->follow_remote_head == FOLLOW_REMOTE_ALWAYS) {
+		struct strbuf config_name = STRBUF_INIT;
+		strbuf_addf(&config_name,
+			"remote.%s.followremotehead", remote->name);
+		git_config_set(config_name.buf, "warn");
+		strbuf_release(&config_name);
+	}
 
 cleanup:
 	free(head_name);
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 7411aa770d..daf70406be 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -504,6 +504,17 @@ test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
 	)
 '
 
+test_expect_success 'set-head changes followRemoteHEAD always to warn' '
+	(
+		cd test &&
+		git config set remote.origin.followRemoteHEAD "always" &&
+		git remote set-head --auto origin &&
+		git config get remote.origin.followRemoteHEAD >output &&
+		echo "warn" >expect &&
+		test_cmp expect output
+	)
+'
+
 cat >test/expect <<\EOF
 refs/remotes/origin/side2
 EOF
-- 
2.47.1.398.g18e7475ebe
Kristoffer Haugsbakk· Dec 4, 2024, 11:43 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v2 3/3] remote set-head: set followRemoteHEAD to "warn" if "always"

Hi
On Wed, Dec 4, 2024, at 11:39, Bence Ferdinandy wrote:
Show 12 quoted lines
> diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
> index 7411aa770d..daf70406be 100755
> --- a/t/t5505-remote.sh
> +++ b/t/t5505-remote.sh
> @@ -504,6 +504,17 @@ test_expect_success 'set-head --auto has no
> problem w/multiple HEADs' '
>  	)
>  '
>
> +test_expect_success 'set-head changes followRemoteHEAD always to warn' '
> +	(
> +		cd test &&

I think you need to `cd` in a subshell here. See `t/README`, “Don't chdir around in tests.”.

> +		git config set remote.origin.followRemoteHEAD "always" &&
> +		git remote set-head --auto origin &&
> +		git config get remote.origin.followRemoteHEAD >output &&
Nit: maybe `actual` instead of `output`?  Just for uniformity.
Junio C Hamano· Dec 4, 2024, 20:40 UTC · re: Kristoffer Haugsbakk · lore

Re: [PATCH v2 3/3] remote set-head: set followRemoteHEAD to "warn" if "always"

"Kristoffer Haugsbakk" <kristofferhaugsbakk@fastmail.com> writes:
Show 16 quoted lines
> On Wed, Dec 4, 2024, at 11:39, Bence Ferdinandy wrote:
>> diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
>> index 7411aa770d..daf70406be 100755
>> --- a/t/t5505-remote.sh
>> +++ b/t/t5505-remote.sh
>> @@ -504,6 +504,17 @@ test_expect_success 'set-head --auto has no
>> problem w/multiple HEADs' '
>>  	)
>>  '
>>
>> +test_expect_success 'set-head changes followRemoteHEAD always to warn' '
>> +	(
>> +		cd test &&
>
> I think you need to `cd` in a subshell here.  See `t/README`, “Don't
> chdir around in tests.”.
Puzzled.  Isn't this inside a (subshell) already?
Show 5 quoted lines
>> +		git config set remote.origin.followRemoteHEAD "always" &&
>> +		git remote set-head --auto origin &&
>> +		git config get remote.origin.followRemoteHEAD >output &&
>
> Nit: maybe `actual` instead of `output`?  Just for uniformity.

t5505 is already heavily contaminated with the pattern to compare "expect" with "output", not with "actual", but that does not make it a good idea to make it even worse by adding more instances ;-)

Kristoffer Haugsbakk· Dec 4, 2024, 20:44 UTC · re: Junio C Hamano · lore

Re: [PATCH v2 3/3] remote set-head: set followRemoteHEAD to "warn" if "always"

On Wed, Dec 4, 2024, at 21:40, Junio C Hamano wrote:
Show 20 quoted lines
> "Kristoffer Haugsbakk" <kristofferhaugsbakk@fastmail.com> writes:
>
>> On Wed, Dec 4, 2024, at 11:39, Bence Ferdinandy wrote:
>>> diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
>>> index 7411aa770d..daf70406be 100755
>>> --- a/t/t5505-remote.sh
>>> +++ b/t/t5505-remote.sh
>>> @@ -504,6 +504,17 @@ test_expect_success 'set-head --auto has no
>>> problem w/multiple HEADs' '
>>>  	)
>>>  '
>>>
>>> +test_expect_success 'set-head changes followRemoteHEAD always to warn' '
>>> +	(
>>> +		cd test &&
>>
>> I think you need to `cd` in a subshell here.  See `t/README`, “Don't
>> chdir around in tests.”.
>
> Puzzled.  Isn't this inside a (subshell) already?
Aha, then I didn’t read the context properly.
Bence Ferdinandy· Dec 5, 2024, 08:14 UTC · re: Kristoffer Haugsbakk · lore

Re: [PATCH v2 3/3] remote set-head: set followRemoteHEAD to "warn" if "always"

On Wed Dec 04, 2024 at 21:44, Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com> wrote:
Show 23 quoted lines
> On Wed, Dec 4, 2024, at 21:40, Junio C Hamano wrote:
>> "Kristoffer Haugsbakk" <kristofferhaugsbakk@fastmail.com> writes:
>>
>>> On Wed, Dec 4, 2024, at 11:39, Bence Ferdinandy wrote:
>>>> diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
>>>> index 7411aa770d..daf70406be 100755
>>>> --- a/t/t5505-remote.sh
>>>> +++ b/t/t5505-remote.sh
>>>> @@ -504,6 +504,17 @@ test_expect_success 'set-head --auto has no
>>>> problem w/multiple HEADs' '
>>>>  	)
>>>>  '
>>>>
>>>> +test_expect_success 'set-head changes followRemoteHEAD always to warn' '
>>>> +	(
>>>> +		cd test &&
>>>
>>> I think you need to `cd` in a subshell here.  See `t/README`, “Don't
>>> chdir around in tests.”.
>>
>> Puzzled.  Isn't this inside a (subshell) already?
>
> Aha, then I didn’t read the context properly.

Thanks for the review! What I gathered is that v3 needs an s/output/actual and we're good. I'll send that soonish.

Thanks, Bence

Bence Ferdinandy· Dec 5, 2024, 12:16 UTC · re: Kristoffer Haugsbakk · lore

[PATCH v3 1/3] fetch set_head: move warn advice into advise_if_enabled

Advice about what to do when getting a warning is typed out explicitly twice and is printed as regular output. The output is also tested for. Extract the advice message into a single place and use a wrapper function, so if later the advice is made more chatty the signature only needs to be changed in once place. Remove the testing for the advice output in the tests.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v2: new patch, with the refactor moved out from the following one
 advice.c         |  1 +
 advice.h         |  1 +
 builtin/fetch.c  | 17 +++++++++++++----
 t/t5510-fetch.sh |  2 --
 4 files changed, 15 insertions(+), 6 deletions(-)
Show changes to 4 files +15 −6

advice.c, advice.h, builtin/fetch.c, t/t5510-fetch.sh

diff --git a/advice.c b/advice.c
index 6b879d805c..66461fdce9 100644
--- a/advice.c
+++ b/advice.c
@@ -53,6 +53,7 @@ static struct {
 	[ADVICE_COMMIT_BEFORE_MERGE]			= { "commitBeforeMerge" },
 	[ADVICE_DETACHED_HEAD]				= { "detachedHead" },
 	[ADVICE_DIVERGING]				= { "diverging" },
+	[ADVICE_FETCH_SET_HEAD_WARN]			= { "fetchRemoteHEADWarn" },
 	[ADVICE_FETCH_SHOW_FORCED_UPDATES]		= { "fetchShowForcedUpdates" },
 	[ADVICE_FORCE_DELETE_BRANCH]			= { "forceDeleteBranch" },
 	[ADVICE_GRAFT_FILE_DEPRECATED]			= { "graftFileDeprecated" },
diff --git a/advice.h b/advice.h
index d7466bc0ef..cf2284ec43 100644
--- a/advice.h
+++ b/advice.h
@@ -20,6 +20,7 @@ enum advice_type {
 	ADVICE_COMMIT_BEFORE_MERGE,
 	ADVICE_DETACHED_HEAD,
 	ADVICE_DIVERGING,
+	ADVICE_FETCH_SET_HEAD_WARN,
 	ADVICE_FETCH_SHOW_FORCED_UPDATES,
 	ADVICE_FORCE_DELETE_BRANCH,
 	ADVICE_GRAFT_FILE_DEPRECATED,
diff --git a/builtin/fetch.c b/builtin/fetch.c
index 62769d1686..087beb4c92 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1579,6 +1579,17 @@ static const char *strip_refshead(const char *name){
 	return name;
 }
 
+static void set_head_advice_msg(const char *remote, const char *head_name)
+{
+	const char message_advice_set_head[] =
+	N_("Run 'git remote set-head %s %s' to follow the change, or set\n"
+	   "'remote.%s.followRemoteHEAD' configuration option to a different value\n"
+	   "if you do not want to see this message.");
+
+	advise_if_enabled(ADVICE_FETCH_SET_HEAD_WARN, _(message_advice_set_head),
+			remote, head_name, remote);
+}
+
 static void report_set_head(const char *remote, const char *head_name,
 			struct strbuf *buf_prev, int updateres) {
 	struct strbuf buf_prefix = STRBUF_INIT;
@@ -1590,15 +1601,13 @@ static void report_set_head(const char *remote, const char *head_name,
 	if (prev_head && strcmp(prev_head, head_name)) {
 		printf("'HEAD' at '%s' is '%s', but we have '%s' locally.\n",
 			remote, head_name, prev_head);
-		printf("Run 'git remote set-head %s %s' to follow the change.\n",
-			remote, head_name);
+		set_head_advice_msg(remote, head_name);
 	}
 	else if (updateres && buf_prev->len) {
 		printf("'HEAD' at '%s' is '%s', "
 			"but we have a detached HEAD pointing to '%s' locally.\n",
 			remote, head_name, buf_prev->buf);
-		printf("Run 'git remote set-head %s %s' to follow the change.\n",
-			remote, head_name);
+		set_head_advice_msg(remote, head_name);
 	}
 	strbuf_release(&buf_prefix);
 }
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index ccb74428bc..5d7ee1b550 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -123,7 +123,6 @@ test_expect_success "fetch test followRemoteHEAD warn no change" '
 		git fetch >output &&
 		echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
 			"but we have ${SQ}other${SQ} locally." >expect &&
-		echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change." >>expect &&
 		test_cmp expect output &&
 		head=$(git rev-parse refs/remotes/origin/HEAD) &&
 		branch=$(git rev-parse refs/remotes/origin/other) &&
@@ -160,7 +159,6 @@ test_expect_success "fetch test followRemoteHEAD warn detached" '
 		echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
 			"but we have a detached HEAD pointing to" \
 			"${SQ}${HEAD}${SQ} locally." >expect &&
-		echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change." >>expect &&
 		test_cmp expect output
 	)
 '
-- 
2.47.1.398.g18e7475ebe
Bence Ferdinandy· Dec 5, 2024, 12:16 UTC · re: Bence Ferdinandy · lore

[PATCH v3 2/3] fetch set_head: add warn-if-not-$branch option

Currently if we want to have a remote/HEAD locally that is different from the one on the remote, but we still want to get a warning if remote changes HEAD, our only option is to have an indiscriminate warning with "follow_remote_head" set to "warn". Add a new option "warn-if-not-$branch", where $branch is a branch name we do not wish to get a warning about. If the remote HEAD is $branch do not warn, otherwise, behave as "warn".

E.g. let's assume, that our remote origin has HEAD set to "master", but locally we have "git remote set-head origin seen". Setting 'remote.origin.followRemoteHEAD = "warn"' will always print a warning, even though the remote has not changed HEAD from "master". Setting 'remote.origin.followRemoteHEAD = "warn-if-not-master" will squelch the warning message, unless the remote changes HEAD from "master". Note, that should the remote change HEAD to "seen" (which we have locally), there will still be no warning.

Improve the advice message in report_set_head to also include silencing the warning message with "warn-if-not-$branch".

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v2: reuse FOLLOW_REMOTE_WARN
        add documentation
    
    v3: s/output/actual in tests
 Documentation/config/remote.txt |  8 ++++---
 builtin/fetch.c                 | 16 +++++++++-----
 remote.c                        | 13 +++++++++--
 remote.h                        |  1 +
 t/t5510-fetch.sh                | 38 +++++++++++++++++++++++++++++++++
 5 files changed, 66 insertions(+), 10 deletions(-)
Show changes to 5 files +66 −10

Documentation/config/remote.txt, builtin/fetch.c, remote.c, remote.h, t/t5510-fetch.sh

diff --git a/Documentation/config/remote.txt b/Documentation/config/remote.txt
index 024f92befc..4118c219c1 100644
--- a/Documentation/config/remote.txt
+++ b/Documentation/config/remote.txt
@@ -106,10 +106,12 @@ remote.<name>.followRemoteHEAD::
 	How linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`.
 	The default value is "create", which will create `remotes/<name>/HEAD`
 	if it exists on the remote, but not locally, but will not touch an
-	already existing local reference.  Setting to "warn" will print
+	already existing local reference. Setting to "warn" will print
 	a message if the remote has a different value, than the local one and
-	in case there is no local reference, it behaves like "create". Setting
-	to "always" will silently update it to the value on the remote.
+	in case there is no local reference, it behaves like "create".
+	A variant on "warn" is "warn-if-not-$branch", which behaves like
+	"warn", but if `HEAD` on the remote is `$branch` it will be silent.
+	Setting to "always" will silently update it to the value on the remote.
 	Finally, setting it to "never" will never change or create the local
 	reference.
 +
diff --git a/builtin/fetch.c b/builtin/fetch.c
index 087beb4c92..b3f6793026 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1584,10 +1584,12 @@ static void set_head_advice_msg(const char *remote, const char *head_name)
 	const char message_advice_set_head[] =
 	N_("Run 'git remote set-head %s %s' to follow the change, or set\n"
 	   "'remote.%s.followRemoteHEAD' configuration option to a different value\n"
-	   "if you do not want to see this message.");
+	   "if you do not want to see this message. Specifically running\n"
+	   "'git config set remote.%s.followRemoteHEAD %s' will disable the warning\n"
+	   "until the remote changes HEAD to something else.");
 
 	advise_if_enabled(ADVICE_FETCH_SET_HEAD_WARN, _(message_advice_set_head),
-			remote, head_name, remote);
+			remote, head_name, remote, remote, head_name);
 }
 
 static void report_set_head(const char *remote, const char *head_name,
@@ -1612,7 +1614,8 @@ static void report_set_head(const char *remote, const char *head_name,
 	strbuf_release(&buf_prefix);
 }
 
-static int set_head(const struct ref *remote_refs, int follow_remote_head)
+static int set_head(const struct ref *remote_refs, int follow_remote_head,
+		const char *no_warn_branch)
 {
 	int result = 0, create_only, is_bare, was_detached;
 	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
@@ -1669,7 +1672,9 @@ static int set_head(const struct ref *remote_refs, int follow_remote_head)
 		result = 1;
 		goto cleanup;
 	}
-	if (follow_remote_head == FOLLOW_REMOTE_WARN && verbosity >= 0)
+	if (verbosity >= 0 &&
+		follow_remote_head == FOLLOW_REMOTE_WARN &&
+		(!no_warn_branch || strcmp(no_warn_branch, head_name)))
 		report_set_head(remote, head_name, &b_local_head, was_detached);
 
 cleanup:
@@ -1898,7 +1903,8 @@ static int do_fetch(struct transport *transport,
 				  "you need to specify exactly one branch with the --set-upstream option"));
 		}
 	}
-	if (set_head(remote_refs, transport->remote->follow_remote_head))
+	if (set_head(remote_refs, transport->remote->follow_remote_head,
+		transport->remote->no_warn_branch))
 		;
 		/*
 		 * Way too many cases where this can go wrong
diff --git a/remote.c b/remote.c
index 0b18840d43..4ec5d3f47b 100644
--- a/remote.c
+++ b/remote.c
@@ -515,14 +515,23 @@ static int handle_config(const char *key, const char *value,
 		return parse_transport_option(key, value,
 					      &remote->server_options);
 	} else if (!strcmp(subkey, "followremotehead")) {
+		const char *no_warn_branch;
 		if (!strcmp(value, "never"))
 			remote->follow_remote_head = FOLLOW_REMOTE_NEVER;
 		else if (!strcmp(value, "create"))
 			remote->follow_remote_head = FOLLOW_REMOTE_CREATE;
-		else if (!strcmp(value, "warn"))
+		else if (!strcmp(value, "warn")) {
 			remote->follow_remote_head = FOLLOW_REMOTE_WARN;
-		else if (!strcmp(value, "always"))
+			remote->no_warn_branch = NULL;
+		} else if (skip_prefix(value, "warn-if-not-", &no_warn_branch)) {
+			remote->follow_remote_head = FOLLOW_REMOTE_WARN;
+			remote->no_warn_branch = no_warn_branch;
+		} else if (!strcmp(value, "always")) {
 			remote->follow_remote_head = FOLLOW_REMOTE_ALWAYS;
+		} else {
+			warning(_("unrecognized followRemoteHEAD value '%s' ignored"),
+				value);
+		}
 	}
 	return 0;
 }
diff --git a/remote.h b/remote.h
index 184b35653d..bda10dd5c8 100644
--- a/remote.h
+++ b/remote.h
@@ -116,6 +116,7 @@ struct remote {
 	struct string_list server_options;
 
 	enum follow_remote_head_settings follow_remote_head;
+	const char *no_warn_branch;
 };
 
 /**
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index 5d7ee1b550..3885461ac1 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -181,6 +181,44 @@ test_expect_success "fetch test followRemoteHEAD warn quiet" '
 	)
 '
 
+test_expect_success "fetch test followRemoteHEAD warn-if-not-branch branch is same" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git rev-parse --verify refs/remotes/origin/other &&
+		git remote set-head origin other &&
+		git rev-parse --verify refs/remotes/origin/HEAD &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		git config set remote.origin.followRemoteHEAD "warn-if-not-main" &&
+		actual=$(git fetch) &&
+		test "z" = "z$actual" &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/other) &&
+		test "z$head" = "z$branch"
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD warn-if-not-branch branch is different" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git rev-parse --verify refs/remotes/origin/other &&
+		git remote set-head origin other &&
+		git rev-parse --verify refs/remotes/origin/HEAD &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		git config set remote.origin.followRemoteHEAD "warn-if-not-some/different-branch" &&
+		git fetch >actual &&
+		echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
+			"but we have ${SQ}other${SQ} locally." >expect &&
+		test_cmp expect actual &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/other) &&
+		test "z$head" = "z$branch"
+	)
+'
+
 test_expect_success "fetch test followRemoteHEAD always" '
 	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
 	(
-- 
2.47.1.398.g18e7475ebe
Teng Long· Jan 5, 2025, 11:42 UTC · re: Bence Ferdinandy · lore

[PATCH v3 2/3] fetch set_head: add warn-if-not-$branch option

Bence Ferdinandy <bence@ferdinandy.com> wrote:
Show 13 quoted lines
> @@ -1584,10 +1584,12 @@ static void set_head_advice_msg(const char *remote, const char *head_name)
>  	const char message_advice_set_head[] =
>  	N_("Run 'git remote set-head %s %s' to follow the change, or set\n"
>  	   "'remote.%s.followRemoteHEAD' configuration option to a different value\n"
> -	   "if you do not want to see this message.");
> +	   "if you do not want to see this message. Specifically running\n"
> +	   "'git config set remote.%s.followRemoteHEAD %s' will disable the warning\n"
> +	   "until the remote changes HEAD to something else.");
> 
>  	advise_if_enabled(ADVICE_FETCH_SET_HEAD_WARN, _(message_advice_set_head),
> -			remote, head_name, remote);
> +			remote, head_name, remote, remote, head_name);
> 
Seems like the config is unsupported to be feed a variable named "head_name"?
Thanks.
Bence Ferdinandy· Jan 5, 2025, 15:13 UTC · re: Teng Long · lore

Re: [PATCH v3 2/3] fetch set_head: add warn-if-not-$branch option

On Sun Jan 05, 2025 at 12:42, Teng Long <dyroneteng@gmail.com> wrote:
Show 20 quoted lines
>
> Bence Ferdinandy <bence@ferdinandy.com> wrote:
>
>> @@ -1584,10 +1584,12 @@ static void set_head_advice_msg(const char *remote, const char *head_name)
>>  	const char message_advice_set_head[] =
>>  	N_("Run 'git remote set-head %s %s' to follow the change, or set\n"
>>  	   "'remote.%s.followRemoteHEAD' configuration option to a different value\n"
>> -	   "if you do not want to see this message.");
>> +	   "if you do not want to see this message. Specifically running\n"
>> +	   "'git config set remote.%s.followRemoteHEAD %s' will disable the warning\n"
>> +	   "until the remote changes HEAD to something else.");
>> 
>>  	advise_if_enabled(ADVICE_FETCH_SET_HEAD_WARN, _(message_advice_set_head),
>> -			remote, head_name, remote);
>> +			remote, head_name, remote, remote, head_name);
>> 
>
> Seems like the config is unsupported to be feed a variable named "head_name"?
>
> Thanks.
Ah, yes the warning seems to be off, it should read
git config set remote.%s.followRemoteHEAD warn-if-not-%s
thanks for catching this mistake!

Best, Bence

Bence Ferdinandy· Jan 5, 2025, 16:09 UTC · re: Teng Long · lore

[PATCH] fetch: fix erroneous set_head advice message

9e2b7005be (fetch set_head: add warn-if-not-$branch option, 2024-12-05) tried to expand the advice message for set_head with the new option, but unfortunately did not manage to add the right incantation. Fix the advice message with the correct usage of warn-if-not-$branch.

Reported-by: Teng Long <dyroneteng@gmail.com>
Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
 builtin/fetch.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
Show changes to builtin/fetch.c +2 −2
diff --git a/builtin/fetch.c b/builtin/fetch.c
index 2d37a378ba..fe2b26c74a 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1588,8 +1588,8 @@ static void set_head_advice_msg(const char *remote, const char *head_name)
 	N_("Run 'git remote set-head %s %s' to follow the change, or set\n"
 	   "'remote.%s.followRemoteHEAD' configuration option to a different value\n"
 	   "if you do not want to see this message. Specifically running\n"
-	   "'git config set remote.%s.followRemoteHEAD %s' will disable the warning\n"
-	   "until the remote changes HEAD to something else.");
+	   "'git config set remote.%s.followRemoteHEAD warn-if-not-branch-%s'\n"
+	   "will disable the warning until the remote changes HEAD to something else.");
 
 	advise_if_enabled(ADVICE_FETCH_SET_HEAD_WARN, _(message_advice_set_head),
 			remote, head_name, remote, remote, head_name);

base-commit: 1b4e9a5f8b5f048972c21fe8acafe0404096f694
-- 
2.48.0.rc0.146.g306ab352f4
Junio C Hamano· Jan 6, 2025, 14:50 UTC · re: Bence Ferdinandy · lore

Re: [PATCH] fetch: fix erroneous set_head advice message

Bence Ferdinandy <bence@ferdinandy.com> writes:
Show 26 quoted lines
> 9e2b7005be (fetch set_head: add warn-if-not-$branch option, 2024-12-05)
> tried to expand the advice message for set_head with the new option, but
> unfortunately did not manage to add the right incantation. Fix the
> advice message with the correct usage of warn-if-not-$branch.
>
> Reported-by: Teng Long <dyroneteng@gmail.com>
> Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
> ---
>  builtin/fetch.c | 4 ++--
>  1 file changed, 2 insertions(+), 2 deletions(-)
>
> diff --git a/builtin/fetch.c b/builtin/fetch.c
> index 2d37a378ba..fe2b26c74a 100644
> --- a/builtin/fetch.c
> +++ b/builtin/fetch.c
> @@ -1588,8 +1588,8 @@ static void set_head_advice_msg(const char *remote, const char *head_name)
>  	N_("Run 'git remote set-head %s %s' to follow the change, or set\n"
>  	   "'remote.%s.followRemoteHEAD' configuration option to a different value\n"
>  	   "if you do not want to see this message. Specifically running\n"
> -	   "'git config set remote.%s.followRemoteHEAD %s' will disable the warning\n"
> -	   "until the remote changes HEAD to something else.");
> +	   "'git config set remote.%s.followRemoteHEAD warn-if-not-branch-%s'\n"
> +	   "will disable the warning until the remote changes HEAD to something else.");
>  
>  	advise_if_enabled(ADVICE_FETCH_SET_HEAD_WARN, _(message_advice_set_head),
>  			remote, head_name, remote, remote, head_name);
Thanks, both.  Will queue.
Bence Ferdinandy· Dec 5, 2024, 12:16 UTC · re: Bence Ferdinandy · lore

[PATCH v3 3/3] remote set-head: set followRemoteHEAD to "warn" if "always"

When running "remote set-head" manually it is unlikely, that the user would actually like to have "fetch" always update the remote/HEAD. On the contrary, it is more likely, that the user would expect remote/HEAD to stay the way they manually set it, and just forgot about having "followRemoteHEAD" set to "always".

When "followRemoteHEAD" is set to "always" make running "remote set-head" change the config to "warn".

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v3: s/output/actual
 builtin/remote.c  | 12 +++++++++++-
 t/t5505-remote.sh | 11 +++++++++++
 2 files changed, 22 insertions(+), 1 deletion(-)
Show changes to 2 files +22 −1

builtin/remote.c, t/t5505-remote.sh

diff --git a/builtin/remote.c b/builtin/remote.c
index d5b81445f2..32d02ca4a0 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1438,6 +1438,7 @@ static int set_head(int argc, const char **argv, const char *prefix,
 		b_local_head = STRBUF_INIT;
 	char *head_name = NULL;
 	struct ref_store *refs = get_main_ref_store(the_repository);
+	struct remote *remote;
 
 	struct option options[] = {
 		OPT_BOOL('a', "auto", &opt_a,
@@ -1448,8 +1449,10 @@ static int set_head(int argc, const char **argv, const char *prefix,
 	};
 	argc = parse_options(argc, argv, prefix, options,
 			     builtin_remote_sethead_usage, 0);
-	if (argc)
+	if (argc) {
 		strbuf_addf(&b_head, "refs/remotes/%s/HEAD", argv[0]);
+		remote = remote_get(argv[0]);
+	}
 
 	if (!opt_a && !opt_d && argc == 2) {
 		head_name = xstrdup(argv[1]);
@@ -1488,6 +1491,13 @@ static int set_head(int argc, const char **argv, const char *prefix,
 	}
 	if (opt_a)
 		report_set_head_auto(argv[0], head_name, &b_local_head, was_detached);
+	if (remote->follow_remote_head == FOLLOW_REMOTE_ALWAYS) {
+		struct strbuf config_name = STRBUF_INIT;
+		strbuf_addf(&config_name,
+			"remote.%s.followremotehead", remote->name);
+		git_config_set(config_name.buf, "warn");
+		strbuf_release(&config_name);
+	}
 
 cleanup:
 	free(head_name);
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 7411aa770d..519f7973e3 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -504,6 +504,17 @@ test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
 	)
 '
 
+test_expect_success 'set-head changes followRemoteHEAD always to warn' '
+	(
+		cd test &&
+		git config set remote.origin.followRemoteHEAD "always" &&
+		git remote set-head --auto origin &&
+		git config get remote.origin.followRemoteHEAD >actual &&
+		echo "warn" >expect &&
+		test_cmp expect actual
+	)
+'
+
 cat >test/expect <<\EOF
 refs/remotes/origin/side2
 EOF
-- 
2.47.1.398.g18e7475ebe
Junio C Hamano· Dec 4, 2024, 19:28 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v2 1/3] fetch set_head: move warn advice into advise_if_enabled

Bence Ferdinandy <bence@ferdinandy.com> writes:
Show 21 quoted lines
> +static void set_head_advice_msg(const char *remote, const char *head_name)
> +{
> +	const char message_advice_set_head[] =
> +	N_("Run 'git remote set-head %s %s' to follow the change, or set\n"
> +	   "'remote.%s.followRemoteHEAD' configuration option to a different value\n"
> +	   "if you do not want to see this message.");
> +
> +	advise_if_enabled(ADVICE_FETCH_SET_HEAD_WARN, _(message_advice_set_head),
> +			remote, head_name, remote);
> +}
> ...
>  	else if (updateres && buf_prev->len) {
>  		printf("'HEAD' at '%s' is '%s', "
>  			"but we have a detached HEAD pointing to '%s' locally.\n",
>  			remote, head_name, buf_prev->buf);
> -		printf("Run 'git remote set-head %s %s' to follow the change.\n",
> -			remote, head_name);
> +		set_head_advice_msg(remote, head_name);
>  	}
>  	strbuf_release(&buf_prefix);
>  }
Looking good.
Show 10 quoted lines
> diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
> index ccb74428bc..5d7ee1b550 100755
> --- a/t/t5510-fetch.sh
> +++ b/t/t5510-fetch.sh
> @@ -123,7 +123,6 @@ test_expect_success "fetch test followRemoteHEAD warn no change" '
>  		git fetch >output &&
>  		echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
>  			"but we have ${SQ}other${SQ} locally." >expect &&
> -		echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change." >>expect &&
>  		test_cmp expect output &&

OK. We just lose this part of the test, not because the advice messages are squelched but because they go to the standard error stream. An obvious alternative is to capture it in a separate file and grep, i.e.

	git fetch >output 2>error &&
	echo ... >expect &&
	test_grep "Run ${SQ}git remote set-head" error &&
	test_cmp expect output

If we were testing that the advice mechanism is used, do that with and without the ADVICE_FETCH_SET_HEAD_WARN squelched, but probably it is a bit too much, in the sense that we are not in the business of testing that the advice mechanism works correctly in this series.

Bence Ferdinandy· Nov 28, 2024, 11:19 UTC · re: Junio C Hamano · lore

[PATCH v2] fetch: add configuration for set_head behaviour

In the current implementation, if refs/remotes/$remote/HEAD does not exist, running fetch will create it, but if it does exist it will not do anything, which is a somewhat safe and minimal approach. Unfortunately, for users who wish to NOT have refs/remotes/$remote/HEAD set for any reason (e.g. so that `git rev-parse origin` doesn't accidentally point them somewhere they do not want to), there is no way to remove this behaviour. On the other side of the spectrum, users may want fetch to automatically update HEAD or at least give them a warning if something changed on the remote.

Introduce a new setting, remote.$remote.followRemoteHEAD with four options:

    - "never": do not ever do anything, not even create
    - "create": the current behaviour, now the default behaviour
    - "warn": print a message if remote and local HEAD is different
    - "always": silently update HEAD on every change
Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    This patch is built off v15 of the set-head/fetch updates series:
    https://lore.kernel.org/git/D5WEJJBMSO1K.2TPXDI1K08SHT@ferdinandy.com/T/#m13a0c3e8919872188ef07fd9fd984c78e8aa35ba
    
    v2:
        - magic numbers converted to enum
        - since the enum is descriptive (well, along with the documentation)
          I felt there is no need for the comment explaining the values
 Documentation/config/remote.txt |  11 ++++
 builtin/fetch.c                 |  46 ++++++++++++--
 remote.c                        |   9 +++
 remote.h                        |   9 +++
 t/t5510-fetch.sh                | 102 ++++++++++++++++++++++++++++++++
 5 files changed, 171 insertions(+), 6 deletions(-)
Show changes to 5 files +171 −6

Documentation/config/remote.txt, builtin/fetch.c, remote.c, remote.h, t/t5510-fetch.sh

diff --git a/Documentation/config/remote.txt b/Documentation/config/remote.txt
index 6d8b7d6c63..024f92befc 100644
--- a/Documentation/config/remote.txt
+++ b/Documentation/config/remote.txt
@@ -101,6 +101,17 @@ remote.<name>.serverOption::
 	The default set of server options used when fetching from this remote.
 	These server options can be overridden by the `--server-option=` command
 	line arguments.
+
+remote.<name>.followRemoteHEAD::
+	How linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`.
+	The default value is "create", which will create `remotes/<name>/HEAD`
+	if it exists on the remote, but not locally, but will not touch an
+	already existing local reference.  Setting to "warn" will print
+	a message if the remote has a different value, than the local one and
+	in case there is no local reference, it behaves like "create". Setting
+	to "always" will silently update it to the value on the remote.
+	Finally, setting it to "never" will never change or create the local
+	reference.
 +
 This is a multi-valued variable, and an empty value can be used in a higher
 priority configuration file (e.g. `.git/config` in a repository) to clear
diff --git a/builtin/fetch.c b/builtin/fetch.c
index 2f416cf867..d74b20b49c 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1579,10 +1579,35 @@ static const char *strip_refshead(const char *name){
 	return name;
 }
 
-static int set_head(const struct ref *remote_refs)
+static void report_set_head(const char *remote, const char *head_name,
+			struct strbuf *buf_prev, int updateres) {
+	struct strbuf buf_prefix = STRBUF_INIT;
+	const char *prev_head = NULL;
+
+	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
+	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
+
+	if (prev_head && strcmp(prev_head, head_name)) {
+		printf("'HEAD' at '%s' is '%s', but we have '%s' locally.\n",
+			remote, head_name, prev_head);
+		printf("Run 'git remote set-head %s %s' to follow the change.\n",
+			remote, head_name);
+	}
+	else if (updateres && buf_prev->len) {
+		printf("'HEAD' at '%s' is '%s', "
+			"but we have a detached HEAD pointing to '%s' locally.\n",
+			remote, head_name, buf_prev->buf);
+		printf("Run 'git remote set-head %s %s' to follow the change.\n",
+			remote, head_name);
+	}
+	strbuf_release(&buf_prefix);
+}
+
+static int set_head(const struct ref *remote_refs, int follow_remote_head)
 {
-	int result = 0, is_bare;
-	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT;
+	int result = 0, create_only, is_bare, was_detached;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
+		      b_local_head = STRBUF_INIT;
 	const char *remote = gtransport->remote->name;
 	char *head_name = NULL;
 	struct ref *ref, *matches;
@@ -1603,6 +1628,8 @@ static int set_head(const struct ref *remote_refs)
 		string_list_append(&heads, strip_refshead(ref->name));
 	}
 
+	if (follow_remote_head == FOLLOW_REMOTE_NEVER)
+		goto cleanup;
 
 	if (!heads.nr)
 		result = 1;
@@ -1614,6 +1641,7 @@ static int set_head(const struct ref *remote_refs)
 	if (!head_name)
 		goto cleanup;
 	is_bare = is_bare_repository();
+	create_only = follow_remote_head == 2 ? 0 : !is_bare;
 	if (is_bare) {
 		strbuf_addstr(&b_head, "HEAD");
 		strbuf_addf(&b_remote_head, "refs/heads/%s", head_name);
@@ -1626,9 +1654,14 @@ static int set_head(const struct ref *remote_refs)
 		result = 1;
 		goto cleanup;
 	}
-	if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
-					"fetch", NULL, !is_bare))
+	was_detached = refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
+					"fetch", &b_local_head, create_only);
+	if (was_detached == -1) {
 		result = 1;
+		goto cleanup;
+	}
+	if (follow_remote_head == FOLLOW_REMOTE_WARN && verbosity >= 0)
+		report_set_head(remote, head_name, &b_local_head, was_detached);
 
 cleanup:
 	free(head_name);
@@ -1636,6 +1669,7 @@ static int set_head(const struct ref *remote_refs)
 	free_refs(matches);
 	string_list_clear(&heads, 0);
 	strbuf_release(&b_head);
+	strbuf_release(&b_local_head);
 	strbuf_release(&b_remote_head);
 	return result;
 }
@@ -1855,7 +1889,7 @@ static int do_fetch(struct transport *transport,
 				  "you need to specify exactly one branch with the --set-upstream option"));
 		}
 	}
-	if (set_head(remote_refs))
+	if (set_head(remote_refs, transport->remote->follow_remote_head))
 		;
 		/*
 		 * Way too many cases where this can go wrong
diff --git a/remote.c b/remote.c
index 10104d11e3..0b18840d43 100644
--- a/remote.c
+++ b/remote.c
@@ -514,6 +514,15 @@ static int handle_config(const char *key, const char *value,
 	} else if (!strcmp(subkey, "serveroption")) {
 		return parse_transport_option(key, value,
 					      &remote->server_options);
+	} else if (!strcmp(subkey, "followremotehead")) {
+		if (!strcmp(value, "never"))
+			remote->follow_remote_head = FOLLOW_REMOTE_NEVER;
+		else if (!strcmp(value, "create"))
+			remote->follow_remote_head = FOLLOW_REMOTE_CREATE;
+		else if (!strcmp(value, "warn"))
+			remote->follow_remote_head = FOLLOW_REMOTE_WARN;
+		else if (!strcmp(value, "always"))
+			remote->follow_remote_head = FOLLOW_REMOTE_ALWAYS;
 	}
 	return 0;
 }
diff --git a/remote.h b/remote.h
index a7e5c4e07c..184b35653d 100644
--- a/remote.h
+++ b/remote.h
@@ -59,6 +59,13 @@ struct remote_state {
 void remote_state_clear(struct remote_state *remote_state);
 struct remote_state *remote_state_new(void);
 
+	enum follow_remote_head_settings {
+		FOLLOW_REMOTE_NEVER = -1,
+		FOLLOW_REMOTE_CREATE = 0,
+		FOLLOW_REMOTE_WARN = 1,
+		FOLLOW_REMOTE_ALWAYS = 2,
+	};
+
 struct remote {
 	struct hashmap_entry ent;
 
@@ -107,6 +114,8 @@ struct remote {
 	char *http_proxy_authmethod;
 
 	struct string_list server_options;
+
+	enum follow_remote_head_settings follow_remote_head;
 };
 
 /**
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index 87698341f5..2467027d34 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -99,6 +99,108 @@ test_expect_success "fetch test remote HEAD change" '
 	branch=$(git rev-parse refs/remotes/origin/other) &&
 	test "z$head" = "z$branch"'
 
+test_expect_success "fetch test followRemoteHEAD never" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
+		git config set remote.origin.followRemoteHEAD "never" &&
+		git fetch &&
+		test_must_fail git rev-parse --verify refs/remotes/origin/HEAD
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD warn no change" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git rev-parse --verify refs/remotes/origin/other &&
+		git remote set-head origin other &&
+		git rev-parse --verify refs/remotes/origin/HEAD &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		git config set remote.origin.followRemoteHEAD "warn" &&
+		git fetch >output &&
+		echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
+			"but we have ${SQ}other${SQ} locally." >expect &&
+		echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change." >>expect &&
+		test_cmp expect output &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/other) &&
+		test "z$head" = "z$branch"
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD warn create" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
+		git config set remote.origin.followRemoteHEAD "warn" &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		output=$(git fetch) &&
+		test "z" = "z$output" &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/main) &&
+		test "z$head" = "z$branch"
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD warn detached" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
+		git update-ref refs/remotes/origin/HEAD HEAD &&
+		HEAD=$(git log --pretty="%H") &&
+		git config set remote.origin.followRemoteHEAD "warn" &&
+		git fetch >output &&
+		echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
+			"but we have a detached HEAD pointing to" \
+			"${SQ}${HEAD}${SQ} locally." >expect &&
+		echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change." >>expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD warn quiet" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git rev-parse --verify refs/remotes/origin/other &&
+		git remote set-head origin other &&
+		git rev-parse --verify refs/remotes/origin/HEAD &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		git config set remote.origin.followRemoteHEAD "warn" &&
+		output=$(git fetch --quiet) &&
+		test "z" = "z$output" &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/other) &&
+		test "z$head" = "z$branch"
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD always" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git rev-parse --verify refs/remotes/origin/other &&
+		git remote set-head origin other &&
+		git rev-parse --verify refs/remotes/origin/HEAD &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		git config set remote.origin.followRemoteHEAD "always" &&
+		git fetch &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/main) &&
+		test "z$head" = "z$branch"
+	)
+'
+
 test_expect_success 'fetch --prune on its own works as expected' '
 	cd "$D" &&
 	git clone . prune &&
-- 
2.47.1.295.g12e80f91dc.dirty
Bence Ferdinandy· Nov 28, 2024, 23:06 UTC · re: Bence Ferdinandy · lore

[PATCH v3] fetch: add configuration for set_head behaviour

In the current implementation, if refs/remotes/$remote/HEAD does not exist, running fetch will create it, but if it does exist it will not do anything, which is a somewhat safe and minimal approach. Unfortunately, for users who wish to NOT have refs/remotes/$remote/HEAD set for any reason (e.g. so that `git rev-parse origin` doesn't accidentally point them somewhere they do not want to), there is no way to remove this behaviour. On the other side of the spectrum, users may want fetch to automatically update HEAD or at least give them a warning if something changed on the remote.

Introduce a new setting, remote.$remote.followRemoteHEAD with four options:

    - "never": do not ever do anything, not even create
    - "create": the current behaviour, now the default behaviour
    - "warn": print a message if remote and local HEAD is different
    - "always": silently update HEAD on every change
Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    This patch is built off v15 of the set-head/fetch updates series:
    https://lore.kernel.org/git/D5WEJJBMSO1K.2TPXDI1K08SHT@ferdinandy.com/T/#m13a0c3e8919872188ef07fd9fd984c78e8aa35ba
    
    v2:
        - magic numbers converted to enum
        - since the enum is descriptive (well, along with the documentation)
          I felt there is no need for the comment explaining the values
    
    v3: missed a magic number in v2
 Documentation/config/remote.txt |  11 ++++
 builtin/fetch.c                 |  46 ++++++++++++--
 remote.c                        |   9 +++
 remote.h                        |   9 +++
 t/t5510-fetch.sh                | 102 ++++++++++++++++++++++++++++++++
 5 files changed, 171 insertions(+), 6 deletions(-)
Show changes to 5 files +171 −6

Documentation/config/remote.txt, builtin/fetch.c, remote.c, remote.h, t/t5510-fetch.sh

diff --git a/Documentation/config/remote.txt b/Documentation/config/remote.txt
index 6d8b7d6c63..024f92befc 100644
--- a/Documentation/config/remote.txt
+++ b/Documentation/config/remote.txt
@@ -101,6 +101,17 @@ remote.<name>.serverOption::
 	The default set of server options used when fetching from this remote.
 	These server options can be overridden by the `--server-option=` command
 	line arguments.
+
+remote.<name>.followRemoteHEAD::
+	How linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`.
+	The default value is "create", which will create `remotes/<name>/HEAD`
+	if it exists on the remote, but not locally, but will not touch an
+	already existing local reference.  Setting to "warn" will print
+	a message if the remote has a different value, than the local one and
+	in case there is no local reference, it behaves like "create". Setting
+	to "always" will silently update it to the value on the remote.
+	Finally, setting it to "never" will never change or create the local
+	reference.
 +
 This is a multi-valued variable, and an empty value can be used in a higher
 priority configuration file (e.g. `.git/config` in a repository) to clear
diff --git a/builtin/fetch.c b/builtin/fetch.c
index 2f416cf867..88c5c5d781 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1579,10 +1579,35 @@ static const char *strip_refshead(const char *name){
 	return name;
 }
 
-static int set_head(const struct ref *remote_refs)
+static void report_set_head(const char *remote, const char *head_name,
+			struct strbuf *buf_prev, int updateres) {
+	struct strbuf buf_prefix = STRBUF_INIT;
+	const char *prev_head = NULL;
+
+	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
+	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
+
+	if (prev_head && strcmp(prev_head, head_name)) {
+		printf("'HEAD' at '%s' is '%s', but we have '%s' locally.\n",
+			remote, head_name, prev_head);
+		printf("Run 'git remote set-head %s %s' to follow the change.\n",
+			remote, head_name);
+	}
+	else if (updateres && buf_prev->len) {
+		printf("'HEAD' at '%s' is '%s', "
+			"but we have a detached HEAD pointing to '%s' locally.\n",
+			remote, head_name, buf_prev->buf);
+		printf("Run 'git remote set-head %s %s' to follow the change.\n",
+			remote, head_name);
+	}
+	strbuf_release(&buf_prefix);
+}
+
+static int set_head(const struct ref *remote_refs, int follow_remote_head)
 {
-	int result = 0, is_bare;
-	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT;
+	int result = 0, create_only, is_bare, was_detached;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
+		      b_local_head = STRBUF_INIT;
 	const char *remote = gtransport->remote->name;
 	char *head_name = NULL;
 	struct ref *ref, *matches;
@@ -1603,6 +1628,8 @@ static int set_head(const struct ref *remote_refs)
 		string_list_append(&heads, strip_refshead(ref->name));
 	}
 
+	if (follow_remote_head == FOLLOW_REMOTE_NEVER)
+		goto cleanup;
 
 	if (!heads.nr)
 		result = 1;
@@ -1614,6 +1641,7 @@ static int set_head(const struct ref *remote_refs)
 	if (!head_name)
 		goto cleanup;
 	is_bare = is_bare_repository();
+	create_only = follow_remote_head == FOLLOW_REMOTE_ALWAYS ? 0 : !is_bare;
 	if (is_bare) {
 		strbuf_addstr(&b_head, "HEAD");
 		strbuf_addf(&b_remote_head, "refs/heads/%s", head_name);
@@ -1626,9 +1654,14 @@ static int set_head(const struct ref *remote_refs)
 		result = 1;
 		goto cleanup;
 	}
-	if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
-					"fetch", NULL, !is_bare))
+	was_detached = refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
+					"fetch", &b_local_head, create_only);
+	if (was_detached == -1) {
 		result = 1;
+		goto cleanup;
+	}
+	if (follow_remote_head == FOLLOW_REMOTE_WARN && verbosity >= 0)
+		report_set_head(remote, head_name, &b_local_head, was_detached);
 
 cleanup:
 	free(head_name);
@@ -1636,6 +1669,7 @@ static int set_head(const struct ref *remote_refs)
 	free_refs(matches);
 	string_list_clear(&heads, 0);
 	strbuf_release(&b_head);
+	strbuf_release(&b_local_head);
 	strbuf_release(&b_remote_head);
 	return result;
 }
@@ -1855,7 +1889,7 @@ static int do_fetch(struct transport *transport,
 				  "you need to specify exactly one branch with the --set-upstream option"));
 		}
 	}
-	if (set_head(remote_refs))
+	if (set_head(remote_refs, transport->remote->follow_remote_head))
 		;
 		/*
 		 * Way too many cases where this can go wrong
diff --git a/remote.c b/remote.c
index 10104d11e3..0b18840d43 100644
--- a/remote.c
+++ b/remote.c
@@ -514,6 +514,15 @@ static int handle_config(const char *key, const char *value,
 	} else if (!strcmp(subkey, "serveroption")) {
 		return parse_transport_option(key, value,
 					      &remote->server_options);
+	} else if (!strcmp(subkey, "followremotehead")) {
+		if (!strcmp(value, "never"))
+			remote->follow_remote_head = FOLLOW_REMOTE_NEVER;
+		else if (!strcmp(value, "create"))
+			remote->follow_remote_head = FOLLOW_REMOTE_CREATE;
+		else if (!strcmp(value, "warn"))
+			remote->follow_remote_head = FOLLOW_REMOTE_WARN;
+		else if (!strcmp(value, "always"))
+			remote->follow_remote_head = FOLLOW_REMOTE_ALWAYS;
 	}
 	return 0;
 }
diff --git a/remote.h b/remote.h
index a7e5c4e07c..184b35653d 100644
--- a/remote.h
+++ b/remote.h
@@ -59,6 +59,13 @@ struct remote_state {
 void remote_state_clear(struct remote_state *remote_state);
 struct remote_state *remote_state_new(void);
 
+	enum follow_remote_head_settings {
+		FOLLOW_REMOTE_NEVER = -1,
+		FOLLOW_REMOTE_CREATE = 0,
+		FOLLOW_REMOTE_WARN = 1,
+		FOLLOW_REMOTE_ALWAYS = 2,
+	};
+
 struct remote {
 	struct hashmap_entry ent;
 
@@ -107,6 +114,8 @@ struct remote {
 	char *http_proxy_authmethod;
 
 	struct string_list server_options;
+
+	enum follow_remote_head_settings follow_remote_head;
 };
 
 /**
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index 87698341f5..2467027d34 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -99,6 +99,108 @@ test_expect_success "fetch test remote HEAD change" '
 	branch=$(git rev-parse refs/remotes/origin/other) &&
 	test "z$head" = "z$branch"'
 
+test_expect_success "fetch test followRemoteHEAD never" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
+		git config set remote.origin.followRemoteHEAD "never" &&
+		git fetch &&
+		test_must_fail git rev-parse --verify refs/remotes/origin/HEAD
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD warn no change" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git rev-parse --verify refs/remotes/origin/other &&
+		git remote set-head origin other &&
+		git rev-parse --verify refs/remotes/origin/HEAD &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		git config set remote.origin.followRemoteHEAD "warn" &&
+		git fetch >output &&
+		echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
+			"but we have ${SQ}other${SQ} locally." >expect &&
+		echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change." >>expect &&
+		test_cmp expect output &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/other) &&
+		test "z$head" = "z$branch"
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD warn create" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
+		git config set remote.origin.followRemoteHEAD "warn" &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		output=$(git fetch) &&
+		test "z" = "z$output" &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/main) &&
+		test "z$head" = "z$branch"
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD warn detached" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
+		git update-ref refs/remotes/origin/HEAD HEAD &&
+		HEAD=$(git log --pretty="%H") &&
+		git config set remote.origin.followRemoteHEAD "warn" &&
+		git fetch >output &&
+		echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
+			"but we have a detached HEAD pointing to" \
+			"${SQ}${HEAD}${SQ} locally." >expect &&
+		echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change." >>expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD warn quiet" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git rev-parse --verify refs/remotes/origin/other &&
+		git remote set-head origin other &&
+		git rev-parse --verify refs/remotes/origin/HEAD &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		git config set remote.origin.followRemoteHEAD "warn" &&
+		output=$(git fetch --quiet) &&
+		test "z" = "z$output" &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/other) &&
+		test "z$head" = "z$branch"
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD always" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git rev-parse --verify refs/remotes/origin/other &&
+		git remote set-head origin other &&
+		git rev-parse --verify refs/remotes/origin/HEAD &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		git config set remote.origin.followRemoteHEAD "always" &&
+		git fetch &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/main) &&
+		test "z$head" = "z$branch"
+	)
+'
+
 test_expect_success 'fetch --prune on its own works as expected' '
 	cd "$D" &&
 	git clone . prune &&

Range-diff against v2:
1:  60986e890e ! 1:  4e9ac5524a fetch: add configuration for set_head behaviour
    @@ Notes
             - since the enum is descriptive (well, along with the documentation)
               I felt there is no need for the comment explaining the values
     
    +    v3: missed a magic number in v2
    +
      ## Documentation/config/remote.txt ##
     @@ Documentation/config/remote.txt: remote.<name>.serverOption::
      	The default set of server options used when fetching from this remote.
    @@ builtin/fetch.c: static int set_head(const struct ref *remote_refs)
      	if (!head_name)
      		goto cleanup;
      	is_bare = is_bare_repository();
    -+	create_only = follow_remote_head == 2 ? 0 : !is_bare;
    ++	create_only = follow_remote_head == FOLLOW_REMOTE_ALWAYS ? 0 : !is_bare;
      	if (is_bare) {
      		strbuf_addstr(&b_head, "HEAD");
      		strbuf_addf(&b_remote_head, "refs/heads/%s", head_name);
-- 
2.47.1.295.g66bad26ef4
Junio C Hamano· Dec 2, 2024, 00:58 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v3] fetch: add configuration for set_head behaviour

Bence Ferdinandy <bence@ferdinandy.com> writes:
Show 6 quoted lines
>     v2:
>         - magic numbers converted to enum
>         - since the enum is descriptive (well, along with the documentation)
>           I felt there is no need for the comment explaining the values
>     
>     v3: missed a magic number in v2
Both changes look quite good.  Will queue.
Let me mark the topic for 'next'.
Thanks.
Bence Ferdinandy· Nov 28, 2024, 23:09 UTC · re: Bence Ferdinandy · lore

[PATCH v3] fetch: add configuration for set_head behaviour

In the current implementation, if refs/remotes/$remote/HEAD does not exist, running fetch will create it, but if it does exist it will not do anything, which is a somewhat safe and minimal approach. Unfortunately, for users who wish to NOT have refs/remotes/$remote/HEAD set for any reason (e.g. so that `git rev-parse origin` doesn't accidentally point them somewhere they do not want to), there is no way to remove this behaviour. On the other side of the spectrum, users may want fetch to automatically update HEAD or at least give them a warning if something changed on the remote.

Introduce a new setting, remote.$remote.followRemoteHEAD with four options:

    - "never": do not ever do anything, not even create
    - "create": the current behaviour, now the default behaviour
    - "warn": print a message if remote and local HEAD is different
    - "always": silently update HEAD on every change
Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    This patch is built off v15 of the set-head/fetch updates series:
    https://lore.kernel.org/git/D5WEJJBMSO1K.2TPXDI1K08SHT@ferdinandy.com/T/#m13a0c3e8919872188ef07fd9fd984c78e8aa35ba
    
    v2:
        - magic numbers converted to enum
        - since the enum is descriptive (well, along with the documentation)
          I felt there is no need for the comment explaining the values
    
    v3: missed a magic number in v2
 Documentation/config/remote.txt |  11 ++++
 builtin/fetch.c                 |  46 ++++++++++++--
 remote.c                        |   9 +++
 remote.h                        |   9 +++
 t/t5510-fetch.sh                | 102 ++++++++++++++++++++++++++++++++
 5 files changed, 171 insertions(+), 6 deletions(-)
Show changes to 5 files +171 −6

Documentation/config/remote.txt, builtin/fetch.c, remote.c, remote.h, t/t5510-fetch.sh

diff --git a/Documentation/config/remote.txt b/Documentation/config/remote.txt
index 6d8b7d6c63..024f92befc 100644
--- a/Documentation/config/remote.txt
+++ b/Documentation/config/remote.txt
@@ -101,6 +101,17 @@ remote.<name>.serverOption::
 	The default set of server options used when fetching from this remote.
 	These server options can be overridden by the `--server-option=` command
 	line arguments.
+
+remote.<name>.followRemoteHEAD::
+	How linkgit:git-fetch[1] should handle updates to `remotes/<name>/HEAD`.
+	The default value is "create", which will create `remotes/<name>/HEAD`
+	if it exists on the remote, but not locally, but will not touch an
+	already existing local reference.  Setting to "warn" will print
+	a message if the remote has a different value, than the local one and
+	in case there is no local reference, it behaves like "create". Setting
+	to "always" will silently update it to the value on the remote.
+	Finally, setting it to "never" will never change or create the local
+	reference.
 +
 This is a multi-valued variable, and an empty value can be used in a higher
 priority configuration file (e.g. `.git/config` in a repository) to clear
diff --git a/builtin/fetch.c b/builtin/fetch.c
index 2f416cf867..88c5c5d781 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1579,10 +1579,35 @@ static const char *strip_refshead(const char *name){
 	return name;
 }
 
-static int set_head(const struct ref *remote_refs)
+static void report_set_head(const char *remote, const char *head_name,
+			struct strbuf *buf_prev, int updateres) {
+	struct strbuf buf_prefix = STRBUF_INIT;
+	const char *prev_head = NULL;
+
+	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
+	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
+
+	if (prev_head && strcmp(prev_head, head_name)) {
+		printf("'HEAD' at '%s' is '%s', but we have '%s' locally.\n",
+			remote, head_name, prev_head);
+		printf("Run 'git remote set-head %s %s' to follow the change.\n",
+			remote, head_name);
+	}
+	else if (updateres && buf_prev->len) {
+		printf("'HEAD' at '%s' is '%s', "
+			"but we have a detached HEAD pointing to '%s' locally.\n",
+			remote, head_name, buf_prev->buf);
+		printf("Run 'git remote set-head %s %s' to follow the change.\n",
+			remote, head_name);
+	}
+	strbuf_release(&buf_prefix);
+}
+
+static int set_head(const struct ref *remote_refs, int follow_remote_head)
 {
-	int result = 0, is_bare;
-	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT;
+	int result = 0, create_only, is_bare, was_detached;
+	struct strbuf b_head = STRBUF_INIT, b_remote_head = STRBUF_INIT,
+		      b_local_head = STRBUF_INIT;
 	const char *remote = gtransport->remote->name;
 	char *head_name = NULL;
 	struct ref *ref, *matches;
@@ -1603,6 +1628,8 @@ static int set_head(const struct ref *remote_refs)
 		string_list_append(&heads, strip_refshead(ref->name));
 	}
 
+	if (follow_remote_head == FOLLOW_REMOTE_NEVER)
+		goto cleanup;
 
 	if (!heads.nr)
 		result = 1;
@@ -1614,6 +1641,7 @@ static int set_head(const struct ref *remote_refs)
 	if (!head_name)
 		goto cleanup;
 	is_bare = is_bare_repository();
+	create_only = follow_remote_head == FOLLOW_REMOTE_ALWAYS ? 0 : !is_bare;
 	if (is_bare) {
 		strbuf_addstr(&b_head, "HEAD");
 		strbuf_addf(&b_remote_head, "refs/heads/%s", head_name);
@@ -1626,9 +1654,14 @@ static int set_head(const struct ref *remote_refs)
 		result = 1;
 		goto cleanup;
 	}
-	if (refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
-					"fetch", NULL, !is_bare))
+	was_detached = refs_update_symref_extended(refs, b_head.buf, b_remote_head.buf,
+					"fetch", &b_local_head, create_only);
+	if (was_detached == -1) {
 		result = 1;
+		goto cleanup;
+	}
+	if (follow_remote_head == FOLLOW_REMOTE_WARN && verbosity >= 0)
+		report_set_head(remote, head_name, &b_local_head, was_detached);
 
 cleanup:
 	free(head_name);
@@ -1636,6 +1669,7 @@ static int set_head(const struct ref *remote_refs)
 	free_refs(matches);
 	string_list_clear(&heads, 0);
 	strbuf_release(&b_head);
+	strbuf_release(&b_local_head);
 	strbuf_release(&b_remote_head);
 	return result;
 }
@@ -1855,7 +1889,7 @@ static int do_fetch(struct transport *transport,
 				  "you need to specify exactly one branch with the --set-upstream option"));
 		}
 	}
-	if (set_head(remote_refs))
+	if (set_head(remote_refs, transport->remote->follow_remote_head))
 		;
 		/*
 		 * Way too many cases where this can go wrong
diff --git a/remote.c b/remote.c
index 10104d11e3..0b18840d43 100644
--- a/remote.c
+++ b/remote.c
@@ -514,6 +514,15 @@ static int handle_config(const char *key, const char *value,
 	} else if (!strcmp(subkey, "serveroption")) {
 		return parse_transport_option(key, value,
 					      &remote->server_options);
+	} else if (!strcmp(subkey, "followremotehead")) {
+		if (!strcmp(value, "never"))
+			remote->follow_remote_head = FOLLOW_REMOTE_NEVER;
+		else if (!strcmp(value, "create"))
+			remote->follow_remote_head = FOLLOW_REMOTE_CREATE;
+		else if (!strcmp(value, "warn"))
+			remote->follow_remote_head = FOLLOW_REMOTE_WARN;
+		else if (!strcmp(value, "always"))
+			remote->follow_remote_head = FOLLOW_REMOTE_ALWAYS;
 	}
 	return 0;
 }
diff --git a/remote.h b/remote.h
index a7e5c4e07c..184b35653d 100644
--- a/remote.h
+++ b/remote.h
@@ -59,6 +59,13 @@ struct remote_state {
 void remote_state_clear(struct remote_state *remote_state);
 struct remote_state *remote_state_new(void);
 
+	enum follow_remote_head_settings {
+		FOLLOW_REMOTE_NEVER = -1,
+		FOLLOW_REMOTE_CREATE = 0,
+		FOLLOW_REMOTE_WARN = 1,
+		FOLLOW_REMOTE_ALWAYS = 2,
+	};
+
 struct remote {
 	struct hashmap_entry ent;
 
@@ -107,6 +114,8 @@ struct remote {
 	char *http_proxy_authmethod;
 
 	struct string_list server_options;
+
+	enum follow_remote_head_settings follow_remote_head;
 };
 
 /**
diff --git a/t/t5510-fetch.sh b/t/t5510-fetch.sh
index 87698341f5..2467027d34 100755
--- a/t/t5510-fetch.sh
+++ b/t/t5510-fetch.sh
@@ -99,6 +99,108 @@ test_expect_success "fetch test remote HEAD change" '
 	branch=$(git rev-parse refs/remotes/origin/other) &&
 	test "z$head" = "z$branch"'
 
+test_expect_success "fetch test followRemoteHEAD never" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
+		git config set remote.origin.followRemoteHEAD "never" &&
+		git fetch &&
+		test_must_fail git rev-parse --verify refs/remotes/origin/HEAD
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD warn no change" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git rev-parse --verify refs/remotes/origin/other &&
+		git remote set-head origin other &&
+		git rev-parse --verify refs/remotes/origin/HEAD &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		git config set remote.origin.followRemoteHEAD "warn" &&
+		git fetch >output &&
+		echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
+			"but we have ${SQ}other${SQ} locally." >expect &&
+		echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change." >>expect &&
+		test_cmp expect output &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/other) &&
+		test "z$head" = "z$branch"
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD warn create" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
+		git config set remote.origin.followRemoteHEAD "warn" &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		output=$(git fetch) &&
+		test "z" = "z$output" &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/main) &&
+		test "z$head" = "z$branch"
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD warn detached" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git update-ref --no-deref -d refs/remotes/origin/HEAD &&
+		git update-ref refs/remotes/origin/HEAD HEAD &&
+		HEAD=$(git log --pretty="%H") &&
+		git config set remote.origin.followRemoteHEAD "warn" &&
+		git fetch >output &&
+		echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
+			"but we have a detached HEAD pointing to" \
+			"${SQ}${HEAD}${SQ} locally." >expect &&
+		echo "Run ${SQ}git remote set-head origin main${SQ} to follow the change." >>expect &&
+		test_cmp expect output
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD warn quiet" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git rev-parse --verify refs/remotes/origin/other &&
+		git remote set-head origin other &&
+		git rev-parse --verify refs/remotes/origin/HEAD &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		git config set remote.origin.followRemoteHEAD "warn" &&
+		output=$(git fetch --quiet) &&
+		test "z" = "z$output" &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/other) &&
+		test "z$head" = "z$branch"
+	)
+'
+
+test_expect_success "fetch test followRemoteHEAD always" '
+	test_when_finished "git config unset remote.origin.followRemoteHEAD" &&
+	(
+		cd "$D" &&
+		cd two &&
+		git rev-parse --verify refs/remotes/origin/other &&
+		git remote set-head origin other &&
+		git rev-parse --verify refs/remotes/origin/HEAD &&
+		git rev-parse --verify refs/remotes/origin/main &&
+		git config set remote.origin.followRemoteHEAD "always" &&
+		git fetch &&
+		head=$(git rev-parse refs/remotes/origin/HEAD) &&
+		branch=$(git rev-parse refs/remotes/origin/main) &&
+		test "z$head" = "z$branch"
+	)
+'
+
 test_expect_success 'fetch --prune on its own works as expected' '
 	cd "$D" &&
 	git clone . prune &&

Range-diff against v2:
1:  60986e890e ! 1:  4e9ac5524a fetch: add configuration for set_head behaviour
    @@ Notes
             - since the enum is descriptive (well, along with the documentation)
               I felt there is no need for the comment explaining the values
     
    +    v3: missed a magic number in v2
    +
      ## Documentation/config/remote.txt ##
     @@ Documentation/config/remote.txt: remote.<name>.serverOption::
      	The default set of server options used when fetching from this remote.
    @@ builtin/fetch.c: static int set_head(const struct ref *remote_refs)
      	if (!head_name)
      		goto cleanup;
      	is_bare = is_bare_repository();
    -+	create_only = follow_remote_head == 2 ? 0 : !is_bare;
    ++	create_only = follow_remote_head == FOLLOW_REMOTE_ALWAYS ? 0 : !is_bare;
      	if (is_bare) {
      		strbuf_addstr(&b_head, "HEAD");
      		strbuf_addf(&b_remote_head, "refs/heads/%s", head_name);
-- 
2.47.1.295.g66bad26ef4
Bence Ferdinandy· Oct 9, 2024, 13:57 UTC · re: Bence Ferdinandy · lore

[PATCH v5 2/6] set-head: add new variable for readability

Instead of calling get_main_ref_store(the_repository) multiple times, call it once and store in a new refs variable. Although this change probably offers some performance benefits, the main purpose is to shorten the line lengths of function calls using this variable for better readability. ---

Notes:
    v5: new patch (split from the next patch as a preparatory step)
 builtin/remote.c | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)
Show changes to builtin/remote.c +4 −3
diff --git a/builtin/remote.c b/builtin/remote.c
index d8ff440027..353ffd2c43 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1404,6 +1404,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 	int i, opt_a = 0, opt_d = 0, result = 0;
 	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;
 	char *head_name = NULL;
+	struct ref_store *refs = get_main_ref_store(the_repository);
 
 	struct option options[] = {
 		OPT_BOOL('a', "auto", &opt_a,
@@ -1434,7 +1435,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
 			head_name = xstrdup(states.heads.items[0].string);
 		free_remote_ref_states(&states);
 	} else if (opt_d && !opt_a && argc == 1) {
-		if (refs_delete_ref(get_main_ref_store(the_repository), NULL, buf.buf, NULL, REF_NO_DEREF))
+		if (refs_delete_ref(refs, NULL, buf.buf, NULL, REF_NO_DEREF))
 			result |= error(_("Could not delete %s"), buf.buf);
 	} else
 		usage_with_options(builtin_remote_sethead_usage, options);
@@ -1442,9 +1443,9 @@ static int set_head(int argc, const char **argv, const char *prefix)
 	if (head_name) {
 		strbuf_addf(&buf2, "refs/remotes/%s/%s", argv[0], head_name);
 		/* make sure it's valid */
-		if (!refs_ref_exists(get_main_ref_store(the_repository), buf2.buf))
+		if (!refs_ref_exists(refs, buf2.buf))
 			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head", NULL))
+		else if (refs_update_symref(refs, buf.buf, buf2.buf, "remote set-head", NULL))
 			result |= error(_("Could not setup %s"), buf.buf);
 		else if (opt_a)
 			printf("%s/HEAD set to %s\n", argv[0], head_name);
-- 
2.47.0.6.g9542df9767
Junio C Hamano· Oct 9, 2024, 19:26 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v5 2/6] set-head: add new variable for readability

Bence Ferdinandy <bence@ferdinandy.com> writes:
Show 9 quoted lines
> Instead of calling get_main_ref_store(the_repository) multiple times,
> call it once and store in a new refs variable. Although this change
> probably offers some performance benefits, the main purpose is to
> shorten the line lengths of function calls using this variable for
> better readability.
> ---
>
> Notes:
>     v5: new patch (split from the next patch as a preparatory step)

It is a good idea to help making it obvious that we are accessing the same ref store.

The patch needs to be signed-off, though.
Show 36 quoted lines
>  builtin/remote.c | 7 ++++---
>  1 file changed, 4 insertions(+), 3 deletions(-)
>
> diff --git a/builtin/remote.c b/builtin/remote.c
> index d8ff440027..353ffd2c43 100644
> --- a/builtin/remote.c
> +++ b/builtin/remote.c
> @@ -1404,6 +1404,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
>  	int i, opt_a = 0, opt_d = 0, result = 0;
>  	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;
>  	char *head_name = NULL;
> +	struct ref_store *refs = get_main_ref_store(the_repository);
>  
>  	struct option options[] = {
>  		OPT_BOOL('a', "auto", &opt_a,
> @@ -1434,7 +1435,7 @@ static int set_head(int argc, const char **argv, const char *prefix)
>  			head_name = xstrdup(states.heads.items[0].string);
>  		free_remote_ref_states(&states);
>  	} else if (opt_d && !opt_a && argc == 1) {
> -		if (refs_delete_ref(get_main_ref_store(the_repository), NULL, buf.buf, NULL, REF_NO_DEREF))
> +		if (refs_delete_ref(refs, NULL, buf.buf, NULL, REF_NO_DEREF))
>  			result |= error(_("Could not delete %s"), buf.buf);
>  	} else
>  		usage_with_options(builtin_remote_sethead_usage, options);
> @@ -1442,9 +1443,9 @@ static int set_head(int argc, const char **argv, const char *prefix)
>  	if (head_name) {
>  		strbuf_addf(&buf2, "refs/remotes/%s/%s", argv[0], head_name);
>  		/* make sure it's valid */
> -		if (!refs_ref_exists(get_main_ref_store(the_repository), buf2.buf))
> +		if (!refs_ref_exists(refs, buf2.buf))
>  			result |= error(_("Not a valid ref: %s"), buf2.buf);
> -		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head", NULL))
> +		else if (refs_update_symref(refs, buf.buf, buf2.buf, "remote set-head", NULL))
>  			result |= error(_("Could not setup %s"), buf.buf);
>  		else if (opt_a)
>  			printf("%s/HEAD set to %s\n", argv[0], head_name);
Bence Ferdinandy· Oct 9, 2024, 19:47 UTC · re: Junio C Hamano · lore

Re: [PATCH v5 2/6] set-head: add new variable for readability

On Wed Oct 09, 2024 at 21:26, Junio C Hamano <gitster@pobox.com> wrote:
Show 14 quoted lines
> Bence Ferdinandy <bence@ferdinandy.com> writes:
>
>> Instead of calling get_main_ref_store(the_repository) multiple times,
>> call it once and store in a new refs variable. Although this change
>> probably offers some performance benefits, the main purpose is to
>> shorten the line lengths of function calls using this variable for
>> better readability.
>> ---
>>
>> Notes:
>>     v5: new patch (split from the next patch as a preparatory step)
>
> It is a good idea to help making it obvious that we are accessing
> the same ref store.
Indeed, although it was your idea ;)
>
> The patch needs to be signed-off, though.

Damn :/ Sorry, another oversight ... Should I send a v6 for the series with this fixed now or wait a bit to see if there are other comments?

Thanks again for the patience!

Best, Bence

Bence Ferdinandy· Oct 9, 2024, 13:57 UTC · re: Bence Ferdinandy · lore

[PATCH v5 4/6] transaction: add TRANSACTION_CREATE_EXISTS error

Currently there is only one special error for transaction, for when there is a naming conflict, all other errors are dumped under a generic error. Add a new special error case for when the caller requests the reference to be updated only when it does not yet exist and the reference actually does exist.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v4: new patch
    v5: no change
 refs.h                  |  4 +++-
 refs/files-backend.c    | 28 ++++++++++++++++++++--------
 refs/reftable-backend.c |  6 ++++--
 3 files changed, 27 insertions(+), 11 deletions(-)
Show changes to 3 files +27 −11

refs.h, refs/files-backend.c, refs/reftable-backend.c

diff --git a/refs.h b/refs.h
index f38616db84..166affbc89 100644
--- a/refs.h
+++ b/refs.h
@@ -759,8 +759,10 @@ int ref_transaction_verify(struct ref_transaction *transaction,
 
 /* Naming conflict (for example, the ref names A and A/B conflict). */
 #define TRANSACTION_NAME_CONFLICT -1
+/* When only creation was requested, but the ref already exists. */
+#define TRANSACTION_CREATE_EXISTS -2
 /* All other errors. */
-#define TRANSACTION_GENERIC_ERROR -2
+#define TRANSACTION_GENERIC_ERROR -3
 
 /*
  * Perform the preparatory stages of committing `transaction`. Acquire
diff --git a/refs/files-backend.c b/refs/files-backend.c
index 8415f2d020..272ad81315 100644
--- a/refs/files-backend.c
+++ b/refs/files-backend.c
@@ -2502,14 +2502,18 @@ static int split_symref_update(struct ref_update *update,
 static int check_old_oid(struct ref_update *update, struct object_id *oid,
 			 struct strbuf *err)
 {
+	int ret = TRANSACTION_GENERIC_ERROR;
+
 	if (!(update->flags & REF_HAVE_OLD) ||
 		   oideq(oid, &update->old_oid))
 		return 0;
 
-	if (is_null_oid(&update->old_oid))
+	if (is_null_oid(&update->old_oid)) {
 		strbuf_addf(err, "cannot lock ref '%s': "
 			    "reference already exists",
 			    ref_update_original_update_refname(update));
+		ret = TRANSACTION_CREATE_EXISTS;
+	}
 	else if (is_null_oid(oid))
 		strbuf_addf(err, "cannot lock ref '%s': "
 			    "reference is missing but expected %s",
@@ -2522,7 +2526,7 @@ static int check_old_oid(struct ref_update *update, struct object_id *oid,
 			    oid_to_hex(oid),
 			    oid_to_hex(&update->old_oid));
 
-	return -1;
+	return ret;
 }
 
 /*
@@ -2603,9 +2607,13 @@ static int lock_ref_for_update(struct files_ref_store *refs,
 					ret = TRANSACTION_GENERIC_ERROR;
 					goto out;
 				}
-			} else if  (check_old_oid(update, &lock->old_oid, err)) {
-				ret = TRANSACTION_GENERIC_ERROR;
-				goto out;
+			} else {
+				int checkret;
+				checkret = check_old_oid(update, &lock->old_oid, err);
+				if  (checkret) {
+					ret = checkret;
+					goto out;
+				}
 			}
 		} else {
 			/*
@@ -2636,9 +2644,13 @@ static int lock_ref_for_update(struct files_ref_store *refs,
 				    update->old_target);
 			ret = TRANSACTION_GENERIC_ERROR;
 			goto out;
-		} else if  (check_old_oid(update, &lock->old_oid, err)) {
-			ret = TRANSACTION_GENERIC_ERROR;
-			goto out;
+		} else {
+			int checkret;
+			checkret = check_old_oid(update, &lock->old_oid, err);
+			if  (checkret) {
+				ret = checkret;
+				goto out;
+			}
 		}
 
 		/*
diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
index 32330b6bc6..c6b25ebac4 100644
--- a/refs/reftable-backend.c
+++ b/refs/reftable-backend.c
@@ -1206,10 +1206,13 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 				goto done;
 			}
 		} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {
-			if (is_null_oid(&u->old_oid))
+			ret = TRANSACTION_NAME_CONFLICT;
+			if (is_null_oid(&u->old_oid)) {
 				strbuf_addf(err, _("cannot lock ref '%s': "
 						   "reference already exists"),
 					    ref_update_original_update_refname(u));
+				ret = TRANSACTION_CREATE_EXISTS;
+			}
 			else if (is_null_oid(&current_oid))
 				strbuf_addf(err, _("cannot lock ref '%s': "
 						   "reference is missing but expected %s"),
@@ -1221,7 +1224,6 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
 					    ref_update_original_update_refname(u),
 					    oid_to_hex(&current_oid),
 					    oid_to_hex(&u->old_oid));
-			ret = -1;
 			goto done;
 		}
 
-- 
2.47.0.6.g9542df9767
Junio C Hamano· Oct 9, 2024, 21:08 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v5 4/6] transaction: add TRANSACTION_CREATE_EXISTS error

Bence Ferdinandy <bence@ferdinandy.com> writes:
Show 7 quoted lines
>  /* Naming conflict (for example, the ref names A and A/B conflict). */
>  #define TRANSACTION_NAME_CONFLICT -1
> +/* When only creation was requested, but the ref already exists. */
> +#define TRANSACTION_CREATE_EXISTS -2
>  /* All other errors. */
> -#define TRANSACTION_GENERIC_ERROR -2
> +#define TRANSACTION_GENERIC_ERROR -3

My initial knee-jerk reaction to the name "CREATE_EXISTS" was that the "CREATE" part is redundant, as the only case that the fact that something exists is a valid reason for an error is when we attempted to create it. EEXIST in errno(3) is not ECREATE_EXIST for the same reason.

But let's let it pass; contrasting with the fact that what ought to be named similarly to EISDIR is called NAME_CONFLICT, CREATE_EXISTS is fine.

Show 31 quoted lines
> diff --git a/refs/files-backend.c b/refs/files-backend.c
> index 8415f2d020..272ad81315 100644
> --- a/refs/files-backend.c
> +++ b/refs/files-backend.c
> @@ -2502,14 +2502,18 @@ static int split_symref_update(struct ref_update *update,
>  static int check_old_oid(struct ref_update *update, struct object_id *oid,
>  			 struct strbuf *err)
>  {
> +	int ret = TRANSACTION_GENERIC_ERROR;
> +
>  	if (!(update->flags & REF_HAVE_OLD) ||
>  		   oideq(oid, &update->old_oid))
>  		return 0;
>  
> -	if (is_null_oid(&update->old_oid))
> +	if (is_null_oid(&update->old_oid)) {
>  		strbuf_addf(err, "cannot lock ref '%s': "
>  			    "reference already exists",
>  			    ref_update_original_update_refname(update));
> +		ret = TRANSACTION_CREATE_EXISTS;
> +	}
>  	else if (is_null_oid(oid))
>  		strbuf_addf(err, "cannot lock ref '%s': "
>  			    "reference is missing but expected %s",
> @@ -2522,7 +2526,7 @@ static int check_old_oid(struct ref_update *update, struct object_id *oid,
>  			    oid_to_hex(oid),
>  			    oid_to_hex(&update->old_oid));
>  
> -	return -1;
> +	return ret;
>  }

OK. So if a caller does not care which exact kind of error it got, they will keep working just the way they used to work, and a caller that cares can now check. A nice API enhancement.

Show 15 quoted lines
> @@ -2603,9 +2607,13 @@ static int lock_ref_for_update(struct files_ref_store *refs,
>  					ret = TRANSACTION_GENERIC_ERROR;
>  					goto out;
>  				}
> -			} else if  (check_old_oid(update, &lock->old_oid, err)) {
> -				ret = TRANSACTION_GENERIC_ERROR;
> -				goto out;
> +			} else {
> +				int checkret;
> +				checkret = check_old_oid(update, &lock->old_oid, err);
> +				if  (checkret) {
> +					ret = checkret;
> +					goto out;
> +				}
>  			}

Likewise; this code is just propagating what check_old_oid() did to its callers.

Show 14 quoted lines
> @@ -2636,9 +2644,13 @@ static int lock_ref_for_update(struct files_ref_store *refs,
>  				    update->old_target);
>  			ret = TRANSACTION_GENERIC_ERROR;
>  			goto out;
> -		} else if  (check_old_oid(update, &lock->old_oid, err)) {
> -			ret = TRANSACTION_GENERIC_ERROR;
> -			goto out;
> +		} else {
> +			int checkret;
> +			checkret = check_old_oid(update, &lock->old_oid, err);
> +			if  (checkret) {
> +				ret = checkret;
> +				goto out;
> +			}
Ditto.
Show 26 quoted lines
> diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
> index 32330b6bc6..c6b25ebac4 100644
> --- a/refs/reftable-backend.c
> +++ b/refs/reftable-backend.c
> @@ -1206,10 +1206,13 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
>  				goto done;
>  			}
>  		} else if ((u->flags & REF_HAVE_OLD) && !oideq(&current_oid, &u->old_oid)) {
> -			if (is_null_oid(&u->old_oid))
> +			ret = TRANSACTION_NAME_CONFLICT;
> +			if (is_null_oid(&u->old_oid)) {
>  				strbuf_addf(err, _("cannot lock ref '%s': "
>  						   "reference already exists"),
>  					    ref_update_original_update_refname(u));
> +				ret = TRANSACTION_CREATE_EXISTS;
> +			}
>  			else if (is_null_oid(&current_oid))
>  				strbuf_addf(err, _("cannot lock ref '%s': "
>  						   "reference is missing but expected %s"),
> @@ -1221,7 +1224,6 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
>  					    ref_update_original_update_refname(u),
>  					    oid_to_hex(&current_oid),
>  					    oid_to_hex(&u->old_oid));
> -			ret = -1;
>  			goto done;
>  		}

So it remains a little mystery how the ultimate caller uses this new bit of information.

Which is OK---the next step seems to have a new condition on the error status.

Bence Ferdinandy· Oct 9, 2024, 13:57 UTC · re: Bence Ferdinandy · lore

[PATCH v5 3/6] set-head: better output for --auto

Currently, set-head --auto will print a message saying "remote/HEAD set to branch", which implies something was changed.

Change the output of --auto, so the output actually reflects what was done: a) set a previously unset HEAD, b) change HEAD because remote changed or c) no updates.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
Notes:
    v1-v2:
    
        was RFC in https://lore.kernel.org/git/20240910203835.2288291-1-bence@ferdinandy.com/
    
    v3:
    
        This patch was originally sent along when I thought set-head was
        going to be invoked by fetch, but the discussion on the RFC
        concluded that it should be not. This opened the possibility to make
        it more explicit.
    
        Note: although I feel both things the patch does are really just
        cosmetic, an argument could be made for breaking it into two, one
        for the no-op part and one for the --auto print update.
    
        Was sent in:
        https://lore.kernel.org/git/20240915221055.904107-1-bence@ferdinandy.com/
    
    v4:
        - changes are now handled atomically via the ref update transaction
        - outputs have changed along the lines of Junio's suggestion
        - minor refactor to set_head for improved legibility
    
    v5: - the minor refactor has been split out into its own patch
 builtin/remote.c  | 30 ++++++++++++++++++++++++++----
 t/t5505-remote.sh | 13 ++++++++++++-
 2 files changed, 38 insertions(+), 5 deletions(-)
Show changes to 2 files +38 −5

builtin/remote.c, t/t5505-remote.sh

diff --git a/builtin/remote.c b/builtin/remote.c
index 353ffd2c43..2480128b88 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1399,10 +1399,30 @@ static int show(int argc, const char **argv, const char *prefix)
 	return result;
 }
 
+static void report_auto(const char *remote, const char *head_name,
+			struct strbuf *buf_prev) {
+	struct strbuf buf_prefix = STRBUF_INIT;
+	const char *prev_head;
+
+	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
+	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
+
+	if (prev_head && !strcmp(prev_head, head_name))
+		printf("'%s/HEAD' is unchanged and points to '%s'\n",
+			remote, head_name);
+	else if (prev_head)
+		printf("'%s/HEAD' has changed from '%s' and now points to '%s'\n",
+			remote, prev_head, head_name);
+	else
+		printf("'%s/HEAD' is now created and points to '%s'\n",
+			remote, head_name);
+}
+
 static int set_head(int argc, const char **argv, const char *prefix)
 {
 	int i, opt_a = 0, opt_d = 0, result = 0;
-	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;
+	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT,
+		buf_prev = STRBUF_INIT;
 	char *head_name = NULL;
 	struct ref_store *refs = get_main_ref_store(the_repository);
 
@@ -1445,15 +1465,17 @@ static int set_head(int argc, const char **argv, const char *prefix)
 		/* make sure it's valid */
 		if (!refs_ref_exists(refs, buf2.buf))
 			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(refs, buf.buf, buf2.buf, "remote set-head", NULL))
+		else if (refs_update_symref(refs, buf.buf, buf2.buf, "remote set-head", &buf_prev))
 			result |= error(_("Could not setup %s"), buf.buf);
-		else if (opt_a)
-			printf("%s/HEAD set to %s\n", argv[0], head_name);
+		else if (opt_a) {
+			report_auto(argv[0], head_name, &buf_prev);
+		}
 		free(head_name);
 	}
 
 	strbuf_release(&buf);
 	strbuf_release(&buf2);
+	strbuf_release(&buf_prev);
 	return result;
 }
 
diff --git a/t/t5505-remote.sh b/t/t5505-remote.sh
index 532035933f..262a4de0aa 100755
--- a/t/t5505-remote.sh
+++ b/t/t5505-remote.sh
@@ -434,7 +434,7 @@ test_expect_success 'set-head --auto has no problem w/multiple HEADs' '
 		cd test &&
 		git fetch two "refs/heads/*:refs/remotes/two/*" &&
 		git remote set-head --auto two >output 2>&1 &&
-		echo "two/HEAD set to main" >expect &&
+		echo "'\''two/HEAD'\'' is unchanged and points to '\''main'\''" >expect &&
 		test_cmp expect output
 	)
 '
@@ -453,6 +453,17 @@ test_expect_success 'set-head explicit' '
 	)
 '
 
+
+test_expect_success 'set-head --auto reports change' '
+	(
+		cd test &&
+		git remote set-head origin side2 &&
+		git remote set-head --auto origin >output 2>&1 &&
+		echo "'\''origin/HEAD'\'' has changed from '\''side2'\'' and now points to '\''main'\''" >expect &&
+		test_cmp expect output
+	)
+'
+
 cat >test/expect <<EOF
 Pruning origin
 URL: $(pwd)/one
-- 
2.47.0.6.g9542df9767
Junio C Hamano· Oct 9, 2024, 20:53 UTC · re: Bence Ferdinandy · lore

Re: [PATCH v5 3/6] set-head: better output for --auto

Bence Ferdinandy <bence@ferdinandy.com> writes:
> +static void report_auto(const char *remote, const char *head_name,
> +			struct strbuf *buf_prev) {
> +	struct strbuf buf_prefix = STRBUF_INIT;
> +	const char *prev_head;
I think we need to initialize prev_head to NULL.
> +	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
> +	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);

If the symref was not pointing into the "refs/remotes/<remote>/" hierarchy previously, skip_prefix() comes back without touching prev_head (i.e. not starting with the prefix does not clear it).

Assuming that we fix the initialization, the rest of the function looks more or less correct.

Show 9 quoted lines
> +	if (prev_head && !strcmp(prev_head, head_name))
> +		printf("'%s/HEAD' is unchanged and points to '%s'\n",
> +			remote, head_name);
> +	else if (prev_head)
> +		printf("'%s/HEAD' has changed from '%s' and now points to '%s'\n",
> +			remote, prev_head, head_name);
> +	else
> +		printf("'%s/HEAD' is now created and points to '%s'\n",
> +			remote, head_name);

The "more or less" part is that the message does not let you tell between refs/remotes/<name>/HEAD that did not exist, and refs/remotes/<name>/HEAD that used to point at somewhere unexpected, outside refs/remotes/<name>/ hierarchy.

For that, we can check if buf_prev->len is 0 (in which case, the "now created and points at" applies) or non-zero (in which case, we say something like "used to point at '%s' (which is unusual), but now points at '%s' as requested").

Junio C Hamano· Oct 10, 2024, 15:57 UTC · re: Junio C Hamano · lore

Re: [PATCH v5 3/6] set-head: better output for --auto

Junio C Hamano <gitster@pobox.com> writes:
Show 15 quoted lines
> Bence Ferdinandy <bence@ferdinandy.com> writes:
>
>> +static void report_auto(const char *remote, const char *head_name,
>> +			struct strbuf *buf_prev) {
>> +	struct strbuf buf_prefix = STRBUF_INIT;
>> +	const char *prev_head;
>
> I think we need to initialize prev_head to NULL.
>
>> +	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
>> +	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
>
> If the symref was not pointing into the "refs/remotes/<remote>/"
> hierarchy previously, skip_prefix() comes back without touching
> prev_head (i.e. not starting with the prefix does not clear it).

The two uninitialized prev_head were also noticed by compilers in multiple jobs at GitHub CI.

https://github.com/git/git/actions/runs/11265515664
Ramsay Jones· Oct 10, 2024, 16:54 UTC · re: Junio C Hamano · lore

Re: [PATCH v5 3/6] set-head: better output for --auto

On 10/10/2024 16:57, Junio C Hamano wrote:
Show 22 quoted lines
> Junio C Hamano <gitster@pobox.com> writes:
> 
>> Bence Ferdinandy <bence@ferdinandy.com> writes:
>>
>>> +static void report_auto(const char *remote, const char *head_name,
>>> +			struct strbuf *buf_prev) {
>>> +	struct strbuf buf_prefix = STRBUF_INIT;
>>> +	const char *prev_head;
>>
>> I think we need to initialize prev_head to NULL.
>>
>>> +	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
>>> +	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
>>
>> If the symref was not pointing into the "refs/remotes/<remote>/"
>> hierarchy previously, skip_prefix() comes back without touching
>> prev_head (i.e. not starting with the prefix does not clear it).
> 
> The two uninitialized prev_head were also noticed by compilers in
> multiple jobs at GitHub CI.
> 
> https://github.com/git/git/actions/runs/11265515664

Heh, I was just about to send a patch to fix up these two warnings (errors with -Werror), so that I could build 'seen'. However, it seems to be well in hand, so I won't bother! :)

ATB, Ramsay Jones

Bence Ferdinandy· Oct 10, 2024, 19:08 UTC · re: Junio C Hamano · lore

Re: [PATCH v5 3/6] set-head: better output for --auto

On Thu Oct 10, 2024 at 17:57, Junio C Hamano <gitster@pobox.com> wrote:
Show 22 quoted lines
> Junio C Hamano <gitster@pobox.com> writes:
>
>> Bence Ferdinandy <bence@ferdinandy.com> writes:
>>
>>> +static void report_auto(const char *remote, const char *head_name,
>>> +			struct strbuf *buf_prev) {
>>> +	struct strbuf buf_prefix = STRBUF_INIT;
>>> +	const char *prev_head;
>>
>> I think we need to initialize prev_head to NULL.
>>
>>> +	strbuf_addf(&buf_prefix, "refs/remotes/%s/", remote);
>>> +	skip_prefix(buf_prev->buf, buf_prefix.buf, &prev_head);
>>
>> If the symref was not pointing into the "refs/remotes/<remote>/"
>> hierarchy previously, skip_prefix() comes back without touching
>> prev_head (i.e. not starting with the prefix does not clear it).
>
> The two uninitialized prev_head were also noticed by compilers in
> multiple jobs at GitHub CI.
>
> https://github.com/git/git/actions/runs/11265515664
I wonder why that didn't fail the build for me locally. I was running
make -j8 install PREFIX=$HOME/.local
for testing. Should I have added a flag or ENV variable somewhere?

Anyhow, the v6 I sent earlier today should fix this (and hopefully all your other comments as well).

Thanks, Bence

-- 
bence.ferdinandy.com
Bence Ferdinandy· Sep 10, 2024, 20:37 UTC · re: Bence Ferdinandy · lore

[RFC PATCH v2 2/2] set-head: do not update if there is no change

Currently, even if there is no actual change to remote/HEAD calling remote set-head will overwrite the appropriate file and if set to --auto will also print a message saying "remote/HEAD set to branch", which implies something was changed. In contrast, on a nil operation e.g. pull will clearly state that nothing was done, while fetch will not output anything.

Change the behaviour of remote set-head so that the reference is only updated if it actually needs to change. Since set-head --auto is essentially a fetch-like operation, align it's behaviour with fetch and only print output if something was actually done.

Signed-off-by: Bence Ferdinandy <bence@ferdinandy.com>
---
 builtin/remote.c | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)
Show changes to builtin/remote.c +6 −4
diff --git a/builtin/remote.c b/builtin/remote.c
index 35c54dd103..e220e51b84 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -1400,8 +1400,8 @@ static int show(int argc, const char **argv, const char *prefix)
 
 static int set_head(int argc, const char **argv, const char *prefix)
 {
-	int i, opt_a = 0, opt_d = 0, result = 0;
-	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT;
+	int i, opt_a = 0, opt_d = 0, is_ref_changed = 0, result = 0;
+	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT, buf3 = STRBUF_INIT;
 	char *head_name = NULL;
 
 	struct option options[] = {
@@ -1440,12 +1440,14 @@ static int set_head(int argc, const char **argv, const char *prefix)
 
 	if (head_name) {
 		strbuf_addf(&buf2, "refs/remotes/%s/%s", argv[0], head_name);
+		refs_read_symbolic_ref(get_main_ref_store(the_repository),buf.buf,&buf3);
+		is_ref_changed = strcmp(buf2.buf,buf3.buf);
 		/* make sure it's valid */
 		if (!refs_ref_exists(get_main_ref_store(the_repository), buf2.buf))
 			result |= error(_("Not a valid ref: %s"), buf2.buf);
-		else if (refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head"))
+		else if (is_ref_changed && refs_update_symref(get_main_ref_store(the_repository), buf.buf, buf2.buf, "remote set-head"))
 			result |= error(_("Could not setup %s"), buf.buf);
-		else if (opt_a)
+		else if (opt_a && is_ref_changed)
 			printf("%s/HEAD set to %s\n", argv[0], head_name);
 		free(head_name);
 	}
-- 
2.46.0
Junio C Hamano· Sep 10, 2024, 22:29 UTC · re: Bence Ferdinandy · lore

Re: [RFC PATCH v2 0/2] set remote/HEAD with fetch

Bence Ferdinandy <bence@ferdinandy.com> writes:
> What is missing for sure is:
> - documentation
> - tests (if needed)
What change does not need tests?
Show 12 quoted lines
> - settings
>
> For settings, my idea would be a fetch/remote.set_head that could take three values:
>     * never
>     * missing: run it only if the ref is missing, this setting would basically
>       allow replicating the result of a clone
>     * always (with the other patch, this would still be a no-op if it didn't change)
>
> This would probably also require a --no-set-head flag, to disable an
> always/missing setting. A --missing-set-head or something of the like also may
> or may not make sense. Alternatively, only two behaviours might be enough
> (missing and always) since clone already sort of does this.

If we were to assume "always" is needed, then the tristate like the above may be a reasonable way to go.

But as I outlined in my response to [1/2], I suspect that an approach without configuration or command line option would give the users the most smooth experience. They are used to seeing "clone" to give them a remote tracking HEAD and nobody complained that we lack the option to "clone" to prevent that. If "fetch" notices we do not have remote tracking HEAD for a remote [*] and stores what it observed at their HEAD in remote tracking HEAD, that should not bother anybody. No matter what mechanism gave you the initial remote tracking HEAD, if you want to update it to something else, we already have the "remote set-head" command.

[Footnote]
 * One thing we MUST be careful about is that some remotes may not
   have ANY remote tracking branches (i.e. you only want to use the
   remote mechanism to give you a shorthand for URL, but you do not
   have fetch refspec at all).  Even if refs/remotes/$repo/HEAD is
   missing for such a remote, we should *not* attempt to create it,
   as we are not populating refs/remotes/$repo/master and friends at
   all in such a case.
Bence Ferdinandy· Sep 11, 2024, 12:24 UTC · re: Junio C Hamano · lore

Re: [RFC PATCH v2 0/2] set remote/HEAD with fetch

On Wed Sep 11, 2024 at 00:29, Junio C Hamano <gitster@pobox.com> wrote:
Show 7 quoted lines
> Bence Ferdinandy <bence@ferdinandy.com> writes:
>
> > What is missing for sure is:
> > - documentation
> > - tests (if needed)
>
> What change does not need tests?
Fair enough, for the next iteration I'll look into tests as well!
Show 27 quoted lines
>
> > - settings
> >
> > For settings, my idea would be a fetch/remote.set_head that could take three values:
> >     * never
> >     * missing: run it only if the ref is missing, this setting would basically
> >       allow replicating the result of a clone
> >     * always (with the other patch, this would still be a no-op if it didn't change)
> >
> > This would probably also require a --no-set-head flag, to disable an
> > always/missing setting. A --missing-set-head or something of the like also may
> > or may not make sense. Alternatively, only two behaviours might be enough
> > (missing and always) since clone already sort of does this.
>
> If we were to assume "always" is needed, then the tristate like the
> above may be a reasonable way to go.
>
> But as I outlined in my response to [1/2], I suspect that an
> approach without configuration or command line option would give the
> users the most smooth experience.  They are used to seeing "clone"
> to give them a remote tracking HEAD and nobody complained that we
> lack the option to "clone" to prevent that.  If "fetch" notices we
> do not have remote tracking HEAD for a remote [*] and stores what it
> observed at their HEAD in remote tracking HEAD, that should not
> bother anybody.  No matter what mechanism gave you the initial
> remote tracking HEAD, if you want to update it to something else, we
> already have the "remote set-head" command.
So I guess we did conclude that no settings are actually needed.
Show 11 quoted lines
>
>
> [Footnote]
>
>  * One thing we MUST be careful about is that some remotes may not
>    have ANY remote tracking branches (i.e. you only want to use the
>    remote mechanism to give you a shorthand for URL, but you do not
>    have fetch refspec at all).  Even if refs/remotes/$repo/HEAD is
>    missing for such a remote, we should *not* attempt to create it,
>    as we are not populating refs/remotes/$repo/master and friends at
>    all in such a case.

You mean that somebody does git init && git remote add origin $remote, but never does calls fetch? Currently remote set-head -a origin will fail with

error: Not a valid ref: refs/remotes/origin/master

Although if everything is done _after_ a fetch, unless I misunderstood, we shouldn't be able to be in this situation at all.

Best, Bence

Junio C Hamano· Sep 11, 2024, 15:59 UTC · re: Bence Ferdinandy · lore

Re: [RFC PATCH v2 0/2] set remote/HEAD with fetch

"Bence Ferdinandy" <bence@ferdinandy.com> writes:
Show 10 quoted lines
> On Wed Sep 11, 2024 at 00:29, Junio C Hamano <gitster@pobox.com> wrote:
>> Bence Ferdinandy <bence@ferdinandy.com> writes:
>>
>> > What is missing for sure is:
>> > - documentation
>> > - tests (if needed)
>>
>> What change does not need tests?
>
> Fair enough, for the next iteration I'll look into tests as well!

In this project, tests are written not because we want to demonstrate the shiny new feature we just invented. We write tests so that the invention we are shipping to our end users will keep behaveing in the way we originall intended without getting broken by future careless developers. The tests serve as watching eyes while the original author of the feature is not watching ;-)

And that is why we also write tests that a feature does not trigger when it should not, as well as it triggers when it should.

Show 10 quoted lines
>>  * One thing we MUST be careful about is that some remotes may not
>>    have ANY remote tracking branches (i.e. you only want to use the
>>    remote mechanism to give you a shorthand for URL, but you do not
>>    have fetch refspec at all).  Even if refs/remotes/$repo/HEAD is
>>    missing for such a remote, we should *not* attempt to create it,
>>    as we are not populating refs/remotes/$repo/master and friends at
>>    all in such a case.
>
> You mean that somebody does git init && git remote add origin $remote, but
> never does calls fetch?

No. If the remote HEAD does not exist, we may still not want to create it. Imagine

	[remote "his"]
		url = https://git.kernel.org/pub/scm/git/git
		push = refs/heads/maint
		push = refs/heads/master
		push = refs/heads/next
		push = +refs/heads/seen

without any refspec for the fetching side. "git fetch his master" may learn where the remote HEAD is, and it may even be pointing at their 'master' branch, but because we do not maintain any remote tracking information for their 'master' (in other words, refs/remotes/his/master is not updated by this 'fetch' and there is no configuration to make future 'fetch' to do so).

Thanks.

← back to recent threads