Merge branch 'sn/rebase-update-refs-symrefs' into seen

'git rebase --update-refs' has been taught to resolve local branch
symrefs to their referents before queuing updates, ensuring aliases of
the current branch are skipped and duplicate updates are avoided to
prevent failures when branch aliases are present.

* sn/rebase-update-refs-symrefs:
  rebase: guard non-branch symref targets
  rebase: skip branch symref aliases
This commit is contained in:
Junio C Hamano
2026-08-07 14:48:23 -07:00
4 changed files with 155 additions and 13 deletions

View File

@@ -474,9 +474,24 @@ static void prepare_checked_out_branches(void)
&update_refs)) {
struct string_list_item *item;
for_each_string_list_item(item, &update_refs) {
char *resolved_ref;
int flags = 0;
register_checked_out_branch("", item->string,
wt->path,
BRANCH_CHECKOUT_KIND_UPDATE_REF);
resolved_ref = refs_resolve_refdup(
get_main_ref_store(the_repository),
item->string, RESOLVE_REF_READING,
NULL, &flags);
if (resolved_ref && (flags & REF_ISSYMREF)) {
char *old = strmap_put(
&current_checked_out_branches,
resolved_ref, xstrdup(wt->path));
free(old);
}
free(resolved_ref);
}
string_list_clear(&update_refs, 1);
}

View File

