Merge branch 'rj/avoid-switching-to-already-used-branch'

A few subcommands have been taught to stop users from working on a
branch that is being used in another worktree linked to the same
repository.

* rj/avoid-switching-to-already-used-branch:
  switch: reject if the branch is already checked out elsewhere (test)
  rebase: refuse to switch to a branch already checked out elsewhere (test)
  branch: fix die_if_checked_out() when ignore_current_worktree
  worktree: introduce is_shared_symref()
This commit is contained in:
Junio C Hamano 2023-03-19 15:03:11 -07:00
commit 96a806f87a
5 changed files with 89 additions and 37 deletions

View File

@ -821,12 +821,16 @@ void remove_branch_state(struct repository *r, int verbose)
void die_if_checked_out(const char *branch, int ignore_current_worktree)
{
struct worktree **worktrees = get_worktrees();
const struct worktree *wt;
wt = find_shared_symref(worktrees, "HEAD", branch);
if (wt && (!ignore_current_worktree || !wt->is_current)) {
skip_prefix(branch, "refs/heads/", &branch);
die(_("'%s' is already checked out at '%s'"), branch, wt->path);
for (int i = 0; worktrees[i]; i++) {
if (worktrees[i]->is_current && ignore_current_worktree)
continue;
if (is_shared_symref(worktrees[i], "HEAD", branch)) {
skip_prefix(branch, "refs/heads/", &branch);
die(_("'%s' is already checked out at '%s'"),
branch, worktrees[i]->path);
}
}
free_worktrees(worktrees);

View File

@ -146,4 +146,33 @@ test_expect_success 'tracking info copied with autoSetupMerge=inherit' '
test_cmp_config "" --default "" branch.main2.merge
'
test_expect_success 'switch back when temporarily detached and checked out elsewhere ' '
test_when_finished "
git worktree remove wt1 ||:
git worktree remove wt2 ||:
git checkout - ||:
git branch -D shared ||:
" &&
git checkout -b shared &&
test_commit shared-first &&
HASH1=$(git rev-parse --verify HEAD) &&
test_commit shared-second &&
test_commit shared-third &&
HASH2=$(git rev-parse --verify HEAD) &&
git worktree add wt1 -f shared &&
git -C wt1 bisect start &&
git -C wt1 bisect good $HASH1 &&
git -C wt1 bisect bad $HASH2 &&
git worktree add wt2 -f shared &&
git -C wt2 bisect start &&
git -C wt2 bisect good $HASH1 &&
git -C wt2 bisect bad $HASH2 &&
# we test in both worktrees to ensure that works
# as expected with "first" and "next" worktrees
test_must_fail git -C wt1 switch shared &&
git -C wt1 switch --ignore-other-worktrees shared &&
test_must_fail git -C wt2 switch shared &&
git -C wt2 switch --ignore-other-worktrees shared
'
test_done

View File

@ -388,6 +388,20 @@ test_expect_success 'switch to branch checked out here' '
git rebase main main
'
test_expect_success 'switch to branch checked out elsewhere fails' '
test_when_finished "
git worktree remove wt1 &&
git worktree remove wt2 &&
git branch -d shared
" &&
git worktree add wt1 -b shared &&
git worktree add wt2 -f shared &&
# we test in both worktrees to ensure that works
# as expected with "first" and "next" worktrees
test_must_fail git -C wt1 rebase shared shared &&
test_must_fail git -C wt2 rebase shared shared
'
test_expect_success 'switch to branch not checked out' '
git checkout main &&
git branch other &&

View File

@ -404,44 +404,43 @@ int is_worktree_being_bisected(const struct worktree *wt,
* bisect). New commands that do similar things should update this
* function as well.
*/
int is_shared_symref(const struct worktree *wt, const char *symref,
const char *target)
{
const char *symref_target;
struct ref_store *refs;
int flags;
if (wt->is_bare)
return 0;
if (wt->is_detached && !strcmp(symref, "HEAD")) {
if (is_worktree_being_rebased(wt, target))
return 1;
if (is_worktree_being_bisected(wt, target))
return 1;
}
refs = get_worktree_ref_store(wt);
symref_target = refs_resolve_ref_unsafe(refs, symref, 0,
NULL, &flags);
if ((flags & REF_ISSYMREF) &&
symref_target && !strcmp(symref_target, target))
return 1;
return 0;
}
const struct worktree *find_shared_symref(struct worktree **worktrees,
const char *symref,
const char *target)
{
const struct worktree *existing = NULL;
int i = 0;
for (i = 0; worktrees[i]; i++) {
struct worktree *wt = worktrees[i];
const char *symref_target;
struct ref_store *refs;
int flags;
for (int i = 0; worktrees[i]; i++)
if (is_shared_symref(worktrees[i], symref, target))
return worktrees[i];
if (wt->is_bare)
continue;
if (wt->is_detached && !strcmp(symref, "HEAD")) {
if (is_worktree_being_rebased(wt, target)) {
existing = wt;
break;
}
if (is_worktree_being_bisected(wt, target)) {
existing = wt;
break;
}
}
refs = get_worktree_ref_store(wt);
symref_target = refs_resolve_ref_unsafe(refs, symref, 0,
NULL, &flags);
if ((flags & REF_ISSYMREF) &&
symref_target && !strcmp(symref_target, target)) {
existing = wt;
break;
}
}
return existing;
return NULL;
}
int submodule_uses_worktrees(const char *path)

View File

@ -148,6 +148,12 @@ const struct worktree *find_shared_symref(struct worktree **worktrees,
const char *symref,
const char *target);
/*
* Returns true if a symref points to a ref in a worktree.
*/
int is_shared_symref(const struct worktree *wt,
const char *symref, const char *target);
/*
* Similar to head_ref() for all HEADs _except_ one from the current
* worktree, which is covered by head_ref().