From b59eb4e247aa04b35f1f14ca41e8e69cc4b17944 Mon Sep 17 00:00:00 2001 From: Son Luong Ngoc Date: Fri, 29 May 2026 15:40:31 +0200 Subject: [PATCH] rebase: skip branch symref aliases git rebase --update-refs can fail after the normal rebase path has updated the current branch when another local branch is a symref to it. This can happen during a default-branch rename where refs/heads/main points at refs/heads/master while users migrate. The sequencer queues update-ref commands from local branch decorations. Commit 106b6885c7 (rebase: ignore non-branch update-refs) filters out decorations that are not local branches, such as HEAD and tags. A branch symref is different: it is still a local branch decoration under refs/heads/, but it is an alias rather than a concrete branch to update. Queuing the alias by its literal name makes the final update dereference it back to the current branch and fail the old-OID check because that branch moved earlier in the rebase. Resolve local branch symrefs to their referents before queuing update-ref commands. Skip aliases of HEAD and duplicate referents, leaving one old-OID-checked update for each concrete branch. Signed-off-by: Son Luong Ngoc --- sequencer.c | 63 +++++++++++++++++++++++++++++------ t/t3404-rebase-interactive.sh | 25 ++++++++++++++ 2 files changed, 77 insertions(+), 11 deletions(-) diff --git a/sequencer.c b/sequencer.c index 1ee4b2875b25a0..4a83d1337c6aa8 100644 --- a/sequencer.c +++ b/sequencer.c @@ -6445,15 +6445,22 @@ static int add_decorations_to_list(const struct commit *commit, struct todo_add_branch_context *ctx) { const struct name_decoration *decoration = get_name_decoration(&commit->object); - const char *head_ref = refs_resolve_ref_unsafe(get_main_ref_store(the_repository), - "HEAD", + struct ref_store *refs = get_main_ref_store(the_repository); + const char *head_ref = refs_resolve_ref_unsafe(refs, "HEAD", RESOLVE_REF_READING, - NULL, - NULL); + NULL, NULL); + char *resolved_head_ref = refs_resolve_refdup(refs, "HEAD", + RESOLVE_REF_READING, + NULL, NULL); + struct strbuf update_ref = STRBUF_INIT; while (decoration) { struct todo_item *item; const char *path; + const char *ref = decoration->name; + const char *resolved_ref; + int is_symref = 0; + int flags = 0; size_t base_offset = ctx->buf->len; /* @@ -6461,12 +6468,44 @@ static int add_decorations_to_list(const struct commit *commit, * updated by the default rebase behavior. * Exclude it from the list of refs to update, * as well as any non-branch decorations. + * + * Resolve branch symrefs after checking for the current HEAD so + * that aliases do not schedule duplicate updates for their + * referents. + * * Non-branch decorations may be present if the pretty format * includes "%d", which would have loaded all refs * into the global decoration table. */ - if ((head_ref && !strcmp(head_ref, decoration->name)) || - (decoration->type != DECORATION_REF_LOCAL)) { + if (decoration->type != DECORATION_REF_LOCAL) { + decoration = decoration->next; + continue; + } + + if (head_ref && !strcmp(head_ref, ref)) { + decoration = decoration->next; + continue; + } + + strbuf_reset(&update_ref); + resolved_ref = refs_resolve_ref_unsafe(refs, ref, + RESOLVE_REF_READING | + RESOLVE_REF_NO_RECURSE, + NULL, &flags); + if ((flags & REF_ISSYMREF) && resolved_ref) { + if (!starts_with(resolved_ref, "refs/heads/")) { + decoration = decoration->next; + continue; + } + + strbuf_addstr(&update_ref, resolved_ref); + ref = update_ref.buf; + is_symref = 1; + } + + if ((is_symref && resolved_head_ref && + !strcmp(resolved_head_ref, ref)) || + string_list_has_string(&ctx->refs_to_oids, ref)) { decoration = decoration->next; continue; } @@ -6478,19 +6517,19 @@ static int add_decorations_to_list(const struct commit *commit, memset(item, 0, sizeof(*item)); /* If the branch is checked out, then leave a comment instead. */ - if ((path = branch_checked_out(decoration->name))) { + if ((path = branch_checked_out(ref))) { item->command = TODO_COMMENT; strbuf_commented_addf(ctx->buf, comment_line_str, "Ref %s checked out at '%s'\n", - decoration->name, path); + ref, path); } else { struct string_list_item *sti; item->command = TODO_UPDATE_REF; - strbuf_addf(ctx->buf, "%s\n", decoration->name); + strbuf_addf(ctx->buf, "%s\n", ref); sti = string_list_insert(&ctx->refs_to_oids, - decoration->name); - sti->util = init_update_ref_record(decoration->name); + ref); + sti->util = init_update_ref_record(ref); } item->offset_in_buf = base_offset; @@ -6501,6 +6540,8 @@ static int add_decorations_to_list(const struct commit *commit, decoration = decoration->next; } + strbuf_release(&update_ref); + free(resolved_head_ref); return 0; } diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index 58b3bb0c271aae..29447c0fc307ac 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -1978,6 +1978,31 @@ test_expect_success '--update-refs ignores non-branch decorations' ' test_cmp expect actual ' +test_expect_success '--update-refs skips branch symrefs to current branch' ' + test_when_finished " + test_might_fail git rebase --abort && + git checkout primary && + test_might_fail git symbolic-ref -d refs/heads/update-refs-symref-alias && + test_might_fail git branch -D update-refs-symref update-refs-symref-base + " && + git checkout -B update-refs-symref-base primary && + test_commit --no-tag update-refs-symref-base symref-base.t && + git checkout -B update-refs-symref && + test_commit --no-tag update-refs-symref-topic symref-topic.t && + git checkout update-refs-symref-base && + test_commit --no-tag update-refs-symref-newbase symref-newbase.t && + git checkout update-refs-symref && + git symbolic-ref refs/heads/update-refs-symref-alias refs/heads/update-refs-symref && + + git rebase --update-refs update-refs-symref-base 2>err && + + test_cmp_rev update-refs-symref-base update-refs-symref^ && + test_cmp_rev refs/heads/update-refs-symref refs/heads/update-refs-symref-alias && + test_write_lines refs/heads/update-refs-symref >expect && + git symbolic-ref refs/heads/update-refs-symref-alias >actual && + test_cmp expect actual +' + test_expect_success '--update-refs updates refs correctly' ' git checkout -B update-refs no-conflict-branch && git branch -f base HEAD~4 &&