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>
This commit is contained in:
Junio C Hamano
2023-11-23 15:00:31 +09:00
parent 9263c40a0a
commit b23285a921
5 changed files with 41 additions and 3 deletions

View File

@ -63,7 +63,9 @@ $ git checkout <branch>
------------ ------------
+ +
that is to say, the branch is not reset/created unless "git checkout" is that is to say, the branch is not reset/created unless "git checkout" is
successful. successful (e.g., when the branch is in use in another worktree, not
just the current branch stays the same, but the branch is not reset to
the start-point, either).
'git checkout' --detach [<branch>]:: 'git checkout' --detach [<branch>]::
'git checkout' [--detach] <commit>:: 'git checkout' [--detach] <commit>::

View File

@ -59,13 +59,18 @@ out at most one of `A` and `B`, in which case it defaults to `HEAD`.
-c <new-branch>:: -c <new-branch>::
--create <new-branch>:: --create <new-branch>::
Create a new branch named `<new-branch>` starting at Create a new branch named `<new-branch>` starting at
`<start-point>` before switching to the branch. This is a `<start-point>` before switching to the branch. This is the
convenient shortcut for: transactional equivalent of
+ +
------------ ------------
$ git branch <new-branch> $ git branch <new-branch>
$ git switch <new-branch> $ git switch <new-branch>
------------ ------------
+
that is to say, the branch is not reset/created unless "git switch" is
successful (e.g., when the branch is in use in another worktree, not
just the current branch stays the same, but the branch is not reset to
the start-point, either).
-C <new-branch>:: -C <new-branch>::
--force-create <new-branch>:: --force-create <new-branch>::

View File

@ -1600,6 +1600,13 @@ static int checkout_branch(struct checkout_opts *opts,
if (new_branch_info->path && !opts->force_detach && !opts->new_branch) if (new_branch_info->path && !opts->force_detach && !opts->new_branch)
die_if_switching_to_a_branch_in_use(opts, new_branch_info->path); die_if_switching_to_a_branch_in_use(opts, new_branch_info->path);
/* "git checkout -B <branch>" */
if (opts->new_branch_force) {
char *full_ref = xstrfmt("refs/heads/%s", opts->new_branch);
die_if_switching_to_a_branch_in_use(opts, full_ref);
free(full_ref);
}
if (!new_branch_info->commit && opts->new_branch) { if (!new_branch_info->commit && opts->new_branch) {
struct object_id rev; struct object_id rev;
int flag; int flag;

View File

@ -170,8 +170,10 @@ test_expect_success 'switch back when temporarily detached and checked out elsew
# we test in both worktrees to ensure that works # we test in both worktrees to ensure that works
# as expected with "first" and "next" worktrees # as expected with "first" and "next" worktrees
test_must_fail git -C wt1 switch shared && test_must_fail git -C wt1 switch shared &&
test_must_fail git -C wt1 switch -C shared &&
git -C wt1 switch --ignore-other-worktrees shared && git -C wt1 switch --ignore-other-worktrees shared &&
test_must_fail git -C wt2 switch shared && test_must_fail git -C wt2 switch shared &&
test_must_fail git -C wt2 switch -C shared &&
git -C wt2 switch --ignore-other-worktrees shared git -C wt2 switch --ignore-other-worktrees shared
' '

View File

@ -126,6 +126,28 @@ test_expect_success 'die the same branch is already checked out' '
) )
' '
test_expect_success 'refuse to reset a branch in use elsewhere' '
(
cd here &&
# we know we are on detached HEAD but just in case ...
git checkout --detach HEAD &&
git rev-parse --verify HEAD >old.head &&
git rev-parse --verify refs/heads/newmain >old.branch &&
test_must_fail git checkout -B newmain 2>error &&
git rev-parse --verify refs/heads/newmain >new.branch &&
git rev-parse --verify HEAD >new.head &&
grep "already used by worktree at" error &&
test_cmp old.branch new.branch &&
test_cmp old.head new.head &&
# and we must be still on the same detached HEAD state
test_must_fail git symbolic-ref HEAD
)
'
test_expect_success SYMLINKS 'die the same branch is already checked out (symlink)' ' test_expect_success SYMLINKS 'die the same branch is already checked out (symlink)' '
head=$(git -C there rev-parse --git-path HEAD) && head=$(git -C there rev-parse --git-path HEAD) &&
ref=$(git -C there symbolic-ref HEAD) && ref=$(git -C there symbolic-ref HEAD) &&