Skip to content

Commit 97f0fda

Browse files
committed
worktree: allow sharing a checked-out branch across worktrees
When spinning up several worktrees on the same checkout for parallel work (for example a fleet of agents working from one branch), git's refusal to check out a branch that is already checked out elsewhere is just in the way. The restriction exists to stop two worktrees from moving the same branch underneath each other, but plain parallel checkouts do not need that protection. Drop the restriction: "git worktree add <branch>" now checks out a branch even if it is in use by another worktree. The genuinely dangerous case is kept -- a branch that another worktree is in the middle of rebasing or bisecting is still refused, because a second checkout could corrupt that operation. die_if_branch_busy() performs that narrower check in place of the old die_if_checked_out(). The separate guard against force-updating (e.g. with -B) a branch in use elsewhere is left untouched. Signed-off-by: Jason Newton <nevion@gmail.com>
1 parent 7dbd9ca commit 97f0fda

3 files changed

Lines changed: 62 additions & 16 deletions

File tree

Documentation/git-worktree.adoc

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,9 @@ then, as a convenience, the new worktree is associated with a branch (call
9393
it _<branch>_) named after `$(basename <path>)`. If _<branch>_ doesn't
9494
exist, a new branch based on `HEAD` is automatically created as if
9595
`-b <branch>` was given. If _<branch>_ does exist, it will be checked out
96-
in the new worktree, if it's not checked out anywhere else, otherwise the
97-
command will refuse to create the worktree (unless `--force` is used).
96+
in the new worktree, even if it is already checked out in another worktree.
97+
(A branch that another worktree is in the middle of rebasing or bisecting is
98+
refused unless `--force` is used.)
9899
+
99100
If _<commit-ish>_ is omitted, neither `--detach`, or `--orphan` is
100101
used, and there are no valid local branches (or remote branches if
@@ -177,12 +178,12 @@ OPTIONS
177178

178179
`-f`::
179180
`--force`::
180-
By default, `add` refuses to create a new worktree when
181-
_<commit-ish>_ is a branch name and is already checked out by
182-
another worktree, or if _<path>_ is already assigned to some
183-
worktree but is missing (for instance, if _<path>_ was deleted
184-
manually). This option overrides these safeguards. To add a missing but
185-
locked worktree path, specify `--force` twice.
181+
`add` refuses to create a new worktree when _<commit-ish>_ is a
182+
branch that another worktree is in the middle of rebasing or
183+
bisecting, or if _<path>_ is already assigned to some worktree but
184+
is missing (for instance, if _<path>_ was deleted manually). This
185+
option overrides these safeguards. To add a missing but locked
186+
worktree path, specify `--force` twice.
186187
+
187188
`move` refuses to move a locked worktree unless `--force` is specified
188189
twice. If the destination is already assigned to some other worktree but is

builtin/worktree.c

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,34 @@ static void setup_alternate_ref_dir(struct worktree *wt, const char *wt_git_path
648648
strbuf_release(&sb);
649649
}
650650

651+
/*
652+
* Checking out a branch that is already checked out in another worktree is
653+
* fine -- it is exactly what you want when spinning up several worktrees on
654+
* the same checkout for parallel work. The one case that is still unsafe is a
655+
* branch that another worktree is in the middle of rebasing or bisecting,
656+
* since a second checkout could corrupt that operation, so refuse only that.
657+
*/
658+
static void die_if_branch_busy(const char *branch)
659+
{
660+
struct worktree **worktrees = get_worktrees();
661+
int i;
662+
663+
for (i = 0; worktrees[i]; i++) {
664+
const struct worktree *wt = worktrees[i];
665+
666+
if (is_worktree_being_rebased(wt, branch) ||
667+
is_worktree_being_bisected(wt, branch)) {
668+
const char *shortname = branch;
669+
670+
skip_prefix(branch, "refs/heads/", &shortname);
671+
die(_("'%s' is already used by worktree at '%s'"),
672+
shortname, wt->path);
673+
}
674+
}
675+
676+
free_worktrees(worktrees);
677+
}
678+
651679
static int add_worktree(const char *path, const char *refname,
652680
const struct add_opts *opts)
653681
{
@@ -675,7 +703,7 @@ static int add_worktree(const char *path, const char *refname,
675703
refs_ref_exists(get_main_ref_store(the_repository), symref.buf)) {
676704
is_branch = 1;
677705
if (!opts->force)
678-
die_if_checked_out(symref.buf, 0);
706+
die_if_branch_busy(symref.buf);
679707
}
680708
commit = lookup_commit_reference_by_name(refname);
681709
if (!commit && !opts->orphan)

t/t2400-worktree-add.sh

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,24 @@ test_expect_success '"add" using - shorthand' '
4040
test_cmp expect actual
4141
'
4242

43-
test_expect_success '"add" refuses to checkout locked branch' '
44-
test_must_fail git worktree add zere main &&
45-
test_path_is_missing zere &&
46-
test_path_is_missing .git/worktrees/zere
43+
test_expect_success '"add" can check out a branch in use by another worktree' '
44+
test_when_finished "git worktree remove -f zere || :" &&
45+
git worktree add zere main &&
46+
echo refs/heads/main >expect &&
47+
git -C zere symbolic-ref HEAD >actual &&
48+
test_cmp expect actual
49+
'
50+
51+
test_expect_success 'the same branch can be checked out in several worktrees' '
52+
test_when_finished "git worktree remove -f shared1 || :; git worktree remove -f shared2 || :" &&
53+
git branch -f sharedbr main &&
54+
git worktree add shared1 sharedbr &&
55+
git worktree add shared2 sharedbr &&
56+
echo refs/heads/sharedbr >expect &&
57+
git -C shared1 symbolic-ref HEAD >actual &&
58+
test_cmp expect actual &&
59+
git -C shared2 symbolic-ref HEAD >actual &&
60+
test_cmp expect actual
4761
'
4862

4963
test_expect_success 'checking out paths not complaining about linked checkouts' '
@@ -304,10 +318,13 @@ test_expect_success '"add" checks out existing branch of dwimd name' '
304318
)
305319
'
306320

307-
test_expect_success '"add <path>" dwim fails with checked out branch' '
321+
test_expect_success '"add <path>" dwim shares a checked out branch' '
308322
git checkout -b test-branch &&
309-
test_must_fail git worktree add test-branch &&
310-
test_path_is_missing test-branch
323+
git worktree add test-branch &&
324+
echo refs/heads/test-branch >expect &&
325+
git -C test-branch symbolic-ref HEAD >actual &&
326+
test_cmp expect actual &&
327+
git worktree remove test-branch
311328
'
312329

313330
test_expect_success '"add --force" with existing dwimd name doesnt die' '

0 commit comments

Comments
 (0)