rebase: skip branch symref aliases

git rebase --update-refs can finish rewriting the current branch and then fail while updating a local branch that is a symbolic ref. This can happen during a default-branch rename where refs/heads/main points at refs/heads/master while users migrate. The problem is a partially applied ref update: the main rebase has already succeeded when the later ref update fails. The sequencer queues updates from local branch decorations. Commit 106b6885c7 (rebase: ignore non-branch update-refs) filters out decorations such as HEAD and tags. A branch symref is still a local branch decoration, but refs_update_ref() dereferences it, so an alias to another branch duplicates the concrete branch update. Resolve local branch decorations before queuing them. Skip symrefs whose targets are under refs/heads/ so that only the concrete branch update is queued. Keep an owned copy of the resolved HEAD and skip the current branch before checked-out handling so later ref resolution cannot change the comparison. This prevents a successful rebase from being followed by a failed, partially applied ref update while preserving each alias as a symref. Signed-off-by: Son Luong Ngoc <sluongng@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Son Luong Ngoc committed Jul 22, 2026 at 08:15 UTC 9c82ba86a0c5e630d5defbe8118612fd5c5e4612
3 files changed +49 -13
sequencer.c
+32 -12
@@ -6445,32 +6445,50 @@ static int add_decorations_to_list(const struct commit *commit,
6445 struct todo_add_branch_context *ctx)
6446 {
6447 const struct name_decoration *decoration = get_name_decoration(&commit->object);
6448 - const char *head_ref = refs_resolve_ref_unsafe(get_main_ref_store(the_repository),
6449 - "HEAD",
6450 - RESOLVE_REF_READING,
6451 - NULL,
6452 - NULL);
6448 + struct ref_store *refs = get_main_ref_store(the_repository);
6449 + char *head_ref = refs_resolve_refdup(refs, "HEAD",
6450 + RESOLVE_REF_READING,
6451 + NULL, NULL);
6452
6453 while (decoration) {
6454 struct todo_item *item;
6455 const char *path;
6456 + char *resolved_ref;
6457 + int flags = 0;
6458 size_t base_offset = ctx->buf->len;
6459
6460 /*
6460 - * If the branch is the current HEAD, then it will be
6461 - * updated by the default rebase behavior.
6462 - * Exclude it from the list of refs to update,
6463 - * as well as any non-branch decorations.
6461 * Non-branch decorations may be present if the pretty format
6462 * includes "%d", which would have loaded all refs
6463 * into the global decoration table.
6464 */
6468 - if ((head_ref && !strcmp(head_ref, decoration->name)) ||
6469 - (decoration->type != DECORATION_REF_LOCAL)) {
6465 + if (decoration->type != DECORATION_REF_LOCAL) {
6466 + decoration = decoration->next;
6467 + continue;
6468 + }
6469 +
6470 + resolved_ref = refs_resolve_refdup(refs, decoration->name,
6471 + RESOLVE_REF_READING,
6472 + NULL, &flags);
6473 + if (resolved_ref && (flags & REF_ISSYMREF) &&
6474 + starts_with(resolved_ref, "refs/heads/")) {
6475 + free(resolved_ref);
6476 + decoration = decoration->next;
6477 + continue;
6478 + }
6479 +
6480 + /*
6481 + * If the branch is the current HEAD, then it will be
6482 + * updated by the default rebase behavior.
6483 + */
6484 + if (head_ref && !strcmp(head_ref, decoration->name)) {
6485 + free(resolved_ref);
6486 decoration = decoration->next;
6487 continue;
6488 }
6489
6490 + path = branch_checked_out(decoration->name);
6491 +
6492 ALLOC_GROW(ctx->items,
6493 ctx->items_nr + 1,
6494 ctx->items_alloc);
@@ -6478,7 +6496,7 @@ static int add_decorations_to_list(const struct commit *commit,
6496 memset(item, 0, sizeof(*item));
6497
6498 /* If the branch is checked out, then leave a comment instead. */
6481 - if ((path = branch_checked_out(decoration->name))) {
6499 + if (path) {
6500 item->command = TODO_COMMENT;
6501 strbuf_commented_addf(ctx->buf, comment_line_str,
6502 "Ref %s checked out at '%s'\n",
@@ -6498,9 +6516,11 @@ static int add_decorations_to_list(const struct commit *commit,
6516 item->arg_len = ctx->buf->len - base_offset;
6517 ctx->items_nr++;
6518
6519 + free(resolved_ref);
6520 decoration = decoration->next;
6521 }
6522
6523 + free(head_ref);
6524 return 0;
6525 }
6526
t/t3400-rebase.sh
+1 -1
@@ -471,7 +471,7 @@ test_expect_success 'git rebase --update-ref with core.commentChar and branch on
471 GIT_SEQUENCE_EDITOR="cat >actual" git -c core.commentChar=% \
472 rebase -i --update-refs base &&
473 test_grep "% Ref refs/heads/wt-topic checked out at" actual &&
474 - test_grep "% Ref refs/heads/topic2 checked out at" actual
474 + test_grep ! "% Ref refs/heads/topic2 checked out at" actual
475 '
476
477 test_done
t/t3404-rebase-interactive.sh
+16
@@ -1975,15 +1975,23 @@ test_expect_success '--update-refs ignores non-branch decorations' '
1975 ) &&
1976 grep ^update-ref todo >actual &&
1977 test_write_lines "update-ref refs/heads/no-conflict-branch" >expect &&
1978 + test_grep ! "^# Ref refs/heads/update-refs checked out" todo &&
1979 test_cmp expect actual
1980 '
1981
1982 test_expect_success '--update-refs updates refs correctly' '
1983 + test_when_finished "
1984 + test_might_fail git symbolic-ref -d refs/heads/no-conflict-branch-alias &&
1985 + test_might_fail git symbolic-ref -d refs/heads/second-alias
1986 + " &&
1987 git checkout -B update-refs no-conflict-branch &&
1988 git branch -f base HEAD~4 &&
1989 git branch -f first HEAD~3 &&
1990 git branch -f second HEAD~3 &&
1991 git branch -f third HEAD~1 &&
1992 + git symbolic-ref refs/heads/no-conflict-branch-alias \
1993 + refs/heads/no-conflict-branch &&
1994 + git symbolic-ref refs/heads/second-alias refs/heads/second &&
1995 test_commit extra2 fileX &&
1996 git commit --amend --fixup=L &&
1997
@@ -1991,8 +1999,16 @@ test_expect_success '--update-refs updates refs correctly' '
1999
2000 test_cmp_rev HEAD~3 refs/heads/first &&
2001 test_cmp_rev HEAD~3 refs/heads/second &&
2002 + test_cmp_rev HEAD~3 refs/heads/second-alias &&
2003 test_cmp_rev HEAD~1 refs/heads/third &&
2004 test_cmp_rev HEAD refs/heads/no-conflict-branch &&
2005 + test_cmp_rev HEAD refs/heads/no-conflict-branch-alias &&
2006 + test_write_lines refs/heads/no-conflict-branch >expect &&
2007 + git symbolic-ref refs/heads/no-conflict-branch-alias >actual &&
2008 + test_cmp expect actual &&
2009 + test_write_lines refs/heads/second >expect &&
2010 + git symbolic-ref refs/heads/second-alias >actual &&
2011 + test_cmp expect actual &&
2012
2013 q_to_tab >expect <<-\EOF &&
2014 Successfully rebased and updated refs/heads/update-refs.