rebase: guard non-branch symref targets

A local branch symbolic ref may point outside refs/heads/. Such an alias cannot be skipped like a branch-to-branch alias because its concrete target ref is absent from the local branch decoration list. However, queuing each alias independently can update the same target ref more than once and make the second compare-and-swap fail. A reservation from another worktree can also name either an alias or its resolved target ref, so checking only one form can miss an in-progress update. Fix these cases by checking both the literal alias and its resolved target ref against checked-out reservations. Deduplicate updates by target ref. Also reserve both forms when loading another worktree's update-refs state. This makes different aliases honor the same in-progress update. This keeps non-branch symrefs supported without allowing duplicate or cross-worktree ref updates. 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 056eed4ba7c60dfbb220e2ecc49313c919a22221
3 files changed +106
branch.c
+15
@@ -442,10 +442,25 @@ static void prepare_checked_out_branches(void)
442 &update_refs)) {
443 struct string_list_item *item;
444 for_each_string_list_item(item, &update_refs) {
445 + char *resolved_ref;
446 + int flags = 0;
447 +
448 old = strmap_put(&current_checked_out_branches,
449 item->string,
450 xstrdup(wt->path));
451 free(old);
452 +
453 + resolved_ref = refs_resolve_refdup(
454 + get_main_ref_store(the_repository),
455 + item->string, RESOLVE_REF_READING,
456 + NULL, &flags);
457 + if (resolved_ref && (flags & REF_ISSYMREF)) {
458 + old = strmap_put(
459 + &current_checked_out_branches,
460 + resolved_ref, xstrdup(wt->path));
461 + free(old);
462 + }
463 + free(resolved_ref);
464 }
465 string_list_clear(&update_refs, 1);
466 }
sequencer.c
+19
@@ -6439,6 +6439,7 @@ struct todo_add_branch_context {
6439 size_t items_alloc;
6440 struct strbuf *buf;
6441 struct string_list refs_to_oids;
6442 + struct string_list symref_update_targets;
6443 };
6444
6445 static int add_decorations_to_list(const struct commit *commit,
@@ -6453,6 +6454,7 @@ static int add_decorations_to_list(const struct commit *commit,
6454 while (decoration) {
6455 struct todo_item *item;
6456 const char *path;
6457 + const char *checked_ref;
6458 char *resolved_ref;
6459 int flags = 0;
6460 size_t base_offset = ctx->buf->len;
@@ -6488,6 +6490,17 @@ static int add_decorations_to_list(const struct commit *commit,
6490 }
6491
6492 path = branch_checked_out(decoration->name);
6493 + if (!path && resolved_ref && (flags & REF_ISSYMREF)) {
6494 + checked_ref = resolved_ref;
6495 + path = branch_checked_out(checked_ref);
6496 + }
6497 + if (!path && resolved_ref && (flags & REF_ISSYMREF) &&
6498 + string_list_has_string(&ctx->symref_update_targets,
6499 + resolved_ref)) {
6500 + free(resolved_ref);
6501 + decoration = decoration->next;
6502 + continue;
6503 + }
6504
6505 ALLOC_GROW(ctx->items,
6506 ctx->items_nr + 1,
@@ -6503,6 +6516,10 @@ static int add_decorations_to_list(const struct commit *commit,
6516 decoration->name, path);
6517 } else {
6518 struct string_list_item *sti;
6519 +
6520 + if (resolved_ref && (flags & REF_ISSYMREF))
6521 + string_list_insert(&ctx->symref_update_targets,
6522 + resolved_ref);
6523 item->command = TODO_UPDATE_REF;
6524 strbuf_addf(ctx->buf, "%s\n", decoration->name);
6525
@@ -6534,6 +6551,7 @@ static int todo_list_add_update_ref_commands(struct todo_list *todo_list)
6551 struct todo_add_branch_context ctx = {
6552 .buf = &todo_list->buf,
6553 .refs_to_oids = STRING_LIST_INIT_DUP,
6554 + .symref_update_targets = STRING_LIST_INIT_DUP,
6555 };
6556
6557 ctx.items_alloc = 2 * todo_list->nr + 1;
@@ -6559,6 +6577,7 @@ static int todo_list_add_update_ref_commands(struct todo_list *todo_list)
6577 res = write_update_refs_state(&ctx.refs_to_oids);
6578
6579 string_list_clear(&ctx.refs_to_oids, 1);
6580 + string_list_clear(&ctx.symref_update_targets, 0);
6581
6582 if (res) {
6583 /* we failed, so clean up the new list. */
t/t3404-rebase-interactive.sh
+72
@@ -2024,6 +2024,78 @@ test_expect_success '--update-refs updates refs correctly' '
2024 test_cmp expect err.trimmed
2025 '
2026
2027 +test_expect_success '--update-refs checks resolved non-branch symref target' '
2028 + test_when_finished "
2029 + git worktree remove --force checked-out-target-wt &&
2030 + git symbolic-ref -d refs/heads/non-branch-alias &&
2031 + git tag -d checked-out-target
2032 + " &&
2033 + git tag checked-out-target HEAD~1 &&
2034 + git symbolic-ref refs/heads/non-branch-alias refs/tags/checked-out-target &&
2035 + git worktree add --detach checked-out-target-wt checked-out-target &&
2036 + git -C checked-out-target-wt symbolic-ref HEAD refs/tags/checked-out-target &&
2037 +
2038 + GIT_SEQUENCE_EDITOR="cat >todo" git rebase -i --update-refs HEAD~2 &&
2039 +
2040 + test_grep "^# Ref refs/heads/non-branch-alias checked out at" todo &&
2041 + test_write_lines refs/tags/checked-out-target >expect &&
2042 + git symbolic-ref refs/heads/non-branch-alias >actual &&
2043 + test_cmp expect actual
2044 +'
2045 +
2046 +test_expect_success '--update-refs deduplicates non-branch symref targets' '
2047 + test_when_finished "
2048 + git symbolic-ref -d refs/heads/non-branch-alias-one &&
2049 + git symbolic-ref -d refs/heads/non-branch-alias-two &&
2050 + git tag -d shared-non-branch-target
2051 + " &&
2052 + git tag shared-non-branch-target HEAD~1 &&
2053 + git symbolic-ref refs/heads/non-branch-alias-one \
2054 + refs/tags/shared-non-branch-target &&
2055 + git symbolic-ref refs/heads/non-branch-alias-two \
2056 + refs/tags/shared-non-branch-target &&
2057 +
2058 + GIT_SEQUENCE_EDITOR=: git rebase -i --force-rebase --update-refs HEAD~2 &&
2059 +
2060 + test_cmp_rev HEAD~1 refs/heads/non-branch-alias-one &&
2061 + test_cmp_rev HEAD~1 refs/heads/non-branch-alias-two &&
2062 + test_write_lines refs/tags/shared-non-branch-target >expect &&
2063 + git symbolic-ref refs/heads/non-branch-alias-one >actual &&
2064 + test_cmp expect actual &&
2065 + git symbolic-ref refs/heads/non-branch-alias-two >actual &&
2066 + test_cmp expect actual
2067 +'
2068 +
2069 +test_expect_success '--update-refs honors non-branch symref reservations' '
2070 + test_when_finished "
2071 + test_might_fail git worktree remove --force reserved-target-wt &&
2072 + test_might_fail git symbolic-ref -d \
2073 + refs/heads/reserved-non-branch-alias-one &&
2074 + test_might_fail git symbolic-ref -d \
2075 + refs/heads/reserved-non-branch-alias-two &&
2076 + test_might_fail git tag -d reserved-non-branch-target
2077 + " &&
2078 + git tag reserved-non-branch-target HEAD~1 &&
2079 + git symbolic-ref refs/heads/reserved-non-branch-alias-one \
2080 + refs/tags/reserved-non-branch-target &&
2081 + git symbolic-ref refs/heads/reserved-non-branch-alias-two \
2082 + refs/tags/reserved-non-branch-target &&
2083 + git worktree add --detach reserved-target-wt HEAD &&
2084 + wt_gitdir=$(git -C reserved-target-wt rev-parse --absolute-git-dir) &&
2085 + mkdir -p "$wt_gitdir/rebase-merge" &&
2086 + old_oid=$(git rev-parse refs/heads/reserved-non-branch-alias-one) &&
2087 + test_write_lines refs/heads/reserved-non-branch-alias-one \
2088 + "$old_oid" "$old_oid" >"$wt_gitdir/rebase-merge/update-refs" &&
2089 +
2090 + GIT_SEQUENCE_EDITOR="cat >todo" git rebase -i --update-refs HEAD~2 &&
2091 +
2092 + test_grep "^# Ref refs/heads/reserved-non-branch-alias-one checked out at" \
2093 + todo &&
2094 + test_grep "^# Ref refs/heads/reserved-non-branch-alias-two checked out at" \
2095 + todo &&
2096 + test_grep ! "^update-ref refs/heads/reserved-non-branch-alias" todo
2097 +'
2098 +
2099 test_expect_success 'respect user edits to update-ref steps' '
2100 git checkout -B update-refs-break no-conflict-branch &&
2101 git branch -f base HEAD~4 &&