@@ -6549,34 +6549,65 @@ struct todo_add_branch_context {
size_t items_alloc;
struct strbuf *buf;
struct string_list refs_to_oids;
struct string_list symref_update_targets;
};
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;
}
/*
* If the branch is the current HEAD, then it will be
* updated by the default rebase behavior.
*/
if (head_ref && !strcmp(head_ref, decoration->name)) {
free(resolved_ref);
decoration = decoration->next;
continue;
}
path = branch_checked_out(decoration->name);
if (!path && resolved_ref && (flags & REF_ISSYMREF)) {
checked_ref = resolved_ref;
path = branch_checked_out(checked_ref);
}
if (!path && resolved_ref && (flags & REF_ISSYMREF) &&
string_list_has_string(&ctx->symref_update_targets,
resolved_ref)) {
free(resolved_ref);
decoration = decoration->next;
continue;
}
@@ -6588,13 +6619,17 @@ 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",
decoration->name, path);
} else {
struct string_list_item *sti;
if (resolved_ref && (flags & REF_ISSYMREF))
string_list_insert(&ctx->symref_update_targets,
resolved_ref);
item->command = TODO_UPDATE_REF;
strbuf_addf(ctx->buf, "%s\n", decoration->name);
@@ -6608,9 +6643,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;
}
@@ -6624,6 +6661,7 @@ static int todo_list_add_update_ref_commands(struct todo_list *todo_list)
struct todo_add_branch_context ctx = {
.buf = &todo_list->buf,
.refs_to_oids = STRING_LIST_INIT_DUP,
.symref_update_targets = STRING_LIST_INIT_DUP,
};
ctx.items_alloc = 2 * todo_list->nr + 1;
@@ -6649,6 +6687,7 @@ static int todo_list_add_update_ref_commands(struct todo_list *todo_list)
res = write_update_refs_state(&ctx.refs_to_oids);
string_list_clear(&ctx.refs_to_oids, 1);
string_list_clear(&ctx.symref_update_targets, 0);
if (res) {
/* we failed, so clean up the new list. */

View File

@@ -483,7 +483,7 @@ test_expect_success 'git rebase --update-ref with core.commentChar and branch on
GIT_SEQUENCE_EDITOR="cat >actual" git -c core.commentChar=% \
rebase -i --update-refs base &&
test_grep "% Ref refs/heads/wt-topic checked out at" actual &&
test_grep "% Ref refs/heads/topic2 checked out at" actual
test_grep ! "% Ref refs/heads/topic2 checked out at" actual
'
test_done

View File

@@ -1986,15 +1986,23 @@ test_expect_success '--update-refs ignores non-branch decorations' '
) &&
grep ^update-ref todo >actual &&
test_write_lines "update-ref refs/heads/no-conflict-branch" >expect &&
test_grep ! "^# Ref refs/heads/update-refs checked out" todo &&
test_cmp expect actual
'
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 &&
@@ -2002,8 +2010,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.
@@ -2019,6 +2035,78 @@ test_expect_success '--update-refs updates refs correctly' '
test_cmp expect err.trimmed
'
test_expect_success '--update-refs checks resolved non-branch symref target' '
test_when_finished "
git worktree remove --force checked-out-target-wt &&
git symbolic-ref -d refs/heads/non-branch-alias &&
git tag -d checked-out-target
" &&
git tag checked-out-target HEAD~1 &&
git symbolic-ref refs/heads/non-branch-alias refs/tags/checked-out-target &&
git worktree add --detach checked-out-target-wt checked-out-target &&
git -C checked-out-target-wt symbolic-ref HEAD refs/tags/checked-out-target &&
GIT_SEQUENCE_EDITOR="cat >todo" git rebase -i --update-refs HEAD~2 &&
test_grep "^# Ref refs/heads/non-branch-alias checked out at" todo &&
test_write_lines refs/tags/checked-out-target >expect &&
git symbolic-ref refs/heads/non-branch-alias >actual &&
test_cmp expect actual
'
test_expect_success '--update-refs deduplicates non-branch symref targets' '
test_when_finished "
git symbolic-ref -d refs/heads/non-branch-alias-one &&
git symbolic-ref -d refs/heads/non-branch-alias-two &&
git tag -d shared-non-branch-target
" &&
git tag shared-non-branch-target HEAD~1 &&
git symbolic-ref refs/heads/non-branch-alias-one \
refs/tags/shared-non-branch-target &&
git symbolic-ref refs/heads/non-branch-alias-two \
refs/tags/shared-non-branch-target &&
GIT_SEQUENCE_EDITOR=: git rebase -i --force-rebase --update-refs HEAD~2 &&
test_cmp_rev HEAD~1 refs/heads/non-branch-alias-one &&
test_cmp_rev HEAD~1 refs/heads/non-branch-alias-two &&
test_write_lines refs/tags/shared-non-branch-target >expect &&
git symbolic-ref refs/heads/non-branch-alias-one >actual &&
test_cmp expect actual &&
git symbolic-ref refs/heads/non-branch-alias-two >actual &&
test_cmp expect actual
'
test_expect_success '--update-refs honors non-branch symref reservations' '
test_when_finished "
test_might_fail git worktree remove --force reserved-target-wt &&
test_might_fail git symbolic-ref -d \
refs/heads/reserved-non-branch-alias-one &&
test_might_fail git symbolic-ref -d \
refs/heads/reserved-non-branch-alias-two &&
test_might_fail git tag -d reserved-non-branch-target
" &&
git tag reserved-non-branch-target HEAD~1 &&
git symbolic-ref refs/heads/reserved-non-branch-alias-one \
refs/tags/reserved-non-branch-target &&
git symbolic-ref refs/heads/reserved-non-branch-alias-two \
refs/tags/reserved-non-branch-target &&
git worktree add --detach reserved-target-wt HEAD &&
wt_gitdir=$(git -C reserved-target-wt rev-parse --absolute-git-dir) &&
mkdir -p "$wt_gitdir/rebase-merge" &&
old_oid=$(git rev-parse refs/heads/reserved-non-branch-alias-one) &&
test_write_lines refs/heads/reserved-non-branch-alias-one \
"$old_oid" "$old_oid" >"$wt_gitdir/rebase-merge/update-refs" &&
GIT_SEQUENCE_EDITOR="cat >todo" git rebase -i --update-refs HEAD~2 &&
test_grep "^# Ref refs/heads/reserved-non-branch-alias-one checked out at" \
todo &&
test_grep "^# Ref refs/heads/reserved-non-branch-alias-two checked out at" \
todo &&
test_grep ! "^update-ref refs/heads/reserved-non-branch-alias" todo
'
test_expect_success 'respect user edits to update-ref steps' '
git checkout -B update-refs-break no-conflict-branch &&
git branch -f base HEAD~4 &&