checkout: forbid "-B <branch>" from touching a branch used elsewhere

"git checkout -B <branch> [<start-point>]", being a "forced" version of "-b", switches to the <branch>, after optionally resetting its tip to the <start-point>, even if the <branch> is in use in another worktree, which is somewhat unexpected. Protect the <branch> using the same logic that forbids "git checkout <branch>" from touching a branch that is in use elsewhere. This is a breaking change that may deserve backward compatibliity warning in the Release Notes. The "--ignore-other-worktrees" option can be used as an escape hatch if the finger memory of existing users depend on the current behaviour of "-B". Reported-by: Willem Verstraeten <willem.verstraeten@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Junio C Hamano committed Nov 23, 2023 at 15:00 UTC b23285a921a84c3b83d9aaca31afedf290c59254
5 files changed +41 -3
Documentation/git-checkout.txt
+3 -1
@@ -63,7 +63,9 @@ $ git checkout <branch>
63 ------------
64 +
65 that is to say, the branch is not reset/created unless "git checkout" is
66 -successful.
66 +successful (e.g., when the branch is in use in another worktree, not
67 +just the current branch stays the same, but the branch is not reset to
68 +the start-point, either).
69
70 'git checkout' --detach [<branch>]::
71 'git checkout' [--detach] <commit>::
Documentation/git-switch.txt
+7 -2
@@ -59,13 +59,18 @@ out at most one of `A` and `B`, in which case it defaults to `HEAD`.
59 -c <new-branch>::
60 --create <new-branch>::
61 Create a new branch named `<new-branch>` starting at
62 - `<start-point>` before switching to the branch. This is a
63 - convenient shortcut for:
62 + `<start-point>` before switching to the branch. This is the
63 + transactional equivalent of
64 +
65 ------------
66 $ git branch <new-branch>
67 $ git switch <new-branch>
68 ------------
69 ++
70 +that is to say, the branch is not reset/created unless "git switch" is
71 +successful (e.g., when the branch is in use in another worktree, not
72 +just the current branch stays the same, but the branch is not reset to
73 +the start-point, either).
74
75 -C <new-branch>::
76 --force-create <new-branch>::
builtin/checkout.c
+7
@@ -1600,6 +1600,13 @@ static int checkout_branch(struct checkout_opts *opts,
1600 if (new_branch_info->path && !opts->force_detach && !opts->new_branch)
1601 die_if_switching_to_a_branch_in_use(opts, new_branch_info->path);
1602
1603 + /* "git checkout -B <branch>" */
1604 + if (opts->new_branch_force) {
1605 + char *full_ref = xstrfmt("refs/heads/%s", opts->new_branch);
1606 + die_if_switching_to_a_branch_in_use(opts, full_ref);
1607 + free(full_ref);
1608 + }
1609 +
1610 if (!new_branch_info->commit && opts->new_branch) {
1611 struct object_id rev;
1612 int flag;
t/t2060-switch.sh
+2
@@ -170,8 +170,10 @@ test_expect_success 'switch back when temporarily detached and checked out elsew
170 # we test in both worktrees to ensure that works
171 # as expected with "first" and "next" worktrees
172 test_must_fail git -C wt1 switch shared &&
173 + test_must_fail git -C wt1 switch -C shared &&
174 git -C wt1 switch --ignore-other-worktrees shared &&
175 test_must_fail git -C wt2 switch shared &&
176 + test_must_fail git -C wt2 switch -C shared &&
177 git -C wt2 switch --ignore-other-worktrees shared
178 '
179
t/t2400-worktree-add.sh
+22
@@ -126,6 +126,28 @@ test_expect_success 'die the same branch is already checked out' '
126 )
127 '
128
129 +test_expect_success 'refuse to reset a branch in use elsewhere' '
130 + (
131 + cd here &&
132 +
133 + # we know we are on detached HEAD but just in case ...
134 + git checkout --detach HEAD &&
135 + git rev-parse --verify HEAD >old.head &&
136 +
137 + git rev-parse --verify refs/heads/newmain >old.branch &&
138 + test_must_fail git checkout -B newmain 2>error &&
139 + git rev-parse --verify refs/heads/newmain >new.branch &&
140 + git rev-parse --verify HEAD >new.head &&
141 +
142 + grep "already used by worktree at" error &&
143 + test_cmp old.branch new.branch &&
144 + test_cmp old.head new.head &&
145 +
146 + # and we must be still on the same detached HEAD state
147 + test_must_fail git symbolic-ref HEAD
148 + )
149 +'
150 +
151 test_expect_success SYMLINKS 'die the same branch is already checked out (symlink)' '
152 head=$(git -C there rev-parse --git-path HEAD) &&
153 ref=$(git -C there symbolic-ref HEAD) &&