From ebc4d49bc92c370c32381195b90808f902e5f555 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, but if it resolves to another branch then that target branch is itself present in the decoration list and will be updated as a concrete branch. Skip branch decorations whose symrefs resolve to refs/heads/*, because those targets are already represented by concrete branch decorations. This prevents aliases from scheduling a second update for the same branch. Resolve each local branch decoration before checking whether it is already checked out. Symrefs to other branches are skipped directly. Other symrefs stay on the existing path, but checked-out protection is applied to their resolved target rather than to the symref name. Use a copy of the resolved HEAD ref so later ref resolution does not overwrite it. Signed-off-by: Son Luong Ngoc --- sequencer.c | 49 ++++++++++++++++++++++++++--------- t/t3404-rebase-interactive.sh | 15 +++++++++++ 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/sequencer.c b/sequencer.c index 1ee4b2875b25a0..fdd1b40f422e3e 100644 --- a/sequencer.c +++ b/sequencer.c @@ -6445,28 +6445,51 @@ 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", - RESOLVE_REF_READING, - NULL, - NULL); + struct ref_store *refs = get_main_ref_store(the_repository); + char *head_ref = refs_resolve_refdup(refs, "HEAD", + RESOLVE_REF_READING, + NULL, NULL); while (decoration) { struct todo_item *item; const char *path; + const char *checked_ref; + char *resolved_ref; + int flags = 0; size_t base_offset = ctx->buf->len; /* - * If the branch is the current HEAD, then it will be - * updated by the default rebase behavior. - * Exclude it from the list of refs to update, - * as well as any non-branch decorations. * 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; + } + + resolved_ref = refs_resolve_refdup(refs, decoration->name, + RESOLVE_REF_READING, + NULL, &flags); + if (resolved_ref && (flags & REF_ISSYMREF) && + starts_with(resolved_ref, "refs/heads/")) { + free(resolved_ref); + decoration = decoration->next; + continue; + } + + checked_ref = (resolved_ref && (flags & REF_ISSYMREF)) ? + resolved_ref : decoration->name; + path = branch_checked_out(checked_ref); + + /* + * If the branch is the current HEAD, then it will be + * updated by the default rebase behavior. Exclude it from + * the list of refs to update, unless it is checked out and + * needs a comment in the todo list. + */ + if (!path && head_ref && !strcmp(head_ref, decoration->name)) { + free(resolved_ref); decoration = decoration->next; continue; } @@ -6478,7 +6501,7 @@ 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) { item->command = TODO_COMMENT; strbuf_commented_addf(ctx->buf, comment_line_str, "Ref %s checked out at '%s'\n", @@ -6498,9 +6521,11 @@ static int add_decorations_to_list(const struct commit *commit, item->arg_len = ctx->buf->len - base_offset; ctx->items_nr++; + free(resolved_ref); decoration = decoration->next; } + free(head_ref); return 0; } diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index 58b3bb0c271aae..bc0b6a31f75085 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -1979,11 +1979,18 @@ test_expect_success '--update-refs ignores non-branch decorations' ' ' test_expect_success '--update-refs updates refs correctly' ' + test_when_finished " + test_might_fail git symbolic-ref -d refs/heads/no-conflict-branch-alias && + test_might_fail git symbolic-ref -d refs/heads/second-alias + " && git checkout -B update-refs no-conflict-branch && git branch -f base HEAD~4 && git branch -f first HEAD~3 && git branch -f second HEAD~3 && git branch -f third HEAD~1 && + git symbolic-ref refs/heads/no-conflict-branch-alias \ + refs/heads/no-conflict-branch && + git symbolic-ref refs/heads/second-alias refs/heads/second && test_commit extra2 fileX && git commit --amend --fixup=L && @@ -1991,8 +1998,16 @@ test_expect_success '--update-refs updates refs correctly' ' test_cmp_rev HEAD~3 refs/heads/first && test_cmp_rev HEAD~3 refs/heads/second && + test_cmp_rev HEAD~3 refs/heads/second-alias && test_cmp_rev HEAD~1 refs/heads/third && test_cmp_rev HEAD refs/heads/no-conflict-branch && + test_cmp_rev HEAD refs/heads/no-conflict-branch-alias && + test_write_lines refs/heads/no-conflict-branch >expect && + git symbolic-ref refs/heads/no-conflict-branch-alias >actual && + test_cmp expect actual && + test_write_lines refs/heads/second >expect && + git symbolic-ref refs/heads/second-alias >actual && + test_cmp expect actual && q_to_tab >expect <<-\EOF && Successfully rebased and updated refs/heads/update-refs.