diff --git a/branch.c b/branch.c index 243db7d0fc0226..ccb812d87bbb9c 100644 --- a/branch.c +++ b/branch.c @@ -385,6 +385,35 @@ int validate_branchname(const char *name, struct strbuf *ref) static int initialized_checked_out_branches; static struct strmap current_checked_out_branches = STRMAP_INIT; +static void add_checked_out_branch(const char *branch, const char *path) +{ + struct strbuf ref = STRBUF_INIT; + const char *refname = branch; + char *old, *resolved_ref; + int flags = 0; + + if (!starts_with(refname, "refs/")) { + strbuf_addf(&ref, "refs/heads/%s", refname); + refname = ref.buf; + } + + old = strmap_put(¤t_checked_out_branches, + refname, xstrdup(path)); + free(old); + + resolved_ref = refs_resolve_refdup( + get_main_ref_store(the_repository), + refname, RESOLVE_REF_READING, NULL, &flags); + if (resolved_ref && (flags & REF_ISSYMREF)) { + old = strmap_put(¤t_checked_out_branches, + resolved_ref, xstrdup(path)); + free(old); + } + + free(resolved_ref); + strbuf_release(&ref); +} + static void prepare_checked_out_branches(void) { int i = 0; @@ -397,7 +426,7 @@ static void prepare_checked_out_branches(void) worktrees = get_worktrees(); while (worktrees[i]) { - char *old, *wt_gitdir; + char *wt_gitdir; struct wt_status_state state = { 0 }; struct worktree *wt = worktrees[i++]; struct string_list update_refs = STRING_LIST_INIT_DUP; @@ -405,48 +434,26 @@ static void prepare_checked_out_branches(void) if (wt->is_bare) continue; - if (wt->head_ref) { - old = strmap_put(¤t_checked_out_branches, - wt->head_ref, - xstrdup(wt->path)); - free(old); - } + if (wt->head_ref) + add_checked_out_branch(wt->head_ref, wt->path); if (wt_status_check_rebase(wt, &state) && (state.rebase_in_progress || state.rebase_interactive_in_progress) && - state.branch) { - struct strbuf ref = STRBUF_INIT; - strbuf_addf(&ref, "refs/heads/%s", state.branch); - old = strmap_put(¤t_checked_out_branches, - ref.buf, - xstrdup(wt->path)); - free(old); - strbuf_release(&ref); - } + state.branch) + add_checked_out_branch(state.branch, wt->path); wt_status_state_free_buffers(&state); if (wt_status_check_bisect(wt, &state) && - state.bisecting_from) { - struct strbuf ref = STRBUF_INIT; - strbuf_addf(&ref, "refs/heads/%s", state.bisecting_from); - old = strmap_put(¤t_checked_out_branches, - ref.buf, - xstrdup(wt->path)); - free(old); - strbuf_release(&ref); - } + state.bisecting_from) + add_checked_out_branch(state.bisecting_from, wt->path); wt_status_state_free_buffers(&state); wt_gitdir = get_worktree_git_dir(wt); if (!sequencer_get_update_refs_state(wt_gitdir, &update_refs)) { struct string_list_item *item; - for_each_string_list_item(item, &update_refs) { - old = strmap_put(¤t_checked_out_branches, - item->string, - xstrdup(wt->path)); - free(old); - } + for_each_string_list_item(item, &update_refs) + add_checked_out_branch(item->string, wt->path); string_list_clear(&update_refs, 1); } diff --git a/sequencer.c b/sequencer.c index 1355a99a092268..1e777696dabf7e 100644 --- a/sequencer.c +++ b/sequencer.c @@ -6459,34 +6459,71 @@ 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; + } + + /* + * A symref to another local branch is only an alias. The + * target branch has its own decoration, so only queue the + * concrete branch. + */ + 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 or its referent is the current HEAD, then it + * will be updated by the default rebase behavior. + */ + if (head_ref && resolved_ref && + !strcmp(head_ref, resolved_ref)) { + 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; } @@ -6498,13 +6535,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); @@ -6518,9 +6559,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; } @@ -6534,6 +6577,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; @@ -6559,6 +6603,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. */ diff --git a/t/t3400-rebase.sh b/t/t3400-rebase.sh index e62e07b894669e..1a02f6546ba2f7 100755 --- a/t/t3400-rebase.sh +++ b/t/t3400-rebase.sh @@ -471,7 +471,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 diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index e64816770a3d6e..f862160cdd3dfe 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -1979,11 +1979,15 @@ 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/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/second-alias refs/heads/second && test_commit extra2 fileX && git commit --amend --fixup=L && @@ -1993,6 +1997,9 @@ test_expect_success '--update-refs updates refs correctly' ' test_cmp_rev HEAD~3 refs/heads/second && test_cmp_rev HEAD~1 refs/heads/third && test_cmp_rev HEAD refs/heads/no-conflict-branch && + 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. @@ -2008,6 +2015,110 @@ test_expect_success '--update-refs updates refs correctly' ' test_cmp expect err.trimmed ' +test_expect_success '--update-refs skips symref to current non-branch target' ' + test_create_repo current-non-branch-target && + test_commit -C current-non-branch-target one && + test_commit -C current-non-branch-target two && + test_commit -C current-non-branch-target three && + git -C current-non-branch-target update-ref \ + refs/tags/current-non-branch-target HEAD && + git -C current-non-branch-target symbolic-ref \ + refs/heads/current-non-branch-alias \ + refs/tags/current-non-branch-target && + git -C current-non-branch-target symbolic-ref HEAD \ + refs/heads/current-non-branch-alias && + ( + cd current-non-branch-target && + GIT_SEQUENCE_EDITOR="cat >todo" \ + git rebase -i --force-rebase --update-refs HEAD~2 && + + test_grep ! "refs/heads/current-non-branch-alias" todo + ) +' + +test_expect_success '--update-refs checks resolved non-branch symref target' ' + test_when_finished " + test_might_fail git worktree remove --force checked-out-target-wt && + test_might_fail git symbolic-ref -d refs/heads/non-branch-alias && + test_might_fail git symbolic-ref -d \ + refs/heads/checked-out-target-alias && + test_might_fail 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 symbolic-ref refs/heads/checked-out-target-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/heads/checked-out-target-alias && + + 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_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/tags/shared-non-branch-target && + test_write_lines refs/tags/shared-non-branch-target >expect && + git symbolic-ref refs/heads/non-branch-alias-one >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-update-alias-one && + test_might_fail git symbolic-ref -d \ + refs/heads/reserved-update-alias-two && + test_might_fail git symbolic-ref -d \ + refs/heads/reserved-head-alias-one && + test_might_fail git symbolic-ref -d \ + refs/heads/reserved-head-alias-two && + test_might_fail git tag -d reserved-update-target && + test_might_fail git tag -d reserved-head-target + " && + git tag reserved-update-target HEAD~1 && + git symbolic-ref refs/heads/reserved-update-alias-one \ + refs/tags/reserved-update-target && + git symbolic-ref refs/heads/reserved-update-alias-two \ + refs/tags/reserved-update-target && + git tag reserved-head-target HEAD~2 && + git symbolic-ref refs/heads/reserved-head-alias-one \ + refs/tags/reserved-head-target && + git symbolic-ref refs/heads/reserved-head-alias-two \ + refs/tags/reserved-head-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-update-alias-one) && + test_write_lines refs/heads/reserved-update-alias-one \ + "$old_oid" "$old_oid" >"$wt_gitdir/rebase-merge/update-refs" && + test_write_lines refs/heads/reserved-head-alias-one \ + >"$wt_gitdir/rebase-merge/head-name" && + + GIT_SEQUENCE_EDITOR="cat >todo" git rebase -i --update-refs HEAD~3 && + + test_grep "^# Ref refs/heads/reserved-update-alias-two checked out at" \ + todo && + test_grep "^# Ref refs/heads/reserved-head-alias-two checked out at" \ + todo && + test_grep ! "^update-ref refs/heads/reserved-.*-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 &&