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) void die_if_checked_out(const char *branch, int ignore_current_worktree)
{ {
struct worktree **worktrees = get_worktrees(); struct worktree **worktrees = get_worktrees();
const struct worktree *wt;
wt = find_shared_symref(worktrees, "HEAD", branch); for (int i = 0; worktrees[i]; i++) {
if (wt && (!ignore_current_worktree || !wt->is_current)) { if (worktrees[i]->is_current && ignore_current_worktree)
skip_prefix(branch, "refs/heads/", &branch); continue;
die(_("'%s' is already checked out at '%s'"), branch, wt->path);
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); 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_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 test_done

View File

@ -388,6 +388,20 @@ test_expect_success 'switch to branch checked out here' '
git rebase main main 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' ' test_expect_success 'switch to branch not checked out' '
git checkout main && git checkout main &&
git branch other && 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 * bisect). New commands that do similar things should update this
* function as well. * 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 struct worktree *find_shared_symref(struct worktree **worktrees,
const char *symref, const char *symref,
const char *target) const char *target)
{ {
const struct worktree *existing = NULL;
int i = 0;
for (i = 0; worktrees[i]; i++) { for (int i = 0; worktrees[i]; i++)
struct worktree *wt = worktrees[i]; if (is_shared_symref(worktrees[i], symref, target))
const char *symref_target; return worktrees[i];
struct ref_store *refs;
int flags;
if (wt->is_bare) return NULL;
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;
} }
int submodule_uses_worktrees(const char *path) 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 *symref,
const char *target); 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 * Similar to head_ref() for all HEADs _except_ one from the current
* worktree, which is covered by head_ref(). * worktree, which is covered by head_ref().