Subject: Re: `git remote rename` does not work when `refs/remotes/server/HEAD` is unborn (when right after `git remote add -m`)

From: Patrick Steinhardt <[email-masked]>
Date: Thu, 24 Jul 2025 15:03:27 +0200

On Thu, Jul 24, 2025 at 01:58:37PM +0200, Patrick Steinhardt wrote:
> On Thu, Jul 24, 2025 at 06:45:36AM -0400, Jeff King wrote:
> > On Thu, Jul 24, 2025 at 09:59:45PM +1200, Han Jiang wrote:
> > 
> > > What did you expect to happen? (Expected behavior)
> > > 
> > > `git symbolic-ref 'refs/remotes/server/HEAD'` outputs
> > > "refs/remotes/server/master";
> > > `git symbolic-ref 'refs/remotes/server2/HEAD'` outputs
> > > "refs/remotes/server2/master".
> > > 
> > > What happened instead? (Actual behavior)
> > > 
> > > `git symbolic-ref 'refs/remotes/server/HEAD'` outputs
> > > "refs/remotes/server/master";
> > > `git symbolic-ref 'refs/remotes/server2/HEAD'` outputs "fatal: ref
> > > refs/remotes/server2/HEAD is not a symbolic ref".
> > > `git symbolic-ref 'refs/remotes/server/HEAD'` outputs
> > > "refs/remotes/server/master".
> > 
> > Thanks for the report. I can reproduce the issue easily here. Probably a
> > simpler reproduction is just:
> > 
> >   git init
> >   git remote add -m whatever server1 /does/not/need/to/exist
> >   git remote rename server1 server2
> >   git symbolic-ref refs/remotes/server2/HEAD
> > 
> > The problem is that the branch-renaming code in git-remote is not
> > prepared to handle symrefs that don't resolve. This seems to make it
> > work:
> > 
> > diff --git a/builtin/remote.c b/builtin/remote.c
> > index 5dd6cbbaee..478ea3a80c 100644
> > --- a/builtin/remote.c
> > +++ b/builtin/remote.c
> > @@ -630,7 +630,9 @@ static int read_remote_branches(const char *refname, const char *referent UNUSED
> >  	if (starts_with(refname, buf.buf)) {
> >  		item = string_list_append(rename->remote_branches, refname);
> >  		symref = refs_resolve_ref_unsafe(get_main_ref_store(the_repository),
> > -						 refname, RESOLVE_REF_READING,
> > +						 refname,
> > +						 RESOLVE_REF_READING |
> > +						 RESOLVE_REF_NO_RECURSE,
> >  						 NULL, &flag);
> >  		if (symref && (flag & REF_ISSYMREF)) {
> >  			item->util = xstrdup(symref);
> > @@ -835,8 +837,8 @@ static int mv(int argc, const char **argv, const char *prefix,
> >  	 * First remove symrefs, then rename the rest, finally create
> >  	 * the new symrefs.
> >  	 */
> > -	refs_for_each_ref(get_main_ref_store(the_repository),
> > -			  read_remote_branches, &rename);
> > +	refs_for_each_rawref(get_main_ref_store(the_repository),
> > +			     read_remote_branches, &rename);
> >  	if (show_progress) {
> >  		/*
> >  		 * Count symrefs twice, since "renaming" them is done by
> > 
> > That is, we need two fixes:
> > 
> >   1. When iterating over the refs, we need to cover _all_ refs, not just
> >      those that fully resolve (there's a related bug here: we'll
> >      silently ignore an actual broken or corrupt ref, whereas I think
> >      the right thing would probably be to try copying it and then
> >      complain loudly if we don't have the object).
> > 
> >   2. When resolving each one, we shouldn't recurse. We're doing a
> >      shallow copy, not a deep one.
> > 
> > Reading this code, though, I can't help but think that the recent "git
> > refs migrate" command had to deal with all of these problems. I wonder
> > if we could reuse its code. +cc pks for wisdom.
> 
> I'm not sure whether we can easily reuse the code -- the use case is
> quite different, as the migration works across two totally independent
> refdbs. So all refs are recreated 1:1, without any renaming involved.
> 
> But it certainly seems to me like this whole logic could use quite some
> love:
> 
>   - We create N+M*2 separate ref transactions, where N is the number of
>     direct remote refs we need to migrate and M is the number of
>     symbolic refs. This is bad with the "reftable" backend, but given
>     that the N transactions are all renames that have to delete the old
>     ref it's even quadratic in the worst case for the "files" backend
>     because we may have to rewrite the packed-refs file for each such
>     transaction.
> 
>   - It is way too brittle, as the update isn't even pretending to be
>     atomic. We first delete everything, and then we recreate it. So if
>     any of these updates fails we'll be left in an in-between state.
> 
>   - We shouldn't have to even call `refs_resolve_ref_unsafe()` at all,
>     as the `read_remote_branches()` nowadays gets the referent as
>     parameter.
> 
> To demonstrate:
> 
>      $ git init --ref-format=files repo
>     Initialized empty Git repository in /tmp/repo/.git/
>      $ cd repo/
>      /tmp/repo:HEAD $ git commit --allow-empty -m initial
>     [main (root-commit) 00c2622] x
>      $ git remote add origin /dev/null
>      /tmp/repo:main $ for i in $(seq 100000); do printf "create refs/remotes/origin/branch-%d HEAD\n" $i; done | git update-ref --stdin
>      /tmp/repo:main $ git pack-refs --all
>      /tmp/repo:main $ time git remote rename origin renamed
>     Renaming remote references:   0% (2216/100000)
> 
> I stopped after a minute -- this will take hours to complete.
> 
> So I think we should adapt this logic to use a single transaction.
> There's one catch, as refs_rename_ref()` also migrates any reflogs that
> exist. But with the recent infra that Karthik has added we can now also
> migrate reflogs, so that's all doable.
> 
> Patrick

I've quickly hacked something together now, see the work-in-progress
patch below. The patch does not yet handle reflogs, but that isn't too
hard to implement.

And these changes indeed speed up things by quite a lot: instead of
hours it now takes 7 seconds :) I'll polish this patch series and will
likely send it in tomorrow.

Patrick

diff --git a/builtin/remote.c b/builtin/remote.c
index 5dd6cbbaeed..072a70e6b45 100644
--- a/builtin/remote.c
+++ b/builtin/remote.c
@@ -612,36 +612,53 @@ static int add_branch_for_removal(const char *refname,
 struct rename_info {
 	const char *old_name;
 	const char *new_name;
-	struct string_list *remote_branches;
 	uint32_t symrefs_nr;
+	struct ref_transaction *transaction;
+	struct strbuf *err;
 };
 
-static int read_remote_branches(const char *refname, const char *referent UNUSED,
-				const struct object_id *oid UNUSED,
-				int flags UNUSED, void *cb_data)
+static int queue_one_rename(const char *refname, const char *referent,
+			    const struct object_id *oid,
+			    int flags, void *cb_data)
 {
+	struct strbuf new_refname = STRBUF_INIT, new_referent = STRBUF_INIT;
 	struct rename_info *rename = cb_data;
-	struct strbuf buf = STRBUF_INIT;
-	struct string_list_item *item;
-	int flag;
-	const char *symref;
-
-	strbuf_addf(&buf, "refs/remotes/%s/", rename->old_name);
-	if (starts_with(refname, buf.buf)) {
-		item = string_list_append(rename->remote_branches, refname);
-		symref = refs_resolve_ref_unsafe(get_main_ref_store(the_repository),
-						 refname, RESOLVE_REF_READING,
-						 NULL, &flag);
-		if (symref && (flag & REF_ISSYMREF)) {
-			item->util = xstrdup(symref);
-			rename->symrefs_nr++;
-		} else {
-			item->util = NULL;
-		}
+	int error;
+
+	strbuf_addf(&new_refname, "refs/remotes/%s/", rename->old_name);
+	if (!starts_with(refname, new_refname.buf)) {
+		error = 0;
+		goto out;
 	}
-	strbuf_release(&buf);
 
-	return 0;
+	if (flags & REF_ISSYMREF) {
+		strbuf_addstr(&new_referent, referent);
+		strbuf_splice(&new_referent, strlen("refs/remotes/"), strlen(rename->old_name),
+			      rename->new_name, strlen(rename->new_name));
+		oid = NULL;
+	}
+
+	error = ref_transaction_delete(rename->transaction, refname,
+				       oid, referent, REF_NO_DEREF, NULL, rename->err);
+	if (error < 0)
+		goto out;
+
+	strbuf_reset(&new_refname);
+	strbuf_addstr(&new_refname, refname);
+	strbuf_splice(&new_refname, strlen("refs/remotes/"), strlen(rename->old_name),
+		      rename->new_name, strlen(rename->new_name));
+
+	error = ref_transaction_update(rename->transaction, new_refname.buf, oid,
+				       null_oid(the_hash_algo), (flags & REF_ISSYMREF) ? new_referent.buf : NULL, NULL,
+				       REF_SKIP_CREATE_REFLOG | REF_NO_DEREF | REF_SKIP_OID_VERIFICATION,
+				       NULL, rename->err);
+	if (error < 0)
+		goto out;
+
+out:
+	strbuf_release(&new_refname);
+	strbuf_release(&new_referent);
+	return error;
 }
 
 static int migrate_file(struct remote *remote)
@@ -740,12 +757,10 @@ static int mv(int argc, const char **argv, const char *prefix,
 		OPT_END()
 	};
 	struct remote *oldremote, *newremote;
-	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT, buf3 = STRBUF_INIT,
-		old_remote_context = STRBUF_INIT;
-	struct string_list remote_branches = STRING_LIST_INIT_DUP;
+	struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT,
+		old_remote_context = STRBUF_INIT, err = STRBUF_INIT;
 	struct rename_info rename;
-	int i, refs_renamed_nr = 0, refspec_updated = 0;
-	struct progress *progress = NULL;
+	int i, refspec_updated = 0;
 	int result = 0;
 
 	argc = parse_options(argc, argv, prefix, options,
@@ -756,8 +771,8 @@ static int mv(int argc, const char **argv, const char *prefix,
 
 	rename.old_name = argv[0];
 	rename.new_name = argv[1];
-	rename.remote_branches = &remote_branches;
 	rename.symrefs_nr = 0;
+	rename.err = &err;
 
 	oldremote = remote_get(rename.old_name);
 	if (!remote_is_configured(oldremote, 1)) {
@@ -831,80 +846,28 @@ static int mv(int argc, const char **argv, const char *prefix,
 	if (!refspec_updated)
 		goto out;
 
-	/*
-	 * First remove symrefs, then rename the rest, finally create
-	 * the new symrefs.
-	 */
-	refs_for_each_ref(get_main_ref_store(the_repository),
-			  read_remote_branches, &rename);
-	if (show_progress) {
-		/*
-		 * Count symrefs twice, since "renaming" them is done by
-		 * deleting and recreating them in two separate passes.
-		 */
-		progress = start_progress(the_repository,
-					  _("Renaming remote references"),
-					  rename.remote_branches->nr + rename.symrefs_nr);
-	}
-	for (i = 0; i < remote_branches.nr; i++) {
-		struct string_list_item *item = remote_branches.items + i;
-		struct strbuf referent = STRBUF_INIT;
-
-		if (refs_read_symbolic_ref(get_main_ref_store(the_repository), item->string,
-					   &referent))
-			continue;
-		if (refs_delete_ref(get_main_ref_store(the_repository), NULL, item->string, NULL, REF_NO_DEREF))
-			die(_("deleting '%s' failed"), item->string);
-
-		strbuf_release(&referent);
-		display_progress(progress, ++refs_renamed_nr);
-	}
-	for (i = 0; i < remote_branches.nr; i++) {
-		struct string_list_item *item = remote_branches.items + i;
+	rename.transaction = ref_store_transaction_begin(get_main_ref_store(the_repository),
+							 0, &err);
+	if (!rename.transaction)
+		goto out;
 
-		if (item->util)
-			continue;
-		strbuf_reset(&buf);
-		strbuf_addstr(&buf, item->string);
-		strbuf_splice(&buf, strlen("refs/remotes/"), strlen(rename.old_name),
-				rename.new_name, strlen(rename.new_name));
-		strbuf_reset(&buf2);
-		strbuf_addf(&buf2, "remote: renamed %s to %s",
-				item->string, buf.buf);
-		if (refs_rename_ref(get_main_ref_store(the_repository), item->string, buf.buf, buf2.buf))
-			die(_("renaming '%s' failed"), item->string);
-		display_progress(progress, ++refs_renamed_nr);
-	}
-	for (i = 0; i < remote_branches.nr; i++) {
-		struct string_list_item *item = remote_branches.items + i;
+	result = refs_for_each_rawref(get_main_ref_store(the_repository),
+				      queue_one_rename, &rename);
+	if (result < 0)
+		die(_("renaming references failed: %s"), rename.err->buf);
 
-		if (!item->util)
-			continue;
-		strbuf_reset(&buf);
-		strbuf_addstr(&buf, item->string);
-		strbuf_splice(&buf, strlen("refs/remotes/"), strlen(rename.old_name),
-				rename.new_name, strlen(rename.new_name));
-		strbuf_reset(&buf2);
-		strbuf_addstr(&buf2, item->util);
-		strbuf_splice(&buf2, strlen("refs/remotes/"), strlen(rename.old_name),
-				rename.new_name, strlen(rename.new_name));
-		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))
-			die(_("creating '%s' failed"), buf.buf);
-		display_progress(progress, ++refs_renamed_nr);
-	}
-	stop_progress(&progress);
+	result = ref_transaction_commit(rename.transaction, &err);
+	if (result < 0)
+		die(_("committing renamed references failed: %s"), rename.err->buf);
 
 	handle_push_default(rename.old_name, rename.new_name);
 
 out:
-	string_list_clear(&remote_branches, 1);
+	ref_transaction_free(rename.transaction);
 	strbuf_release(&old_remote_context);
 	strbuf_release(&buf);
 	strbuf_release(&buf2);
-	strbuf_release(&buf3);
+	strbuf_release(&err);
 	return result;
 }