* [RFC PATCH 0/1] worktree list: add --merged and --no-merged
@ 2026-10-21 17:33 Jasper Mayone
2026-10-21 17:33 ` [RFC PATCH 1/1] " Jasper Mayone
2026-10-21 21:36 ` [RFC PATCH 0/1] worktree list: add --merged and --no-merged Harald Nordgren
0 siblings, 2 replies; 33+ messages in thread
From: Jasper Mayone @ 2026-10-21 17:33 UTC (permalink / raw)
To: git; +Cc: gitster, haraldnordgren, sunshine, ben.knoble, Jasper Mayone
I keep one worktree per topic. When a topic lands, its branch stays
checked out in that worktree, so "git branch --delete-merged" skips
it, and there is no direct way to ask which worktrees are done. In
the --delete-merged squash thread [1] I said I would send an RFC for
this. Here it is.
This patch teaches "git worktree list" the --merged and --no-merged
filters that "git branch", "git tag" and "git for-each-ref" already
have, applied to the HEAD of each worktree:
$ git worktree list --merged origin/main
/src/proj 1a2b3c4 [main]
/src/proj-topic-a 5d6e7f8 [topic-a]
/src/proj-fix-b 9a8b7c6 (detached HEAD)
It only lists. Removing stays a separate step with "git worktree
remove", which already refuses a dirty or locked worktree and the
main worktree, so a script can do:
git worktree list --porcelain --merged origin/main |
sed -n "s/^worktree //p" |
while read -r wt; do git worktree remove "$wt"; done
The test is strict reachability, the same as "git branch --merged".
A worktree whose topic was squash or rebase merged upstream is not
listed. If Harald's "has this landed" check ends up as a helper, a
later option could use it here too, but I kept that out of this RFC.
Questions for the list:
1. The main worktree is filtered like any other, so it is usually
listed. "git branch --merged" also lists the current branch, so
I kept that. Should it be left out instead?
2. The commit defaults to HEAD, like the ref filters. For cleanup,
each worktree's branch upstream would often be the more useful
default, but that is a different meaning for the same option
name. Is HEAD the right default?
3. I reused struct ref_filter and OPT_MERGED from ref-filter.h, so
the parsing, the default and the error messages match the other
commands. It only needs the two commit lists. Is that acceptable,
or would you prefer a small local option callback? If the reuse
is fine, --contains and --points-at could follow the same way.
4. Is a later "git worktree remove --merged <commit>" (with
--dry-run) wanted, or should removal stay in scripts?
[1] https://lore.kernel.org/git/pull.2425.v3.git.git.1792585717.gitgitgadget@gmail.com/
Jasper Mayone (1):
worktree list: add --merged and --no-merged
Documentation/git-worktree.adoc | 24 +++++-
builtin/worktree.c | 80 +++++++++++++++++++-
t/t2402-worktree-list.sh | 125 ++++++++++++++++++++++++++++++++
3 files changed, 226 insertions(+), 3 deletions(-)
base-commit: 47fdf2208c5b94c8cc01c106b8f7c85efb91c926
--
2.56.0
^ permalink raw reply [flat|nested] 33+ messages in thread
* [RFC PATCH 1/1] worktree list: add --merged and --no-merged
2026-10-21 17:33 [RFC PATCH 0/1] worktree list: add --merged and --no-merged Jasper Mayone
@ 2026-10-21 17:33 ` Jasper Mayone
2026-10-23 18:30 ` Junio C Hamano
2026-10-21 21:36 ` [RFC PATCH 0/1] worktree list: add --merged and --no-merged Harald Nordgren
1 sibling, 1 reply; 33+ messages in thread
From: Jasper Mayone @ 2026-10-21 17:33 UTC (permalink / raw)
To: git; +Cc: gitster, haraldnordgren, sunshine, ben.knoble, Jasper Mayone
Users who keep one worktree per topic have no direct way to find the
worktrees whose work has already landed. "git branch --merged" lists
the branches, but the user then has to match each branch to a
worktree by hand, and a detached worktree has no branch to match.
"git branch --delete-merged" skips any branch that is checked out in
a worktree, so these are exactly the branches it leaves behind.
Teach "git worktree list" the same --merged and --no-merged filters
that "git branch", "git tag" and "git for-each-ref" have, applied to
the HEAD of each worktree. The options are parsed by the same
OPT_MERGED and OPT_NO_MERGED helpers, so the default (the HEAD of the
current worktree), the error messages and the rules for combining
them are the same: a worktree is shown when its HEAD is reachable
from at least one of the --merged commits and from none of the
--no-merged commits.
A bare worktree, a worktree on an unborn branch, and a worktree whose
HEAD cannot be read have no HEAD commit, so the filters leave them
out. Both output formats are kept, so the result can be used directly
with "--porcelain -z":
git worktree list --porcelain -z --merged origin/main
Nothing is removed. Removing the worktrees stays a separate step with
"git worktree remove", which already refuses a dirty or locked
worktree.
Signed-off-by: Jasper Mayone <me@jaspermayone.com>
---
Documentation/git-worktree.adoc | 24 +++++-
builtin/worktree.c | 80 +++++++++++++++++++-
t/t2402-worktree-list.sh | 125 ++++++++++++++++++++++++++++++++
3 files changed, 226 insertions(+), 3 deletions(-)
diff --git a/Documentation/git-worktree.adoc b/Documentation/git-worktree.adoc
index e6e77252ab..817766b07c 100644
--- a/Documentation/git-worktree.adoc
+++ b/Documentation/git-worktree.adoc
@@ -11,7 +11,7 @@ SYNOPSIS
[synopsis]
git worktree add [-f] [--detach] [--checkout] [--lock [--reason <string>]]
[--orphan] [(-b | -B) <new-branch>] <path> [<commit-ish>]
-git worktree list [-v | --porcelain [-z]]
+git worktree list [-v | --porcelain [-z]] [--merged [<commit>]] [--no-merged [<commit>]]
git worktree lock [--reason <string>] <worktree>
git worktree move <worktree> <new-path>
git worktree prune [-n] [-v] [--expire <expire>]
@@ -114,6 +114,11 @@ whether the worktree is bare, the revision currently checked out, the
branch currently checked out (or "detached HEAD" if none), "locked" if
the worktree is locked, "prunable" if the worktree can be pruned by the
`prune` command.
++
+With `--merged` or `--no-merged`, only the worktrees whose `HEAD` is
+(or is not) reachable from the given commit are listed. This finds the
+worktrees whose work has landed, for example to remove them with
+`git worktree remove`.
`lock`::
@@ -268,6 +273,23 @@ mismatch, even if the links are correct.
to parse the output when a worktree path contains a newline
character.
+`--merged [<commit>]`::
+ With `list`, only list worktrees whose `HEAD` is reachable from
+ _<commit>_. _<commit>_ defaults to the `HEAD` of the current
+ worktree.
+
+`--no-merged [<commit>]`::
+ With `list`, only list worktrees whose `HEAD` is not reachable
+ from _<commit>_. _<commit>_ defaults to the `HEAD` of the current
+ worktree.
++
+When `--merged` and `--no-merged` are combined or repeated, only
+worktrees whose `HEAD` is reachable from at least one of the
+`--merged` commits and from none of the `--no-merged` commits are
+listed. With either option, a bare worktree, a worktree on an unborn
+branch, and a worktree whose `HEAD` cannot be read are not listed,
+because they have no `HEAD` commit.
+
`-q`::
`--quiet`::
With `add`, suppress feedback messages.
diff --git a/builtin/worktree.c b/builtin/worktree.c
index 77ecd0f71f..5db9efb435 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -5,6 +5,7 @@
#include "abspath.h"
#include "advice.h"
#include "checkout.h"
+#include "commit-reach.h"
#include "config.h"
#include "copy.h"
#include "dir.h"
@@ -18,8 +19,10 @@
#include "strvec.h"
#include "branch.h"
#include "read-cache-ll.h"
+#include "ref-filter.h"
#include "refs.h"
#include "remote.h"
+#include "revision.h"
#include "run-command.h"
#include "hook.h"
#include "sigchain.h"
@@ -33,7 +36,7 @@
" [--orphan] [(-b | -B) <new-branch>] <path> [<commit-ish>]")
#define BUILTIN_WORKTREE_LIST_USAGE \
- N_("git worktree list [-v | --porcelain [-z]]")
+ N_("git worktree list [-v | --porcelain [-z]] [--merged [<commit>]] [--no-merged [<commit>]]")
#define BUILTIN_WORKTREE_LOCK_USAGE \
N_("git worktree lock [--reason <string>] <worktree>")
#define BUILTIN_WORKTREE_MOVE_USAGE \
@@ -1125,11 +1128,78 @@ static void pathsort(struct worktree **wt)
QSORT(wt, n, pathcmp);
}
+/*
+ * Set result[i] when tips[i] is reachable from one of the commits in
+ * "bases".
+ */
+static void mark_reachable(struct repository *repo,
+ struct commit **tips, size_t nr,
+ struct commit_list *bases, int *result)
+{
+ tips_reachable_from_bases(repo, bases, tips, nr, UNINTERESTING);
+ for (size_t i = 0; i < nr; i++)
+ result[i] = !!(tips[i]->object.flags & UNINTERESTING);
+ clear_commit_marks_many(nr, tips, ALL_REV_FLAGS);
+ for (; bases; bases = bases->next)
+ clear_commit_marks(bases->item, ALL_REV_FLAGS);
+}
+
+/*
+ * Keep only the worktrees whose HEAD is reachable from one of the
+ * "--merged" commits and from none of the "--no-merged" commits, like
+ * the ref filters of "git branch". A bare or unborn worktree has no
+ * HEAD commit, and neither has a broken one, so these are dropped.
+ */
+static void filter_merged(struct repository *repo, struct worktree **wt,
+ struct ref_filter *filter)
+{
+ struct commit **tips = NULL;
+ int *merged, *unmerged;
+ size_t nr = 0, alloc = 0, kept = 0;
+
+ for (size_t i = 0; wt[i]; i++) {
+ struct commit *commit = NULL;
+
+ if (!wt[i]->is_bare && !is_null_oid(&wt[i]->head_oid))
+ commit = lookup_commit_reference_gently(repo,
+ &wt[i]->head_oid, 1);
+ if (!commit) {
+ free_worktree(wt[i]);
+ continue;
+ }
+ ALLOC_GROW(tips, nr + 1, alloc);
+ tips[nr] = commit;
+ wt[nr++] = wt[i];
+ }
+ wt[nr] = NULL;
+
+ CALLOC_ARRAY(merged, nr);
+ CALLOC_ARRAY(unmerged, nr);
+ if (filter->reachable_from)
+ mark_reachable(repo, tips, nr, filter->reachable_from, merged);
+ if (filter->unreachable_from)
+ mark_reachable(repo, tips, nr, filter->unreachable_from,
+ unmerged);
+
+ for (size_t i = 0; i < nr; i++) {
+ if ((!filter->reachable_from || merged[i]) && !unmerged[i])
+ wt[kept++] = wt[i];
+ else
+ free_worktree(wt[i]);
+ }
+ wt[kept] = NULL;
+
+ free(tips);
+ free(merged);
+ free(unmerged);
+}
+
static int list(int ac, const char **av, const char *prefix,
- struct repository *repo UNUSED)
+ struct repository *repo)
{
int porcelain = 0;
int line_terminator = '\n';
+ struct ref_filter filter = REF_FILTER_INIT;
struct option options[] = {
OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")),
@@ -1138,6 +1208,8 @@ static int list(int ac, const char **av, const char *prefix,
N_("add 'prunable' annotation to missing worktrees older than <time>")),
OPT_SET_INT('z', NULL, &line_terminator,
N_("terminate records with a NUL character"), '\0'),
+ OPT_MERGED(&filter, N_("show only worktrees whose HEAD is merged")),
+ OPT_NO_MERGED(&filter, N_("show only worktrees whose HEAD is not merged")),
OPT_END()
};
@@ -1157,6 +1229,9 @@ static int list(int ac, const char **av, const char *prefix,
/* sort worktrees by path but keep main worktree at top */
pathsort(worktrees + 1);
+ if (filter.reachable_from || filter.unreachable_from)
+ filter_merged(repo, worktrees, &filter);
+
if (!porcelain)
measure_widths(worktrees, &abbrev,
&display, &path_maxwidth);
@@ -1174,6 +1249,7 @@ static int list(int ac, const char **av, const char *prefix,
free(display);
free_worktrees(worktrees);
}
+ ref_filter_clear(&filter);
return 0;
}
diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
index eeaf800d74..0e0e3d7f06 100755
--- a/t/t2402-worktree-list.sh
+++ b/t/t2402-worktree-list.sh
@@ -318,4 +318,129 @@ test_expect_success 'worktree path when called in .git directory' '
test_cmp list1 list2
'
+test_expect_success 'setup for "list --merged"' '
+ git init merged &&
+ (
+ cd merged &&
+ test_commit base &&
+ git branch landed &&
+ git branch pending &&
+ git worktree add ../wt-landed landed &&
+ git worktree add ../wt-pending pending &&
+ git worktree add --detach ../wt-detached main &&
+ git -C ../wt-landed commit --allow-empty -m landed &&
+ git -C ../wt-pending commit --allow-empty -m pending &&
+ git merge --ff-only landed
+ )
+'
+
+list_paths () {
+ git -C merged worktree list --porcelain "$@" >out &&
+ sed -n "s/^worktree //p" out >actual
+}
+
+test_expect_success '"list --merged" shows only merged worktrees' '
+ list_paths --merged main &&
+ cat >expect <<-EOF &&
+ $(pwd)/merged
+ $(pwd)/wt-detached
+ $(pwd)/wt-landed
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success '"list --merged" defaults to the current worktree HEAD' '
+ git -C wt-pending worktree list --porcelain --merged >out &&
+ sed -n "s/^worktree //p" out >actual &&
+ cat >expect <<-EOF &&
+ $(pwd)/wt-detached
+ $(pwd)/wt-pending
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success '"list --no-merged" shows only unmerged worktrees' '
+ list_paths --no-merged main &&
+ echo "$(pwd)/wt-pending" >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success '"list --merged" with several commits' '
+ list_paths --merged main --merged pending --no-merged base &&
+ cat >expect <<-EOF &&
+ $(pwd)/merged
+ $(pwd)/wt-landed
+ $(pwd)/wt-pending
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success '"list --merged" keeps the normal output format' '
+ git -C merged worktree list --merged main >out &&
+ sed "s/ */ /g" <out >actual &&
+ cat >expect <<-EOF &&
+ $(pwd)/merged $(git -C merged rev-parse --short main) [main]
+ $(pwd)/wt-detached $(git -C merged rev-parse --short base) (detached HEAD)
+ $(pwd)/wt-landed $(git -C merged rev-parse --short landed) [landed]
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success '"list --merged" rejects a bad commit' '
+ test_must_fail git -C merged worktree list --merged no-such-ref 2>err &&
+ test_grep "malformed object name" err &&
+ test_must_fail git -C merged worktree list --merged "main^{tree}" 2>err &&
+ test_grep "must point to a commit" err
+'
+
+test_expect_success '"list --merged" can list nothing' '
+ git -C merged worktree list --merged base --no-merged base >actual &&
+ test_must_be_empty actual
+'
+
+test_expect_success '"list --merged" works with -z' '
+ git -C merged worktree list --porcelain -z --no-merged main >out &&
+ tr "\0" "\n" <out >actual &&
+ cat >expect <<-EOF &&
+ worktree $(pwd)/wt-pending
+ HEAD $(git -C merged rev-parse pending)
+ branch refs/heads/pending
+
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success '"list --merged" works with -v' '
+ test_when_finished "git -C merged worktree unlock wt-landed" &&
+ git -C merged worktree lock --reason "keep it" wt-landed &&
+ git -C merged worktree list -v --merged main >out &&
+ test_grep "^ locked: keep it" out
+'
+
+test_expect_success '"list --[no-]merged" omits unborn and broken worktrees' '
+ test_when_finished "rm -rf wt-unborn wt-broken && git -C merged worktree prune" &&
+ git -C merged worktree add --orphan -b unborn ../wt-unborn &&
+ git -C merged worktree add --detach ../wt-broken main &&
+ echo "$ZERO_OID" >merged/.git/worktrees/wt-broken/HEAD &&
+ git -C merged worktree list --porcelain >out &&
+ test_grep "^worktree $(pwd)/wt-unborn" out &&
+ test_grep "^worktree $(pwd)/wt-broken" out &&
+ git -C merged worktree list --porcelain --merged main >out &&
+ test_grep ! "wt-unborn" out &&
+ test_grep ! "wt-broken" out &&
+ git -C merged worktree list --porcelain --no-merged main >out &&
+ test_grep ! "wt-unborn" out &&
+ test_grep ! "wt-broken" out
+'
+
+test_expect_success '"list --merged" omits a bare main worktree' '
+ test_when_finished "rm -rf bare-merged there" &&
+ git clone --bare merged bare-merged &&
+ git -C bare-merged worktree add --detach ../there main &&
+ git -C bare-merged worktree list --porcelain --merged main >out &&
+ sed -n "s/^worktree //p" out >actual &&
+ git -C there rev-parse --show-toplevel >expect &&
+ test_cmp expect actual
+'
+
test_done
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* Re: [RFC PATCH 0/1] worktree list: add --merged and --no-merged
2026-10-21 17:33 [RFC PATCH 0/1] worktree list: add --merged and --no-merged Jasper Mayone
2026-10-21 17:33 ` [RFC PATCH 1/1] " Jasper Mayone
@ 2026-10-21 21:36 ` Harald Nordgren
2026-10-21 22:12 ` Junio C Hamano
2026-10-22 17:58 ` Kristoffer Haugsbakk
1 sibling, 2 replies; 33+ messages in thread
From: Harald Nordgren @ 2026-10-21 21:36 UTC (permalink / raw)
To: Jasper Mayone; +Cc: git, gitster, sunshine, ben.knoble
I wonder if we should relax (or allow relaxing) the requirements on
worktree branches overall?
I noticed that I can't check out a branch that is checked out in
another worktree. Which seems a bit ridiculous because two regular
repos of the same upstream would never enforce that.
AI loves worktress, so I suspect we'll see more of these discussions
in the future.
Harald
^ permalink raw reply [flat|nested] 33+ messages in thread
* Re: [RFC PATCH 0/1] worktree list: add --merged and --no-merged
2026-10-21 21:36 ` [RFC PATCH 0/1] worktree list: add --merged and --no-merged Harald Nordgren
@ 2026-10-21 22:12 ` Junio C Hamano
2026-10-22 7:21 ` Harald Nordgren
2026-10-22 17:58 ` Kristoffer Haugsbakk
1 sibling, 1 reply; 33+ messages in thread
From: Junio C Hamano @ 2026-10-21 22:12 UTC (permalink / raw)
To: Harald Nordgren; +Cc: Jasper Mayone, git, sunshine, ben.knoble
Harald Nordgren <haraldnordgren@gmail.com> writes:
> I wonder if we should relax (or allow relaxing) the requirements on
> worktree branches overall?
>
> I noticed that I can't check out a branch that is checked out in
> another worktree. Which seems a bit ridiculous because two regular
> repos of the same upstream would never enforce that.
Because your repository updating what commit your 'main' points at
would not affect my repository if they track the same upstream.
Worktrees are different. If you allow your two worktrees to check
out the same 'main', and one of them makes a commit on top, you know
what happens, right?
> AI loves worktress, so I suspect we'll see more of these discussions
> in the future.
I think that was asked and answered already. "worktree add -d" is
your friend.
^ permalink raw reply [flat|nested] 33+ messages in thread
* Re: [RFC PATCH 0/1] worktree list: add --merged and --no-merged
2026-10-21 22:12 ` Junio C Hamano
@ 2026-10-22 7:21 ` Harald Nordgren
0 siblings, 0 replies; 33+ messages in thread
From: Harald Nordgren @ 2026-10-22 7:21 UTC (permalink / raw)
To: Junio C Hamano; +Cc: Jasper Mayone, git, sunshine, ben.knoble
> > I noticed that I can't check out a branch that is checked out in
> > another worktree. Which seems a bit ridiculous because two regular
> > repos of the same upstream would never enforce that.
>
> Because your repository updating what commit your 'main' points at
> would not affect my repository if they track the same upstream.
>
> Worktrees are different. If you allow your two worktrees to check
> out the same 'main', and one of them makes a commit on top, you know
> what happens, right?
I didn't. I read up on it now, and yes it's hairy. I withdraw my point here.
> > AI loves worktress, so I suspect we'll see more of these discussions
> > in the future.
>
> I think that was asked and answered already. "worktree add -d" is
> your friend.
Ok.
Harald
^ permalink raw reply [flat|nested] 33+ messages in thread
* Re: [RFC PATCH 0/1] worktree list: add --merged and --no-merged
2026-10-21 21:36 ` [RFC PATCH 0/1] worktree list: add --merged and --no-merged Harald Nordgren
2026-10-21 22:12 ` Junio C Hamano
@ 2026-10-22 17:58 ` Kristoffer Haugsbakk
1 sibling, 0 replies; 33+ messages in thread
From: Kristoffer Haugsbakk @ 2026-10-22 17:58 UTC (permalink / raw)
To: Harald Nordgren, Jasper Mayone
Cc: git, Junio C Hamano, Eric Sunshine, D. Ben Knoble
On Wed, Oct 21, 2026, at 23:36, Harald Nordgren wrote:
>[snip]
> AI loves worktress, so I suspect we'll see more of these discussions
> in the future.
Is this what mechanical sympathy has come to?
^ permalink raw reply [flat|nested] 33+ messages in thread
* Re: [RFC PATCH 1/1] worktree list: add --merged and --no-merged
2026-10-21 17:33 ` [RFC PATCH 1/1] " Jasper Mayone
@ 2026-10-23 18:30 ` Junio C Hamano
2026-10-24 6:25 ` Eric Sunshine
0 siblings, 1 reply; 33+ messages in thread
From: Junio C Hamano @ 2026-10-23 18:30 UTC (permalink / raw)
To: Jasper Mayone; +Cc: git, haraldnordgren, sunshine, ben.knoble
Jasper Mayone <me@jaspermayone.com> writes:
> Users who keep one worktree per topic have no direct way to find the
> worktrees whose work has already landed. "git branch --merged" lists
> the branches, but the user then has to match each branch to a
> worktree by hand, and a detached worktree has no branch to match.
> "git branch --delete-merged" skips any branch that is checked out in
> a worktree, so these are exactly the branches it leaves behind.
Real review on this patch never happened, it seems, perhaps because
a discussion on a side issue distracted everybody?
It looks like the intro unfairly narrows the problem space. Even
for those users who do not use one worktree per topic do need to
find worktrees based on what branch they check out. While I agree
that among the criteria to choose these branches to filter worktrees
on, "--merged" and "--not-merged" may be two of the most useful ones,
the feature does not have to be limited to them.
I wonder if "git worktree list --branch=...", where --branch= can be
given multiple times, is a better primitive? With that you could do
something like
$ git worktree list $(git branch --contains HEAD | sed -e 's/^/--branch=/')
to reuse the same machinery without any extra code.
^ permalink raw reply [flat|nested] 33+ messages in thread
* Re: [RFC PATCH 1/1] worktree list: add --merged and --no-merged
2026-10-23 18:30 ` Junio C Hamano
@ 2026-10-24 6:25 ` Eric Sunshine
2026-10-24 16:53 ` [PATCH v2] worktree list: add --branch to select worktrees by branch Jasper Mayone
0 siblings, 1 reply; 33+ messages in thread
From: Eric Sunshine @ 2026-10-24 6:25 UTC (permalink / raw)
To: Junio C Hamano; +Cc: Jasper Mayone, git, haraldnordgren, ben.knoble
On Fri, Oct 23, 2026 at 2:30 PM Junio C Hamano <gitster@pobox.com> wrote:
> Jasper Mayone <me@jaspermayone.com> writes:
> > Users who keep one worktree per topic have no direct way to find the
> > worktrees whose work has already landed. "git branch --merged" lists
> > the branches, but the user then has to match each branch to a
> > worktree by hand, and a detached worktree has no branch to match.
> > "git branch --delete-merged" skips any branch that is checked out in
> > a worktree, so these are exactly the branches it leaves behind.
>
> Real review on this patch never happened, it seems, perhaps because
> a discussion on a side issue distracted everybody?
>
> It looks like the intro unfairly narrows the problem space. Even
> for those users who do not use one worktree per topic do need to
> find worktrees based on what branch they check out. While I agree
> that among the criteria to choose these branches to filter worktrees
> on, "--merged" and "--not-merged" may be two of the most useful ones,
> the feature does not have to be limited to them.
>
> I wonder if "git worktree list --branch=...", where --branch= can be
> given multiple times, is a better primitive? With that you could do
> something like
>
> $ git worktree list $(git branch --contains HEAD | sed -e 's/^/--branch=/')
>
> to reuse the same machinery without any extra code.
I've been too busy lately to allocate time to the Git project, so I
didn't read the patch body and only quickly skimmed the commit
message, but I experienced the same reaction that this seems overly
special-case, and immediately thought of a more general "for-each"
sort of mechanism. The analogy which immediately comes to mind is `git
submodule foreach <command>`.
^ permalink raw reply [flat|nested] 33+ messages in thread
* [PATCH v2] worktree list: add --branch to select worktrees by branch
2026-10-24 6:25 ` Eric Sunshine
@ 2026-10-24 16:53 ` Jasper Mayone
2026-10-25 14:47 ` Phillip Wood
` (2 more replies)
0 siblings, 3 replies; 33+ messages in thread
From: Jasper Mayone @ 2026-10-24 16:53 UTC (permalink / raw)
To: git; +Cc: gitster, sunshine, haraldnordgren, ben.knoble, Jasper Mayone
There is no direct way to ask which worktree has a given branch
checked out. "git worktree list --porcelain" prints the branch of
every worktree, so a script has to parse the whole list and match the
"branch" lines itself.
Teach "git worktree list" a "--branch=<branch>" option that lists
only the worktrees that have <branch> checked out. It can be given
more than once. A worktree that is rebasing or bisecting a branch
counts as having it checked out, as it does for "git branch -d" and
"git worktree add". <branch> is a branch name, or a full name under
refs/heads/.
This keeps the choice of branches in "git branch", which already has
--merged, --contains, --points-at and patterns. For example, to list
the worktrees whose branch is merged into origin/main:
git worktree list \
$(git branch --format='--branch=%(refname)' --merged origin/main)
The --format is needed because the default output of "git branch"
marks the current branch with "*" and the branches checked out in
other worktrees with "+". It uses the full name because
%(refname:short) gives "heads/topic" when a tag is also called
"topic".
Each worktree's branch is found once and looked up in a set of the
requested branches, so feeding it every branch of a large repository
stays cheap.
A detached worktree has no branch, so it is never listed.
Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Jasper Mayone <me@jaspermayone.com>
---
Changes since the RFC [1]:
* Junio pointed out that --merged and --no-merged narrow the problem
to one workflow, and suggested selecting worktrees by branch
instead, with "git branch" doing the choosing. Eric had the same
reaction. This version does that and drops --merged, --no-merged
and the reachability code.
* Junio's example piped "git branch" through sed. Its default output
prefixes the current branch with "*" and branches checked out in
other worktrees with "+", which are the branches this is for, so
the words come out as "--branch=+" and "topic". The documented form
uses --format='--branch=%(refname)'. The full name also avoids
"heads/topic" from %(refname:short) when a tag has the same name.
* <branch> is an exact name, not a pattern, because "git branch"
already has --list <pattern>, --merged, --contains and friends.
An invalid name dies, as for "git branch -d". A valid name that no
worktree has checked out lists nothing.
* Eric, a "git worktree foreach" would be nice, and it could select
worktrees with this same --branch. I left it out of this patch.
[1] https://lore.kernel.org/git/20261021173350.67854-1-me@jaspermayone.com/
Documentation/git-worktree.adoc | 21 ++++-
builtin/worktree.c | 79 ++++++++++++++++++-
t/t2402-worktree-list.sh | 131 ++++++++++++++++++++++++++++++++
3 files changed, 228 insertions(+), 3 deletions(-)
diff --git a/Documentation/git-worktree.adoc b/Documentation/git-worktree.adoc
index e6e77252ab..4db11eab88 100644
--- a/Documentation/git-worktree.adoc
+++ b/Documentation/git-worktree.adoc
@@ -11,7 +11,7 @@ SYNOPSIS
[synopsis]
git worktree add [-f] [--detach] [--checkout] [--lock [--reason <string>]]
[--orphan] [(-b | -B) <new-branch>] <path> [<commit-ish>]
-git worktree list [-v | --porcelain [-z]]
+git worktree list [-v | --porcelain [-z]] [--branch=<branch>...]
git worktree lock [--reason <string>] <worktree>
git worktree move <worktree> <new-path>
git worktree prune [-n] [-v] [--expire <expire>]
@@ -114,6 +114,9 @@ whether the worktree is bare, the revision currently checked out, the
branch currently checked out (or "detached HEAD" if none), "locked" if
the worktree is locked, "prunable" if the worktree can be pruned by the
`prune` command.
++
+With `--branch`, only the worktrees that have one of the given branches
+checked out are listed.
`lock`::
@@ -268,6 +271,22 @@ mismatch, even if the links are correct.
to parse the output when a worktree path contains a newline
character.
+`--branch=<branch>`::
+ With `list`, only list worktrees that have _<branch>_ checked
+ out, including a worktree that is rebasing or bisecting
+ _<branch>_. _<branch>_ is a branch name, such as `topic` or
+ `@{-1}`, or a full name under `refs/heads/`. The option can be
+ given more than once to list the worktrees of several branches.
+ A detached worktree is never listed.
++
+Use linkgit:git-branch[1] to choose the branches. For example, to list
+the worktrees whose branch is merged into `origin/main`:
++
+------------
+$ git worktree list \
+ $(git branch --format='--branch=%(refname)' --merged origin/main)
+------------
+
`-q`::
`--quiet`::
With `add`, suppress feedback messages.
diff --git a/builtin/worktree.c b/builtin/worktree.c
index 77ecd0f71f..1d7db5f7fc 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -15,6 +15,7 @@
#include "object-name.h"
#include "parse-options.h"
#include "path.h"
+#include "strmap.h"
#include "strvec.h"
#include "branch.h"
#include "read-cache-ll.h"
@@ -26,6 +27,7 @@
#include "submodule.h"
#include "utf8.h"
#include "worktree.h"
+#include "wt-status.h"
#include "quote.h"
#define BUILTIN_WORKTREE_ADD_USAGE \
@@ -33,7 +35,7 @@
" [--orphan] [(-b | -B) <new-branch>] <path> [<commit-ish>]")
#define BUILTIN_WORKTREE_LIST_USAGE \
- N_("git worktree list [-v | --porcelain [-z]]")
+ N_("git worktree list [-v | --porcelain [-z]] [--branch=<branch>...]")
#define BUILTIN_WORKTREE_LOCK_USAGE \
N_("git worktree lock [--reason <string>] <worktree>")
#define BUILTIN_WORKTREE_MOVE_USAGE \
@@ -1125,11 +1127,78 @@ static void pathsort(struct worktree **wt)
QSORT(wt, n, pathcmp);
}
+/*
+ * Return the full name of the branch that "wt" has checked out, or
+ * NULL. A worktree that is rebasing or bisecting a branch has it
+ * checked out, too, as for is_shared_symref().
+ */
+static char *checked_out_branch(const struct worktree *wt)
+{
+ struct wt_status_state state = { 0 };
+ char *ref = NULL;
+
+ if (wt->is_bare)
+ return NULL;
+ if (!wt->is_detached)
+ return xstrdup_or_null(wt->head_ref);
+
+ if (wt_status_check_rebase(wt, &state) &&
+ (state.rebase_in_progress || state.rebase_interactive_in_progress) &&
+ state.branch)
+ ref = xstrfmt("refs/heads/%s", state.branch);
+ else if (wt_status_check_bisect(wt, &state) && state.bisecting_from)
+ ref = xstrfmt("refs/heads/%s", state.bisecting_from);
+ wt_status_state_free_buffers(&state);
+ return ref;
+}
+
+/*
+ * Keep only the worktrees that have one of the "--branch" branches
+ * checked out. A branch is given by its name, or by its full name
+ * under refs/heads/.
+ */
+static void filter_by_branch(struct repository *repo, struct worktree **wt,
+ const struct string_list *branches)
+{
+ struct strset refnames = STRSET_INIT;
+ struct string_list_item *item;
+ struct strbuf ref = STRBUF_INIT;
+ size_t kept = 0;
+
+ for_each_string_list_item(item, branches) {
+ strbuf_reset(&ref);
+ if (starts_with(item->string, "refs/heads/")) {
+ strbuf_addstr(&ref, item->string);
+ if (check_refname_format(ref.buf, 0))
+ die(_("'%s' is not a valid branch name"),
+ item->string);
+ } else if (check_branch_ref(repo, &ref, item->string)) {
+ die(_("'%s' is not a valid branch name"), item->string);
+ }
+ strset_add(&refnames, ref.buf);
+ }
+
+ for (size_t i = 0; wt[i]; i++) {
+ char *branch = checked_out_branch(wt[i]);
+
+ if (branch && strset_contains(&refnames, branch))
+ wt[kept++] = wt[i];
+ else
+ free_worktree(wt[i]);
+ free(branch);
+ }
+ wt[kept] = NULL;
+
+ strbuf_release(&ref);
+ strset_clear(&refnames);
+}
+
static int list(int ac, const char **av, const char *prefix,
- struct repository *repo UNUSED)
+ struct repository *repo)
{
int porcelain = 0;
int line_terminator = '\n';
+ struct string_list branches = STRING_LIST_INIT_DUP;
struct option options[] = {
OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")),
@@ -1138,6 +1207,8 @@ static int list(int ac, const char **av, const char *prefix,
N_("add 'prunable' annotation to missing worktrees older than <time>")),
OPT_SET_INT('z', NULL, &line_terminator,
N_("terminate records with a NUL character"), '\0'),
+ OPT_STRING_LIST(0, "branch", &branches, N_("branch"),
+ N_("only show worktrees that have <branch> checked out")),
OPT_END()
};
@@ -1157,6 +1228,9 @@ static int list(int ac, const char **av, const char *prefix,
/* sort worktrees by path but keep main worktree at top */
pathsort(worktrees + 1);
+ if (branches.nr)
+ filter_by_branch(repo, worktrees, &branches);
+
if (!porcelain)
measure_widths(worktrees, &abbrev,
&display, &path_maxwidth);
@@ -1174,6 +1248,7 @@ static int list(int ac, const char **av, const char *prefix,
free(display);
free_worktrees(worktrees);
}
+ string_list_clear(&branches, 0);
return 0;
}
diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
index eeaf800d74..ea08bf79fa 100755
--- a/t/t2402-worktree-list.sh
+++ b/t/t2402-worktree-list.sh
@@ -318,4 +318,135 @@ test_expect_success 'worktree path when called in .git directory' '
test_cmp list1 list2
'
+test_expect_success 'setup for "list --branch"' '
+ git init by-branch &&
+ (
+ cd by-branch &&
+ test_commit base &&
+ git branch topic-a &&
+ git branch topic-b &&
+ git branch unused &&
+ git worktree add ../wt-a topic-a &&
+ git worktree add ../wt-b topic-b &&
+ git worktree add --detach ../wt-detached main &&
+ git -C ../wt-a commit --allow-empty -m a &&
+ git -C ../wt-b commit --allow-empty -m b &&
+ git merge --ff-only topic-a
+ )
+'
+
+list_paths () {
+ git -C by-branch worktree list --porcelain "$@" >out &&
+ sed -n "s/^worktree //p" out >actual
+}
+
+test_expect_success '"list --branch" shows the worktree of that branch' '
+ list_paths --branch=topic-a &&
+ echo "$(pwd)/wt-a" >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success '"list --branch" can be repeated' '
+ list_paths --branch=topic-b --branch=main --branch=topic-a &&
+ cat >expect <<-EOF &&
+ $(pwd)/by-branch
+ $(pwd)/wt-a
+ $(pwd)/wt-b
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success '"list --branch" with a branch that is not checked out' '
+ list_paths --branch=unused --branch=no-such-branch &&
+ test_must_be_empty actual
+'
+
+test_expect_success '"list --branch" never shows a detached worktree' '
+ list_paths --branch=main &&
+ echo "$(pwd)/by-branch" >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success '"list --branch" rejects an invalid branch name' '
+ test_must_fail git -C by-branch worktree list --branch=-bad 2>err &&
+ test_grep "is not a valid branch name" err
+'
+
+test_expect_success '"list --branch" keeps the normal output format' '
+ git -C by-branch worktree list --branch=topic-a >out &&
+ sed "s/ */ /g" <out >actual &&
+ echo "$(pwd)/wt-a $(git -C by-branch rev-parse --short topic-a) [topic-a]" >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success '"list --branch" works with -z' '
+ git -C by-branch worktree list --porcelain -z --branch=topic-b >out &&
+ tr "\0" "\n" <out >actual &&
+ cat >expect <<-EOF &&
+ worktree $(pwd)/wt-b
+ HEAD $(git -C by-branch rev-parse topic-b)
+ branch refs/heads/topic-b
+
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success '"list --branch" shows a worktree bisecting that branch' '
+ test_when_finished "git -C wt-b bisect reset && git -C wt-b reset --hard HEAD~2" &&
+ git -C wt-b commit --allow-empty -m b1 &&
+ git -C wt-b commit --allow-empty -m b2 &&
+ git -C wt-b bisect start topic-b topic-b~2 &&
+ test_must_fail git -C wt-b symbolic-ref -q HEAD &&
+ list_paths --branch=topic-b &&
+ echo "$(pwd)/wt-b" >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success '"list --branch" shows a worktree rebasing that branch' '
+ test_when_finished "git -C wt-b rebase --abort" &&
+ test_must_fail git -C wt-b rebase -f -x false base &&
+ test_must_fail git -C wt-b symbolic-ref -q HEAD &&
+ list_paths --branch=topic-b &&
+ echo "$(pwd)/wt-b" >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success '"list --branch" accepts a full branch name' '
+ list_paths --branch=refs/heads/topic-a &&
+ echo "$(pwd)/wt-a" >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success '"list --branch" with a tag of the same name' '
+ test_when_finished "git -C by-branch tag -d topic-a" &&
+ git -C by-branch tag topic-a base &&
+ list_paths --branch=topic-a &&
+ echo "$(pwd)/wt-a" >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success '"list --branch" shows unborn and prunable worktrees' '
+ test_when_finished "rm -rf wt-unborn wt-gone && git -C by-branch worktree prune" &&
+ git -C by-branch worktree add --orphan -b unborn ../wt-unborn &&
+ git -C by-branch branch gone &&
+ git -C by-branch worktree add ../wt-gone gone &&
+ rm -rf wt-gone &&
+ git -C by-branch worktree list -v --branch=unborn --branch=gone >out &&
+ test_grep "^$(pwd)/wt-unborn .*\[unborn\]$" out &&
+ test_grep "^$(pwd)/wt-gone .*\[gone\]$" out &&
+ test_grep "prunable: gitdir file points to non-existent location" out
+'
+
+test_expect_success '"list --branch" with "git branch --merged"' '
+ git -C by-branch worktree list --porcelain \
+ $(git -C by-branch branch --format="--branch=%(refname)" \
+ --merged main) >out &&
+ sed -n "s/^worktree //p" out >actual &&
+ cat >expect <<-EOF &&
+ $(pwd)/by-branch
+ $(pwd)/wt-a
+ EOF
+ test_cmp expect actual
+'
+
test_done
base-commit: 7e34d8174dcda3435d1404c9a30bef46afe6c13e
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* Re: [PATCH v2] worktree list: add --branch to select worktrees by branch
2026-10-24 16:53 ` [PATCH v2] worktree list: add --branch to select worktrees by branch Jasper Mayone
@ 2026-10-25 14:47 ` Phillip Wood
2026-10-26 11:02 ` Phillip Wood
2026-10-28 6:02 ` Eric Sunshine
2026-10-28 11:32 ` Kristoffer Haugsbakk
2026-10-30 14:21 ` [PATCH 0/2] worktree: treat a branch being rebased or bisected as checked out Jasper Mayone
2 siblings, 2 replies; 33+ messages in thread
From: Phillip Wood @ 2026-10-25 14:47 UTC (permalink / raw)
To: Jasper Mayone, git; +Cc: gitster, sunshine, haraldnordgren, ben.knoble
Hi Jasper
On 24/10/2026 17:53, Jasper Mayone wrote:
> There is no direct way to ask which worktree has a given branch
> checked out. "git worktree list --porcelain" prints the branch of
> every worktree, so a script has to parse the whole list and match the
> "branch" lines itself.
>
> Teach "git worktree list" a "--branch=<branch>" option that lists
> only the worktrees that have <branch> checked out. It can be given
> more than once.
Playing devil's advocate for a minute that means the script still has to
match branch names if it wants to know each branch is checked out. It
does make it easier for scripts that are only interested in a single
branch though.
> A worktree that is rebasing or bisecting a branch
> counts as having it checked out, as it does for "git branch -d" and
> "git worktree add". <branch> is a branch name, or a full name under
> refs/heads/.
That's useful because "git worktree list" does not show branch names
when they're being rebased or bisected. The commit message should spell
that out.
> This keeps the choice of branches in "git branch", which already has
> --merged, --contains, --points-at and patterns.
This sentence seems to assume the reader has some context from the
previous iteration
> For example, to list
> the worktrees whose branch is merged into origin/main:
>
> git worktree list \
> $(git branch --format='--branch=%(refname)' --merged origin/main)
>
> The --format is needed because the default output of "git branch"
> marks the current branch with "*" and the branches checked out in
> other worktrees with "+". It uses the full name because
> %(refname:short) gives "heads/topic" when a tag is also called
> "topic".
>
> Each worktree's branch is found once and looked up in a set of the
> requested branches, so feeding it every branch of a large repository
> stays cheap.
The implementation seems to duplicate much of
branch.c:prepare_checked_out_branches(). It would be nicer to add a
preparatory patch (or maybe two patches) that adds a "struct worktree"
member to "struct checked_out_branch" and converts
"checked_out_branches" to a hashmap indexed by the branch name. Then we
can implement "git worktree list --branch" in terms of a new function
const struct worktree *branch_find_checkout(const char *branch);
in branch.c that looks up the branch in "checked_out_branches" and
returns the worktree if it is checked out, or it is being rebased or
bisected.
Another thing to think about is that normally the main worktree is
listed first, but if we are listing a subset of worktrees there is no
way to tell from the output if one of them is the main worktree. I think
that matters because we don't want to delete the main worktree - "git
worktree remove path/to/main-worktree" will die but it would be nice if
the script could avoid calling that in the first place. So I think we
want another patch that adds something like "is-main-worktree true" when
the main worktree is printed.
Is there a reason we reject syntactically invalid branch names, but
accept the names of branches that do not exist?
> A detached worktree has no branch, so it is never listed.
I'm not sure that sentence is really needed.
Thanks
Phillip
> Suggested-by: Junio C Hamano <gitster@pobox.com>
> Signed-off-by: Jasper Mayone <me@jaspermayone.com>
> ---
> Changes since the RFC [1]:
>
> * Junio pointed out that --merged and --no-merged narrow the problem
> to one workflow, and suggested selecting worktrees by branch
> instead, with "git branch" doing the choosing. Eric had the same
> reaction. This version does that and drops --merged, --no-merged
> and the reachability code.
>
> * Junio's example piped "git branch" through sed. Its default output
> prefixes the current branch with "*" and branches checked out in
> other worktrees with "+", which are the branches this is for, so
> the words come out as "--branch=+" and "topic". The documented form
> uses --format='--branch=%(refname)'. The full name also avoids
> "heads/topic" from %(refname:short) when a tag has the same name.
>
> * <branch> is an exact name, not a pattern, because "git branch"
> already has --list <pattern>, --merged, --contains and friends.
> An invalid name dies, as for "git branch -d". A valid name that no
> worktree has checked out lists nothing.
>
> * Eric, a "git worktree foreach" would be nice, and it could select
> worktrees with this same --branch. I left it out of this patch.
>
> [1] https://lore.kernel.org/git/20261021173350.67854-1-me@jaspermayone.com/
>
> Documentation/git-worktree.adoc | 21 ++++-
> builtin/worktree.c | 79 ++++++++++++++++++-
> t/t2402-worktree-list.sh | 131 ++++++++++++++++++++++++++++++++
> 3 files changed, 228 insertions(+), 3 deletions(-)
>
> diff --git a/Documentation/git-worktree.adoc b/Documentation/git-worktree.adoc
> index e6e77252ab..4db11eab88 100644
> --- a/Documentation/git-worktree.adoc
> +++ b/Documentation/git-worktree.adoc
> @@ -11,7 +11,7 @@ SYNOPSIS
> [synopsis]
> git worktree add [-f] [--detach] [--checkout] [--lock [--reason <string>]]
> [--orphan] [(-b | -B) <new-branch>] <path> [<commit-ish>]
> -git worktree list [-v | --porcelain [-z]]
> +git worktree list [-v | --porcelain [-z]] [--branch=<branch>...]
> git worktree lock [--reason <string>] <worktree>
> git worktree move <worktree> <new-path>
> git worktree prune [-n] [-v] [--expire <expire>]
> @@ -114,6 +114,9 @@ whether the worktree is bare, the revision currently checked out, the
> branch currently checked out (or "detached HEAD" if none), "locked" if
> the worktree is locked, "prunable" if the worktree can be pruned by the
> `prune` command.
> ++
> +With `--branch`, only the worktrees that have one of the given branches
> +checked out are listed.
>
> `lock`::
>
> @@ -268,6 +271,22 @@ mismatch, even if the links are correct.
> to parse the output when a worktree path contains a newline
> character.
>
> +`--branch=<branch>`::
> + With `list`, only list worktrees that have _<branch>_ checked
> + out, including a worktree that is rebasing or bisecting
> + _<branch>_. _<branch>_ is a branch name, such as `topic` or
> + `@{-1}`, or a full name under `refs/heads/`. The option can be
> + given more than once to list the worktrees of several branches.
> + A detached worktree is never listed.
> ++
> +Use linkgit:git-branch[1] to choose the branches. For example, to list
> +the worktrees whose branch is merged into `origin/main`:
> ++
> +------------
> +$ git worktree list \
> + $(git branch --format='--branch=%(refname)' --merged origin/main)
> +------------
> +
> `-q`::
> `--quiet`::
> With `add`, suppress feedback messages.
> diff --git a/builtin/worktree.c b/builtin/worktree.c
> index 77ecd0f71f..1d7db5f7fc 100644
> --- a/builtin/worktree.c
> +++ b/builtin/worktree.c
> @@ -15,6 +15,7 @@
> #include "object-name.h"
> #include "parse-options.h"
> #include "path.h"
> +#include "strmap.h"
> #include "strvec.h"
> #include "branch.h"
> #include "read-cache-ll.h"
> @@ -26,6 +27,7 @@
> #include "submodule.h"
> #include "utf8.h"
> #include "worktree.h"
> +#include "wt-status.h"
> #include "quote.h"
>
> #define BUILTIN_WORKTREE_ADD_USAGE \
> @@ -33,7 +35,7 @@
> " [--orphan] [(-b | -B) <new-branch>] <path> [<commit-ish>]")
>
> #define BUILTIN_WORKTREE_LIST_USAGE \
> - N_("git worktree list [-v | --porcelain [-z]]")
> + N_("git worktree list [-v | --porcelain [-z]] [--branch=<branch>...]")
> #define BUILTIN_WORKTREE_LOCK_USAGE \
> N_("git worktree lock [--reason <string>] <worktree>")
> #define BUILTIN_WORKTREE_MOVE_USAGE \
> @@ -1125,11 +1127,78 @@ static void pathsort(struct worktree **wt)
> QSORT(wt, n, pathcmp);
> }
>
> +/*
> + * Return the full name of the branch that "wt" has checked out, or
> + * NULL. A worktree that is rebasing or bisecting a branch has it
> + * checked out, too, as for is_shared_symref().
> + */
> +static char *checked_out_branch(const struct worktree *wt)
> +{
> + struct wt_status_state state = { 0 };
> + char *ref = NULL;
> +
> + if (wt->is_bare)
> + return NULL;
> + if (!wt->is_detached)
> + return xstrdup_or_null(wt->head_ref);
> +
> + if (wt_status_check_rebase(wt, &state) &&
> + (state.rebase_in_progress || state.rebase_interactive_in_progress) &&
> + state.branch)
> + ref = xstrfmt("refs/heads/%s", state.branch);
> + else if (wt_status_check_bisect(wt, &state) && state.bisecting_from)
> + ref = xstrfmt("refs/heads/%s", state.bisecting_from);
> + wt_status_state_free_buffers(&state);
> + return ref;
> +}
> +
> +/*
> + * Keep only the worktrees that have one of the "--branch" branches
> + * checked out. A branch is given by its name, or by its full name
> + * under refs/heads/.
> + */
> +static void filter_by_branch(struct repository *repo, struct worktree **wt,
> + const struct string_list *branches)
> +{
> + struct strset refnames = STRSET_INIT;
> + struct string_list_item *item;
> + struct strbuf ref = STRBUF_INIT;
> + size_t kept = 0;
> +
> + for_each_string_list_item(item, branches) {
> + strbuf_reset(&ref);
> + if (starts_with(item->string, "refs/heads/")) {
> + strbuf_addstr(&ref, item->string);
> + if (check_refname_format(ref.buf, 0))
> + die(_("'%s' is not a valid branch name"),
> + item->string);
> + } else if (check_branch_ref(repo, &ref, item->string)) {
> + die(_("'%s' is not a valid branch name"), item->string);
> + }
> + strset_add(&refnames, ref.buf);
> + }
> +
> + for (size_t i = 0; wt[i]; i++) {
> + char *branch = checked_out_branch(wt[i]);
> +
> + if (branch && strset_contains(&refnames, branch))
> + wt[kept++] = wt[i];
> + else
> + free_worktree(wt[i]);
> + free(branch);
> + }
> + wt[kept] = NULL;
> +
> + strbuf_release(&ref);
> + strset_clear(&refnames);
> +}
> +
> static int list(int ac, const char **av, const char *prefix,
> - struct repository *repo UNUSED)
> + struct repository *repo)
> {
> int porcelain = 0;
> int line_terminator = '\n';
> + struct string_list branches = STRING_LIST_INIT_DUP;
>
> struct option options[] = {
> OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")),
> @@ -1138,6 +1207,8 @@ static int list(int ac, const char **av, const char *prefix,
> N_("add 'prunable' annotation to missing worktrees older than <time>")),
> OPT_SET_INT('z', NULL, &line_terminator,
> N_("terminate records with a NUL character"), '\0'),
> + OPT_STRING_LIST(0, "branch", &branches, N_("branch"),
> + N_("only show worktrees that have <branch> checked out")),
> OPT_END()
> };
>
> @@ -1157,6 +1228,9 @@ static int list(int ac, const char **av, const char *prefix,
> /* sort worktrees by path but keep main worktree at top */
> pathsort(worktrees + 1);
>
> + if (branches.nr)
> + filter_by_branch(repo, worktrees, &branches);
> +
> if (!porcelain)
> measure_widths(worktrees, &abbrev,
> &display, &path_maxwidth);
> @@ -1174,6 +1248,7 @@ static int list(int ac, const char **av, const char *prefix,
> free(display);
> free_worktrees(worktrees);
> }
> + string_list_clear(&branches, 0);
> return 0;
> }
>
> diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh
> index eeaf800d74..ea08bf79fa 100755
> --- a/t/t2402-worktree-list.sh
> +++ b/t/t2402-worktree-list.sh
> @@ -318,4 +318,135 @@ test_expect_success 'worktree path when called in .git directory' '
> test_cmp list1 list2
> '
>
> +test_expect_success 'setup for "list --branch"' '
> + git init by-branch &&
> + (
> + cd by-branch &&
> + test_commit base &&
> + git branch topic-a &&
> + git branch topic-b &&
> + git branch unused &&
> + git worktree add ../wt-a topic-a &&
> + git worktree add ../wt-b topic-b &&
> + git worktree add --detach ../wt-detached main &&
> + git -C ../wt-a commit --allow-empty -m a &&
> + git -C ../wt-b commit --allow-empty -m b &&
> + git merge --ff-only topic-a
> + )
> +'
> +
> +list_paths () {
> + git -C by-branch worktree list --porcelain "$@" >out &&
> + sed -n "s/^worktree //p" out >actual
> +}
> +
> +test_expect_success '"list --branch" shows the worktree of that branch' '
> + list_paths --branch=topic-a &&
> + echo "$(pwd)/wt-a" >expect &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success '"list --branch" can be repeated' '
> + list_paths --branch=topic-b --branch=main --branch=topic-a &&
> + cat >expect <<-EOF &&
> + $(pwd)/by-branch
> + $(pwd)/wt-a
> + $(pwd)/wt-b
> + EOF
> + test_cmp expect actual
> +'
> +
> +test_expect_success '"list --branch" with a branch that is not checked out' '
> + list_paths --branch=unused --branch=no-such-branch &&
> + test_must_be_empty actual
> +'
> +
> +test_expect_success '"list --branch" never shows a detached worktree' '
> + list_paths --branch=main &&
> + echo "$(pwd)/by-branch" >expect &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success '"list --branch" rejects an invalid branch name' '
> + test_must_fail git -C by-branch worktree list --branch=-bad 2>err &&
> + test_grep "is not a valid branch name" err
> +'
> +
> +test_expect_success '"list --branch" keeps the normal output format' '
> + git -C by-branch worktree list --branch=topic-a >out &&
> + sed "s/ */ /g" <out >actual &&
> + echo "$(pwd)/wt-a $(git -C by-branch rev-parse --short topic-a) [topic-a]" >expect &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success '"list --branch" works with -z' '
> + git -C by-branch worktree list --porcelain -z --branch=topic-b >out &&
> + tr "\0" "\n" <out >actual &&
> + cat >expect <<-EOF &&
> + worktree $(pwd)/wt-b
> + HEAD $(git -C by-branch rev-parse topic-b)
> + branch refs/heads/topic-b
> +
> + EOF
> + test_cmp expect actual
> +'
> +
> +test_expect_success '"list --branch" shows a worktree bisecting that branch' '
> + test_when_finished "git -C wt-b bisect reset && git -C wt-b reset --hard HEAD~2" &&
> + git -C wt-b commit --allow-empty -m b1 &&
> + git -C wt-b commit --allow-empty -m b2 &&
> + git -C wt-b bisect start topic-b topic-b~2 &&
> + test_must_fail git -C wt-b symbolic-ref -q HEAD &&
> + list_paths --branch=topic-b &&
> + echo "$(pwd)/wt-b" >expect &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success '"list --branch" shows a worktree rebasing that branch' '
> + test_when_finished "git -C wt-b rebase --abort" &&
> + test_must_fail git -C wt-b rebase -f -x false base &&
> + test_must_fail git -C wt-b symbolic-ref -q HEAD &&
> + list_paths --branch=topic-b &&
> + echo "$(pwd)/wt-b" >expect &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success '"list --branch" accepts a full branch name' '
> + list_paths --branch=refs/heads/topic-a &&
> + echo "$(pwd)/wt-a" >expect &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success '"list --branch" with a tag of the same name' '
> + test_when_finished "git -C by-branch tag -d topic-a" &&
> + git -C by-branch tag topic-a base &&
> + list_paths --branch=topic-a &&
> + echo "$(pwd)/wt-a" >expect &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success '"list --branch" shows unborn and prunable worktrees' '
> + test_when_finished "rm -rf wt-unborn wt-gone && git -C by-branch worktree prune" &&
> + git -C by-branch worktree add --orphan -b unborn ../wt-unborn &&
> + git -C by-branch branch gone &&
> + git -C by-branch worktree add ../wt-gone gone &&
> + rm -rf wt-gone &&
> + git -C by-branch worktree list -v --branch=unborn --branch=gone >out &&
> + test_grep "^$(pwd)/wt-unborn .*\[unborn\]$" out &&
> + test_grep "^$(pwd)/wt-gone .*\[gone\]$" out &&
> + test_grep "prunable: gitdir file points to non-existent location" out
> +'
> +
> +test_expect_success '"list --branch" with "git branch --merged"' '
> + git -C by-branch worktree list --porcelain \
> + $(git -C by-branch branch --format="--branch=%(refname)" \
> + --merged main) >out &&
> + sed -n "s/^worktree //p" out >actual &&
> + cat >expect <<-EOF &&
> + $(pwd)/by-branch
> + $(pwd)/wt-a
> + EOF
> + test_cmp expect actual
> +'
> +
> test_done
>
> base-commit: 7e34d8174dcda3435d1404c9a30bef46afe6c13e
^ permalink raw reply [flat|nested] 33+ messages in thread
* Re: [PATCH v2] worktree list: add --branch to select worktrees by branch
2026-10-25 14:47 ` Phillip Wood
@ 2026-10-26 11:02 ` Phillip Wood
2026-10-28 6:02 ` Eric Sunshine
1 sibling, 0 replies; 33+ messages in thread
From: Phillip Wood @ 2026-10-26 11:02 UTC (permalink / raw)
To: Jasper Mayone, git; +Cc: gitster, sunshine, haraldnordgren, ben.knoble
One further thought ...
On 25/10/2026 14:47, Phillip Wood wrote:
> On 24/10/2026 17:53, Jasper Mayone wrote:
>
>> For example, to list
>> the worktrees whose branch is merged into origin/main:
>>
>> git worktree list \
>> $(git branch --format='--branch=%(refname)' --merged origin/main)
>>
>> The --format is needed because the default output of "git branch"
>> marks the current branch with "*" and the branches checked out in
>> other worktrees with "+". It uses the full name because
>> %(refname:short) gives "heads/topic" when a tag is also called
>> "topic".
I think you can use "%(refname:lstrip=2)" to always remove "refs/heads/"
which means we could simplify the implementation and just accept branch
names.
Thanks
Phillip
^ permalink raw reply [flat|nested] 33+ messages in thread
* Re: [PATCH v2] worktree list: add --branch to select worktrees by branch
2026-10-25 14:47 ` Phillip Wood
2026-10-26 11:02 ` Phillip Wood
@ 2026-10-28 6:02 ` Eric Sunshine
2026-10-28 15:27 ` Junio C Hamano
1 sibling, 1 reply; 33+ messages in thread
From: Eric Sunshine @ 2026-10-28 6:02 UTC (permalink / raw)
To: phillip.wood; +Cc: Jasper Mayone, git, gitster, haraldnordgren, ben.knoble
On Sun, Oct 25, 2026 at 10:47 AM Phillip Wood <phillip.wood123@gmail.com> wrote:
> On 24/10/2026 17:53, Jasper Mayone wrote:
> > A worktree that is rebasing or bisecting a branch
> > counts as having it checked out, as it does for "git branch -d" and
> > "git worktree add". <branch> is a branch name, or a full name under
> > refs/heads/.
>
> That's useful because "git worktree list" does not show branch names
> when they're being rebased or bisected. The commit message should spell
> that out.
Aside from Philip's excellent review comments, I have a relatively
minor question:
Rather than naming it `--branch`, under the assumption that users may
eventually want to filter on other worktree properties, should this
new option be given a more generic name, such as
`--filter=branch:<name>`?
If so, that potentially raises the question about how such filter
options should combine. It's obvious that multiple
`--filter=branch:<name>` options combine via logical "OR", but a user
might want to combine future filter criteria via other logical
operations (AND, NOT, grouping). Do other Git commands set precedent
regarding filter option naming and logical operations?
Note that I'm not suggesting that this patch should implement such
additional filtering behavior (it should not), or even that all these
questions need to be answered right now. I'm asking only, taking the
above into consideration, whether this option should be given a more
generic name to avoid closing the door to those possible future
enhancements.
^ permalink raw reply [flat|nested] 33+ messages in thread
* Re: [PATCH v2] worktree list: add --branch to select worktrees by branch
2026-10-24 16:53 ` [PATCH v2] worktree list: add --branch to select worktrees by branch Jasper Mayone
2026-10-25 14:47 ` Phillip Wood
@ 2026-10-28 11:32 ` Kristoffer Haugsbakk
2026-10-29 4:46 ` Jasper Mayone
2026-10-30 14:21 ` [PATCH 0/2] worktree: treat a branch being rebased or bisected as checked out Jasper Mayone
2 siblings, 1 reply; 33+ messages in thread
From: Kristoffer Haugsbakk @ 2026-10-28 11:32 UTC (permalink / raw)
To: Jasper Mayone, git
Cc: Junio C Hamano, Eric Sunshine, Harald Nordgren, D. Ben Knoble
On Sat, Oct 24, 2026, at 18:53, Jasper Mayone wrote:
> There is no direct way to ask which worktree has a given branch
> checked out. "git worktree list --porcelain" prints the branch of
> every worktree, so a script has to parse the whole list and match the
> "branch" lines itself.
git-for-each-ref(1) (and git-branch(1) etc.) have `worktreepath`.
git worktree add ../test-branch
git worktree remove $(git for-each-ref \
--format='%(worktreepath)' refs/heads/test-branch)
git branch -d test-branch
It will not list the worktree path if that branch is being rebased.
>
> Teach "git worktree list" a "--branch=<branch>" option that lists
> only the worktrees that have <branch> checked out. It can be given
> more than once. A worktree that is rebasing or bisecting a branch
> counts as having it checked out, as it does for "git branch -d" and
> "git worktree add". <branch> is a branch name, or a full name under
> refs/heads/.
>
> This keeps the choice of branches in "git branch", which already has
> --merged, --contains, --points-at and patterns. For example, to list
> the worktrees whose branch is merged into origin/main:
>
> git worktree list \
> $(git branch --format='--branch=%(refname)' --merged origin/main)
>
> The --format is needed because the default output of "git branch"
> marks the current branch with "*" and the branches checked out in
> other worktrees with "+". It uses the full name because
> %(refname:short) gives "heads/topic" when a tag is also called
> "topic".
>
> Each worktree's branch is found once and looked up in a set of the
> requested branches, so feeding it every branch of a large repository
> stays cheap.
>
> A detached worktree has no branch, so it is never listed.
>
> Suggested-by: Junio C Hamano <gitster@pobox.com>
> Signed-off-by: Jasper Mayone <me@jaspermayone.com>
> ---
>[snip]
^ permalink raw reply [flat|nested] 33+ messages in thread
* Re: [PATCH v2] worktree list: add --branch to select worktrees by branch
2026-10-28 6:02 ` Eric Sunshine
@ 2026-10-28 15:27 ` Junio C Hamano
0 siblings, 0 replies; 33+ messages in thread
From: Junio C Hamano @ 2026-10-28 15:27 UTC (permalink / raw)
To: Eric Sunshine
Cc: phillip.wood, Jasper Mayone, git, haraldnordgren, ben.knoble
Eric Sunshine <sunshine@sunshineco.com> writes:
> If so, that potentially raises the question about how such filter
> options should combine. It's obvious that multiple
> `--filter=branch:<name>` options combine via logical "OR", but a user
> might want to combine future filter criteria via other logical
> operations (AND, NOT, grouping). Do other Git commands set precedent
> regarding filter option naming and logical operations?
>
> Note that I'm not suggesting that this patch should implement such
> additional filtering behavior (it should not), or even that all these
> questions need to be answered right now. I'm asking only, taking the
> above into consideration, whether this option should be given a more
> generic name to avoid closing the door to those possible future
> enhancements.
Perhaps, but at that point I suspect that the most general form is
to allow you to write a callback function that is given the path to
the worktree and determines if you want the worktree to be included
in the result. Then
git worktree list --filter-command='
test 5 -lt $(git rev-list --count master..HEAD)
'
may become a way to list worktrees with work that has more than 5
commits to be massaged down to 'master', for example.
"git branch --contains" etc. can be expressed inside the framework,
so "--branch" may become a special case for that more generalized
mechanism.
I dunno.
^ permalink raw reply [flat|nested] 33+ messages in thread
* Re: [PATCH v2] worktree list: add --branch to select worktrees by branch
2026-10-28 11:32 ` Kristoffer Haugsbakk
@ 2026-10-29 4:46 ` Jasper Mayone
2026-10-29 5:01 ` Eric Sunshine
2026-10-29 11:14 ` Phillip Wood
0 siblings, 2 replies; 33+ messages in thread
From: Jasper Mayone @ 2026-10-29 4:46 UTC (permalink / raw)
To: kristofferhaugsbakk
Cc: git, gitster, sunshine, phillip.wood123, haraldnordgren,
ben.knoble, Jasper Mayone
"Kristoffer Haugsbakk" <kristofferhaugsbakk@fastmail.com> writes:
> git-for-each-ref(1) (and git-branch(1) etc.) have `worktreepath`.
>
> git worktree add ../test-branch
> git worktree remove $(git for-each-ref \
> --format='%(worktreepath)' refs/heads/test-branch)
> git branch -d test-branch
>
> It will not list the worktree path if that branch is being rebased.
Thanks, I had missed %(worktreepath). With it, what I wanted from the
start is already one command:
git for-each-ref --merged origin/main \
--format='%(worktreepath)' refs/heads/
So --branch adds little on top of that. The one real gap is the one
you name. While a branch is being rebased or bisected,
%(worktreepath) is empty, "git branch" does not mark it with "+", and
"git worktree list" shows that worktree as "(detached HEAD)". Yet
"git worktree add" and "git branch -d" already treat that branch as
checked out.
As Phillip pointed out, branch.c already has this information:
branch_checked_out() covers a plain checkout, a rebase, a bisect and
"rebase --update-refs".
So I would like to drop --branch and instead make %(worktreepath) use
the same information, so that it names the worktree of a branch that
is being rebased or bisected. That is a smaller change with no new
option, so the naming question Eric raised and the --filter-command
idea Junio floated do not come up.
Is that change in behavior acceptable? A script that reads
%(worktreepath) would now get a path for a branch that is being
rebased. I think that is a fix, since the rest of Git already
considers that branch checked out, but I would like to hear if anyone
relies on the empty value.
Phillip, thank you for the review. Your point that "git worktree
list" output does not say which entry is the main worktree still
stands, and I can send that as a separate patch if it is wanted.
Thanks,
Jasper
^ permalink raw reply [flat|nested] 33+ messages in thread
* Re: [PATCH v2] worktree list: add --branch to select worktrees by branch
2026-10-29 4:46 ` Jasper Mayone
@ 2026-10-29 5:01 ` Eric Sunshine
2026-10-29 11:14 ` Phillip Wood
1 sibling, 0 replies; 33+ messages in thread
From: Eric Sunshine @ 2026-10-29 5:01 UTC (permalink / raw)
To: Jasper Mayone
Cc: kristofferhaugsbakk, git, gitster, phillip.wood123,
haraldnordgren, ben.knoble
On Thu, Oct 29, 2026 at 12:46 AM Jasper Mayone <me@jaspermayone.com> wrote:
> Phillip, thank you for the review. Your point that "git worktree
> list" output does not say which entry is the main worktree still
> stands, and I can send that as a separate patch if it is wanted.
`git worktree list` guarantees that the main worktree (or bare
repository) is the very first item; this behavior is documented. I
think Philip's point was that once filtering is engaged, you lose that
guarantee: in a filtered list, if the main worktree has been filtered
away, then the first item will not be the main worktree.
^ permalink raw reply [flat|nested] 33+ messages in thread
* Re: [PATCH v2] worktree list: add --branch to select worktrees by branch
2026-10-29 4:46 ` Jasper Mayone
2026-10-29 5:01 ` Eric Sunshine
@ 2026-10-29 11:14 ` Phillip Wood
2026-10-29 14:23 ` Ben Knoble
1 sibling, 1 reply; 33+ messages in thread
From: Phillip Wood @ 2026-10-29 11:14 UTC (permalink / raw)
To: Jasper Mayone, kristofferhaugsbakk
Cc: git, gitster, sunshine, haraldnordgren, ben.knoble
Hi Jasper
On 29/10/2026 04:46, Jasper Mayone wrote:
>
> So I would like to drop --branch and instead make %(worktreepath) use
> the same information, so that it names the worktree of a branch that
> is being rebased or bisected. That is a smaller change with no new
> option, so the naming question Eric raised and the --filter-command
> idea Junio floated do not come up.
That would certainly be a useful improvement, there are a few
differences compared to adding something to "git worktree list" though
- "git for-each-ref" does not have an option for NUL terminated output
and %(worktreepath) may contain a newline. If is possible to work
round that using one of the quoting options, though it is a pain to
use those if you're not using one of the supported languages.
- there is no way to know from the output whether deleting a worktree
will delete the whole repository.
- I'm not sure that it handles branches that are checked out in more
than one worktree. For warning that a branch is in use somewhere that
does not matter, but if you want to track down all the places it is
checked out it does.
> Is that change in behavior acceptable? A script that reads
> %(worktreepath) would now get a path for a branch that is being
> rebased. I think that is a fix, since the rest of Git already
> considers that branch checked out, but I would like to hear if anyone
> relies on the empty value.
I think we can consider the change in behavior to be a bug fix.
> Phillip, thank you for the review. Your point that "git worktree
> list" output does not say which entry is the main worktree still
> stands, and I can send that as a separate patch if it is wanted.
As Eric said, we only need to mark the main worktree if we're filtering
the list of worktrees, otherwise it is always the first one listed.
Thanks
Phillip
^ permalink raw reply [flat|nested] 33+ messages in thread
* Re: [PATCH v2] worktree list: add --branch to select worktrees by branch
2026-10-29 11:14 ` Phillip Wood
@ 2026-10-29 14:23 ` Ben Knoble
2026-10-29 16:10 ` Phillip Wood
0 siblings, 1 reply; 33+ messages in thread
From: Ben Knoble @ 2026-10-29 14:23 UTC (permalink / raw)
To: phillip.wood
Cc: Jasper Mayone, kristofferhaugsbakk, git, gitster, sunshine,
haraldnordgren
> Le 29 oct. 2026 à 07:14, Phillip Wood <phillip.wood123@gmail.com> a écrit :
>
> Hi Jasper
>
>> On 29/10/2026 04:46, Jasper Mayone wrote:
>
>> Is that change in behavior acceptable? A script that reads
>> %(worktreepath) would now get a path for a branch that is being
>> rebased. I think that is a fix, since the rest of Git already
>> considers that branch checked out, but I would like to hear if anyone
>> relies on the empty value.
>
> I think we can consider the change in behavior to be a bug fix.
If we’re doing this in order to usefully feed it to worktree-remove, though,
do you really want to remove a worktree that is mid-rebase? I
would think that could have work you don’t want to throw away
without considering. (Besides which, would it show up
in --merged=origin? Hm.)
I suppose I would expect remove to fail without a « force » flag if
the worktree is mid-rebase, but still.
^ permalink raw reply [flat|nested] 33+ messages in thread
* Re: [PATCH v2] worktree list: add --branch to select worktrees by branch
2026-10-29 14:23 ` Ben Knoble
@ 2026-10-29 16:10 ` Phillip Wood
0 siblings, 0 replies; 33+ messages in thread
From: Phillip Wood @ 2026-10-29 16:10 UTC (permalink / raw)
To: Ben Knoble, phillip.wood
Cc: Jasper Mayone, kristofferhaugsbakk, git, gitster, sunshine,
haraldnordgren
On 29/10/2026 14:23, Ben Knoble wrote:
>
>> Le 29 oct. 2026 à 07:14, Phillip Wood <phillip.wood123@gmail.com> a écrit :
>>
>> Hi Jasper
>>
>>> On 29/10/2026 04:46, Jasper Mayone wrote:
>>
>>> Is that change in behavior acceptable? A script that reads
>>> %(worktreepath) would now get a path for a branch that is being
>>> rebased. I think that is a fix, since the rest of Git already
>>> considers that branch checked out, but I would like to hear if anyone
>>> relies on the empty value.
>>
>> I think we can consider the change in behavior to be a bug fix.
>
> If we’re doing this in order to usefully feed it to worktree-remove, though,
> do you really want to remove a worktree that is mid-rebase? I
> would think that could have work you don’t want to throw away
> without considering. (Besides which, would it show up
> in --merged=origin? Hm.)
>
> I suppose I would expect remove to fail without a « force » flag if
> the worktree is mid-rebase, but still.
That's an interesting point. I agree "git worktree remove" shouldn't
remove a worktree that's in the middle of a rebase or bisect. To me that
is a bug in "git worktree remove" that is more likely to be triggered if
we start reporting the worktree where branches are being rebased.
Thanks
Phillip
^ permalink raw reply [flat|nested] 33+ messages in thread
* [PATCH 0/2] worktree: treat a branch being rebased or bisected as checked out
2026-10-24 16:53 ` [PATCH v2] worktree list: add --branch to select worktrees by branch Jasper Mayone
2026-10-25 14:47 ` Phillip Wood
2026-10-28 11:32 ` Kristoffer Haugsbakk
@ 2026-10-30 14:21 ` Jasper Mayone
2026-10-30 14:21 ` [PATCH 1/2] worktree remove: refuse a worktree in the middle of a rebase or bisect Jasper Mayone
` (3 more replies)
2 siblings, 4 replies; 33+ messages in thread
From: Jasper Mayone @ 2026-10-30 14:21 UTC (permalink / raw)
To: git
Cc: gitster, phillip.wood123, sunshine, kristofferhaugsbakk,
ben.knoble, haraldnordgren, Jasper Mayone
This replaces my "git worktree list --branch" patch [1]. In that
thread Kristoffer pointed out that "git for-each-ref --merged <commit>
--format='%(worktreepath)' refs/heads/" already finds the worktrees of
merged branches, so a new option is not needed. What is missing is
that %(worktreepath) ignores a branch while a worktree is rebasing or
bisecting it, although "git worktree add" and "git branch -d" already
treat that branch as checked out. Phillip agreed that this is a bug.
Ben then asked what happens if such a worktree is passed to "git
worktree remove". Today it is removed, and the rebase or bisect state
is lost with it, if the files happen to be clean. Phillip called that
a bug too, and more likely to be hit once %(worktreepath) reports
these worktrees, so it is fixed first.
1/2 makes "git worktree remove" refuse a worktree in the middle of a
rebase, "git am", bisect, or cherry-pick or revert sequence,
unless --force is given.
2/2 makes %(worktreepath), and so the "+" marker of "git branch",
name the worktree that is rebasing or bisecting a branch, as
prepare_checked_out_branches() in branch.c does. Branches that
"rebase --update-refs" will move are not included.
Two things reviewers may want to decide differently:
* In the worktree that is doing the rebase, "git branch" now shows
both "* (no branch, rebasing topic)" and "+ topic". I documented
that rather than special-case the current worktree in the format
that "git branch" builds, because %(worktreepath) itself should not
depend on where it is run. If you would rather hide the "+" there,
I can do that in "git branch" instead.
* A merge stopped with --no-commit is not checked by 1/2. Its staged
changes already make "git status" report the worktree as unclean in
the common case.
[1] https://lore.kernel.org/git/20261024165311.57387-1-me@jaspermayone.com/
Jasper Mayone (2):
worktree remove: refuse a worktree in the middle of a rebase or bisect
ref-filter: name the worktree of a branch being rebased or bisected
Documentation/git-branch.adoc | 5 ++-
Documentation/git-for-each-ref.adoc | 4 +-
Documentation/git-worktree.adoc | 11 +++--
builtin/worktree.c | 31 +++++++++++++-
ref-filter.c | 60 +++++++++++++++++++++------
t/t2403-worktree-move.sh | 59 +++++++++++++++++++++++++++
t/t3203-branch-output.sh | 35 ++++++++++++++++
t/t6302-for-each-ref-filter.sh | 63 +++++++++++++++++++++++++++++
8 files changed, 249 insertions(+), 19 deletions(-)
base-commit: b9e5cff1803ab24582201ef059516eb16014b9d9
--
2.56.0
^ permalink raw reply [flat|nested] 33+ messages in thread
* [PATCH 1/2] worktree remove: refuse a worktree in the middle of a rebase or bisect
2026-10-30 14:21 ` [PATCH 0/2] worktree: treat a branch being rebased or bisected as checked out Jasper Mayone
@ 2026-10-30 14:21 ` Jasper Mayone
2026-11-02 14:07 ` Phillip Wood
2026-10-30 14:21 ` [PATCH 2/2] ref-filter: name the worktree of a branch being rebased or bisected Jasper Mayone
` (2 subsequent siblings)
3 siblings, 1 reply; 33+ messages in thread
From: Jasper Mayone @ 2026-10-30 14:21 UTC (permalink / raw)
To: git
Cc: gitster, phillip.wood123, sunshine, kristofferhaugsbakk,
ben.knoble, haraldnordgren, Jasper Mayone
"git worktree remove" refuses a worktree with modified or untracked
files unless --force is given, but it removes a worktree that is in
the middle of a rebase, a "git am" session, a bisect or a cherry-pick
or revert sequence when its files happen to be clean. The state of
that operation lives in the worktree's own $GIT_DIR, so it is lost
with the worktree. A rebase stopped at an "edit" or "break" line, an
"exec" that failed, or a cherry-pick sequence after a conflict was
resolved and committed is clean in this sense and still has work in
progress.
Treat such a worktree as unclean: refuse to remove it unless --force
is given, and name the operation in the message.
A worktree whose directory is already gone is still removed without
--force, as before: the check is part of the check for a clean
worktree, which is only done when the directory exists.
Reported-by: D. Ben Knoble <ben.knoble@gmail.com>
Helped-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Jasper Mayone <me@jaspermayone.com>
---
Documentation/git-worktree.adoc | 11 +++---
builtin/worktree.c | 31 ++++++++++++++++-
t/t2403-worktree-move.sh | 59 +++++++++++++++++++++++++++++++++
3 files changed, 96 insertions(+), 5 deletions(-)
diff --git a/Documentation/git-worktree.adoc b/Documentation/git-worktree.adoc
index e6e77252ab..da4c38f909 100644
--- a/Documentation/git-worktree.adoc
+++ b/Documentation/git-worktree.adoc
@@ -142,9 +142,10 @@ the new working tree location.
`remove`::
Remove a worktree. Only clean worktrees (no untracked files and no
-modification in tracked files) can be removed. Unclean worktrees or ones
-with submodules can be removed with `--force`. The main worktree cannot be
-removed.
+modification in tracked files) can be removed. A worktree in the middle
+of a rebase, a `git am` session, a bisect or a cherry-pick or revert
+sequence is not clean either, because removing it loses that state. Unclean worktrees or ones with submodules
+can be removed with `--force`. The main worktree cannot be removed.
`repair [<path>...]`::
@@ -189,7 +190,9 @@ twice. If the destination is already assigned to some other worktree but is
missing (for instance, if _<new-path>_ was deleted manually), then `--force`
allows the move to proceed; use `--force` twice if the destination is locked.
+
-`remove` refuses to remove an unclean worktree unless `--force` is used.
+`remove` refuses to remove an unclean worktree, or one in the middle of a
+rebase, `git am` session, bisect, or cherry-pick or revert sequence,
+unless `--force` is used.
To remove a locked worktree, specify `--force` twice.
`-b <new-branch>`::
diff --git a/builtin/worktree.c b/builtin/worktree.c
index 77ecd0f71f..ed08de1f43 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -26,6 +26,7 @@
#include "submodule.h"
#include "utf8.h"
#include "worktree.h"
+#include "wt-status.h"
#include "quote.h"
#define BUILTIN_WORKTREE_ADD_USAGE \
@@ -1408,6 +1409,32 @@ static void check_clean_worktree(struct worktree *wt,
original_path, ret);
}
+/*
+ * A rebase, "git am", bisect or cherry-pick/revert sequence keeps its
+ * state in the worktree's own $GIT_DIR, and removing the worktree
+ * throws that away even when "git status" says the files are clean.
+ */
+static void check_no_operation_in_progress(struct worktree *wt,
+ const char *original_path)
+{
+ struct wt_status_state state = { 0 };
+
+ if (wt_status_check_rebase(wt, &state)) {
+ if (state.am_in_progress)
+ die(_("'%s' is in the middle of an am session, use --force to delete it"),
+ original_path);
+ die(_("'%s' is in the middle of a rebase, use --force to delete it"),
+ original_path);
+ }
+ if (wt_status_check_bisect(wt, &state))
+ die(_("'%s' is in the middle of a bisect, use --force to delete it"),
+ original_path);
+ wt_status_state_free_buffers(&state);
+ if (is_directory(worktree_git_path(wt, "sequencer")))
+ die(_("'%s' is in the middle of a cherry-pick or revert, use --force to delete it"),
+ original_path);
+}
+
static int delete_git_work_tree(struct worktree *wt)
{
struct strbuf sb = STRBUF_INIT;
@@ -1461,8 +1488,10 @@ static int remove_worktree(int ac, const char **av, const char *prefix,
strbuf_release(&errmsg);
if (file_exists(wt->path)) {
- if (!force)
+ if (!force) {
+ check_no_operation_in_progress(wt, av[0]);
check_clean_worktree(wt, av[0]);
+ }
ret |= delete_git_work_tree(wt);
}
diff --git a/t/t2403-worktree-move.sh b/t/t2403-worktree-move.sh
index 69768c1207..4eb56a8911 100755
--- a/t/t2403-worktree-move.sh
+++ b/t/t2403-worktree-move.sh
@@ -271,4 +271,63 @@ test_expect_success 'move worktree with relative path to absolute path' '
test_cmp expect .git/worktrees/absolute/gitdir
'
+test_expect_success 'remove worktree in the middle of a rebase' '
+ test_when_finished "rm -rf mid-rebase; git worktree prune" &&
+ git worktree add --detach mid-rebase init &&
+ test_commit -C mid-rebase rebase-me &&
+ test_must_fail git -C mid-rebase rebase -f -x false init &&
+ test_must_fail git worktree remove mid-rebase 2>err &&
+ test_grep "in the middle of a rebase" err &&
+ test_path_is_dir mid-rebase &&
+ git worktree remove --force mid-rebase &&
+ test_path_is_missing mid-rebase
+'
+
+test_expect_success 'remove worktree in the middle of an am session' '
+ test_when_finished "rm -rf mid-am patch; git worktree prune" &&
+ git worktree add --detach mid-am init &&
+ test_commit -C mid-am am-me &&
+ git -C mid-am format-patch -1 --stdout >patch &&
+ git -C mid-am reset --hard init &&
+ echo conflict >mid-am/am-me.t &&
+ git -C mid-am add am-me.t &&
+ git -C mid-am commit -m conflict &&
+ test_must_fail git -C mid-am am ../patch &&
+ git -C mid-am reset --hard &&
+ test_must_fail git worktree remove mid-am 2>err &&
+ test_grep "in the middle of an am session" err &&
+ git worktree remove --force mid-am
+'
+
+test_expect_success 'remove worktree in the middle of a bisect' '
+ test_when_finished "rm -rf mid-bisect; git worktree prune" &&
+ git worktree add --detach mid-bisect init &&
+ test_commit -C mid-bisect bisect-one &&
+ test_commit -C mid-bisect bisect-two &&
+ git -C mid-bisect bisect start HEAD init &&
+ test_must_fail git worktree remove mid-bisect 2>err &&
+ test_grep "in the middle of a bisect" err &&
+ git worktree remove --force mid-bisect
+'
+
+test_expect_success 'remove worktree in the middle of a cherry-pick sequence' '
+ test_when_finished "rm -rf mid-pick; git worktree prune" &&
+ git worktree add --detach mid-pick init &&
+ git -C mid-pick switch -c pick-src &&
+ test_commit -C mid-pick pick-one &&
+ test_commit -C mid-pick pick-two &&
+ git -C mid-pick switch --detach init &&
+ echo conflict >mid-pick/pick-one.t &&
+ git -C mid-pick add pick-one.t &&
+ git -C mid-pick commit -m conflict &&
+ test_must_fail git -C mid-pick cherry-pick pick-src~1 pick-src &&
+ git -C mid-pick checkout --theirs pick-one.t &&
+ git -C mid-pick add pick-one.t &&
+ git -C mid-pick commit --no-edit &&
+ test_path_is_dir .git/worktrees/mid-pick/sequencer &&
+ test_must_fail git worktree remove mid-pick 2>err &&
+ test_grep "in the middle of a cherry-pick or revert" err &&
+ git worktree remove --force mid-pick
+'
+
test_done
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH 2/2] ref-filter: name the worktree of a branch being rebased or bisected
2026-10-30 14:21 ` [PATCH 0/2] worktree: treat a branch being rebased or bisected as checked out Jasper Mayone
2026-10-30 14:21 ` [PATCH 1/2] worktree remove: refuse a worktree in the middle of a rebase or bisect Jasper Mayone
@ 2026-10-30 14:21 ` Jasper Mayone
2026-11-02 14:06 ` Phillip Wood
2026-10-30 22:55 ` [PATCH 0/2] worktree: treat a branch being rebased or bisected as checked out Junio C Hamano
2026-11-02 18:29 ` [PATCH v4 0/4] " Jasper Mayone
3 siblings, 1 reply; 33+ messages in thread
From: Jasper Mayone @ 2026-10-30 14:21 UTC (permalink / raw)
To: git
Cc: gitster, phillip.wood123, sunshine, kristofferhaugsbakk,
ben.knoble, haraldnordgren, Jasper Mayone
%(worktreepath) gives the worktree that has a branch checked out, and
"git branch" uses it to mark such branches with "+". It only looks at
the HEAD of each worktree, so while a worktree is rebasing or
bisecting a branch, its HEAD is detached and the branch shows no
worktree at all.
The rest of Git already treats that branch as checked out: "git
worktree add" and "git branch -d" refuse it, because finishing the
rebase or bisect will check it out again. "git worktree list" shows
such a worktree as "(detached HEAD)", so %(worktreepath) is the only
place a script could find out which worktree is working on the
branch.
Also map the branch that a worktree is rebasing or bisecting to that
worktree, as prepare_checked_out_branches() in branch.c does.
Branches that "rebase --update-refs" will update are not mapped: that
worktree has not checked them out, it only moves them when the rebase
is done.
As a result "git branch" marks such a branch with "+", also when it
runs in the worktree that is rebasing or bisecting it, where it shows
both "* (no branch, rebasing topic)" and "+ topic". Document that.
Reported-by: Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com>
Helped-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Jasper Mayone <me@jaspermayone.com>
---
Documentation/git-branch.adoc | 5 ++-
Documentation/git-for-each-ref.adoc | 4 +-
ref-filter.c | 60 +++++++++++++++++++++------
t/t3203-branch-output.sh | 35 ++++++++++++++++
t/t6302-for-each-ref-filter.sh | 63 +++++++++++++++++++++++++++++
5 files changed, 153 insertions(+), 14 deletions(-)
diff --git a/Documentation/git-branch.adoc b/Documentation/git-branch.adoc
index bfdf459329..d499d7e0d4 100644
--- a/Documentation/git-branch.adoc
+++ b/Documentation/git-branch.adoc
@@ -33,7 +33,10 @@ DESCRIPTION
If `--list` is given, or if there are no non-option arguments, existing
branches are listed; the current branch will be highlighted in green and
marked with an asterisk. Any branches checked out in linked worktrees will
-be highlighted in cyan and marked with a plus sign. Option `-r` causes the
+be highlighted in cyan and marked with a plus sign. A branch that a
+worktree is rebasing or bisecting counts as checked out there, so it is
+marked in the same way, also in the worktree that is rebasing or
+bisecting it. Option `-r` causes the
remote-tracking branches to be listed,
and option `-a` shows both local and remote branches.
diff --git a/Documentation/git-for-each-ref.adoc b/Documentation/git-for-each-ref.adoc
index a7b31e8aaf..90ff37259e 100644
--- a/Documentation/git-for-each-ref.adoc
+++ b/Documentation/git-for-each-ref.adoc
@@ -182,7 +182,9 @@ but if used together the last option is selected.
`worktreepath`::
The absolute path to the worktree in which the ref is checked
- out, if it is checked out in any linked worktree. Empty string
+ out, if it is checked out in any linked worktree. A branch that
+ a worktree is rebasing or bisecting counts as checked out there,
+ although that worktree's `HEAD` is detached. Empty string
otherwise.
`ahead-behind:<commit-ish>`::
diff --git a/ref-filter.c b/ref-filter.c
index bdf54f6f59..1aebea243e 100644
--- a/ref-filter.c
+++ b/ref-filter.c
@@ -97,7 +97,8 @@ static struct expand_data {
struct ref_to_worktree_entry {
struct hashmap_entry ent;
- struct worktree *wt; /* key is wt->head_ref */
+ struct worktree *wt;
+ char refname[FLEX_ARRAY]; /* the key */
};
static int ref_to_worktree_map_cmpfnc(const void *lookupdata UNUSED,
@@ -110,8 +111,8 @@ static int ref_to_worktree_map_cmpfnc(const void *lookupdata UNUSED,
e = container_of(eptr, const struct ref_to_worktree_entry, ent);
k = container_of(kptr, const struct ref_to_worktree_entry, ent);
- return strcmp(e->wt->head_ref,
- keydata_aka_refname ? keydata_aka_refname : k->wt->head_ref);
+ return strcmp(e->refname,
+ keydata_aka_refname ? keydata_aka_refname : k->refname);
}
static struct ref_to_worktree_map {
@@ -2380,21 +2381,56 @@ static int get_object(struct ref_array_item *ref, int deref,
return ret;
}
+static void add_worktree_entry(struct hashmap *map, struct worktree *wt,
+ const char *refname)
+{
+ struct ref_to_worktree_entry *entry;
+
+ FLEX_ALLOC_STR(entry, refname, refname);
+ entry->wt = wt;
+ hashmap_entry_init(&entry->ent, strhash(refname));
+ hashmap_add(map, &entry->ent);
+}
+
+/*
+ * Map each branch to the worktree that has it checked out. A worktree
+ * that is rebasing or bisecting a branch has it checked out, too,
+ * even though its HEAD may be detached, as for
+ * prepare_checked_out_branches() in branch.c.
+ */
static void populate_worktree_map(struct hashmap *map, struct worktree **worktrees)
{
- int i;
+ struct strbuf ref = STRBUF_INIT;
- for (i = 0; worktrees[i]; i++) {
- if (worktrees[i]->head_ref) {
- struct ref_to_worktree_entry *entry;
- entry = xmalloc(sizeof(*entry));
- entry->wt = worktrees[i];
- hashmap_entry_init(&entry->ent,
- strhash(worktrees[i]->head_ref));
+ for (size_t i = 0; worktrees[i]; i++) {
+ struct worktree *wt = worktrees[i];
+ struct wt_status_state state = { 0 };
- hashmap_add(map, &entry->ent);
+ if (wt->is_bare)
+ continue;
+
+ if (wt->head_ref)
+ add_worktree_entry(map, wt, wt->head_ref);
+
+ if (wt_status_check_rebase(wt, &state) &&
+ (state.rebase_in_progress ||
+ state.rebase_interactive_in_progress) &&
+ state.branch) {
+ strbuf_reset(&ref);
+ strbuf_addf(&ref, "refs/heads/%s", state.branch);
+ add_worktree_entry(map, wt, ref.buf);
}
+ wt_status_state_free_buffers(&state);
+
+ if (wt_status_check_bisect(wt, &state) &&
+ state.bisecting_from) {
+ strbuf_reset(&ref);
+ strbuf_addf(&ref, "refs/heads/%s", state.bisecting_from);
+ add_worktree_entry(map, wt, ref.buf);
+ }
+ wt_status_state_free_buffers(&state);
}
+ strbuf_release(&ref);
}
static void lazy_init_worktree_map(void)
diff --git a/t/t3203-branch-output.sh b/t/t3203-branch-output.sh
index 7c44186341..e04599392d 100755
--- a/t/t3203-branch-output.sh
+++ b/t/t3203-branch-output.sh
@@ -442,6 +442,41 @@ test_expect_success 'worktree colors correct' '
test_cmp expect actual
'
+test_expect_success 'branch being rebased in a worktree is marked' '
+ test_when_finished "git -C rebase_dir rebase --abort; rm -rf rebase_dir; git worktree prune; git branch -D rebasing" &&
+ git worktree add -b rebasing rebase_dir branch-two &&
+ git -C rebase_dir commit --allow-empty -m rebase-me &&
+ test_must_fail git -C rebase_dir rebase -f -x false HEAD~1 &&
+ test_must_fail git -C rebase_dir symbolic-ref -q HEAD &&
+ git branch --list rebasing >actual &&
+ echo "+ rebasing" >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success 'branch being rebased is marked in its own worktree' '
+ test_when_finished "git -C rebase_dir rebase --abort; rm -rf rebase_dir; git worktree prune; git branch -D rebasing" &&
+ git worktree add -b rebasing rebase_dir branch-two &&
+ git -C rebase_dir commit --allow-empty -m rebase-me &&
+ test_must_fail git -C rebase_dir rebase -f -x false HEAD~1 &&
+ git -C rebase_dir branch --list rebasing >actual &&
+ echo "+ rebasing" >expect &&
+ test_cmp expect actual &&
+ git -C rebase_dir branch >actual &&
+ test_grep "^\* (no branch, rebasing rebasing)" actual
+'
+
+test_expect_success 'branch being bisected in a worktree is marked' '
+ test_when_finished "git -C bisect_dir bisect reset; rm -rf bisect_dir; git worktree prune; git branch -D bisecting" &&
+ git worktree add -b bisecting bisect_dir branch-two &&
+ git -C bisect_dir commit --allow-empty -m bisect-one &&
+ git -C bisect_dir commit --allow-empty -m bisect-two &&
+ git -C bisect_dir bisect start HEAD HEAD~2 &&
+ test_must_fail git -C bisect_dir symbolic-ref -q HEAD &&
+ git branch --list bisecting >actual &&
+ echo "+ bisecting" >expect &&
+ test_cmp expect actual
+'
+
test_expect_success "set up color tests" '
echo "<RED>main<RESET>" >expect.color &&
echo "main" >expect.bare &&
diff --git a/t/t6302-for-each-ref-filter.sh b/t/t6302-for-each-ref-filter.sh
index 7f060d97bf..d656901f21 100755
--- a/t/t6302-for-each-ref-filter.sh
+++ b/t/t6302-for-each-ref-filter.sh
@@ -3,6 +3,7 @@
test_description='test for-each-refs usage of ref-filter APIs'
. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-rebase.sh
. "$TEST_DIRECTORY"/lib-gpg.sh
test_expect_success 'setup some history and refs' '
@@ -541,6 +542,68 @@ test_expect_success 'validate worktree atom' '
test_cmp expect actual
'
+test_expect_success 'worktree atom names a worktree rebasing the branch' '
+ test_when_finished "git -C rebasing rebase --abort; rm -rf rebasing; git worktree prune; git branch -D rebasing" &&
+ git worktree add -b rebasing rebasing main &&
+ git -C rebasing commit --allow-empty -m rebase-me &&
+ test_must_fail git -C rebasing rebase -f -x false main &&
+ test_must_fail git -C rebasing symbolic-ref -q HEAD &&
+ echo "$(pwd)/rebasing" >expect &&
+ git for-each-ref --format="%(worktreepath)" refs/heads/rebasing >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'worktree atom names a worktree bisecting the branch' '
+ test_when_finished "git -C bisecting bisect reset; rm -rf bisecting; git worktree prune; git branch -D bisecting" &&
+ git worktree add -b bisecting bisecting main &&
+ git -C bisecting commit --allow-empty -m bisect-one &&
+ git -C bisecting commit --allow-empty -m bisect-two &&
+ git -C bisecting bisect start HEAD main &&
+ test_must_fail git -C bisecting symbolic-ref -q HEAD &&
+ echo "$(pwd)/bisecting" >expect &&
+ git for-each-ref --format="%(worktreepath)" refs/heads/bisecting >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'worktree atom with an interactive rebase stopped at edit' '
+ test_when_finished "git -C editing rebase --abort; rm -rf editing; git worktree prune; git branch -D editing" &&
+ git worktree add -b editing editing main &&
+ git -C editing commit --allow-empty -m edit-me &&
+ (
+ set_fake_editor &&
+ FAKE_LINES="edit 1" git -C editing rebase -i --keep-empty HEAD~1
+ ) &&
+ test_must_fail git -C editing symbolic-ref -q HEAD &&
+ echo "$(pwd)/editing" >expect &&
+ git for-each-ref --format="%(worktreepath)" refs/heads/editing >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'worktree atom ignores branches rebase --update-refs will move' '
+ test_when_finished "git -C updating rebase --abort; rm -rf updating; git worktree prune; git branch -D updating mid" &&
+ git worktree add -b updating updating main &&
+ git -C updating commit --allow-empty -m one &&
+ git -C updating branch mid &&
+ git -C updating commit --allow-empty -m two &&
+ test_must_fail git -C updating rebase -f --update-refs -x false main &&
+ git for-each-ref --format="%(refname:short) [%(worktreepath)]" \
+ refs/heads/updating refs/heads/mid >actual &&
+ cat >expect <<-EOF &&
+ mid []
+ updating [$(pwd)/updating]
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'worktree atom ignores a rebase of a detached HEAD' '
+ test_when_finished "git -C detached-rebase rebase --abort; rm -rf detached-rebase; git worktree prune" &&
+ git worktree add --detach detached-rebase main &&
+ git -C detached-rebase commit --allow-empty -m detached &&
+ test_must_fail git -C detached-rebase rebase -f -x false main &&
+ git for-each-ref --format="%(worktreepath)" refs/heads/ >actual &&
+ test_grep ! detached-rebase actual
+'
+
test_expect_success 'start after with empty value' '
cat >expect <<-\EOF &&
refs/heads/main
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* Re: [PATCH 0/2] worktree: treat a branch being rebased or bisected as checked out
2026-10-30 14:21 ` [PATCH 0/2] worktree: treat a branch being rebased or bisected as checked out Jasper Mayone
2026-10-30 14:21 ` [PATCH 1/2] worktree remove: refuse a worktree in the middle of a rebase or bisect Jasper Mayone
2026-10-30 14:21 ` [PATCH 2/2] ref-filter: name the worktree of a branch being rebased or bisected Jasper Mayone
@ 2026-10-30 22:55 ` Junio C Hamano
2026-10-30 23:06 ` Jasper Mayone
2026-11-02 18:29 ` [PATCH v4 0/4] " Jasper Mayone
3 siblings, 1 reply; 33+ messages in thread
From: Junio C Hamano @ 2026-10-30 22:55 UTC (permalink / raw)
To: Jasper Mayone
Cc: git, phillip.wood123, sunshine, kristofferhaugsbakk, ben.knoble,
haraldnordgren
Jasper Mayone <me@jaspermayone.com> writes:
> This replaces my "git worktree list --branch" patch [1].
Then please label it as v3 as the old one is v2, or it breaks
automation.
> In that
> thread Kristoffer pointed out that "git for-each-ref --merged <commit>
> --format='%(worktreepath)' refs/heads/" already finds the worktrees of
> merged branches, so a new option is not needed. What is missing is
> that %(worktreepath) ignores a branch while a worktree is rebasing or
> bisecting it, although "git worktree add" and "git branch -d" already
> treat that branch as checked out. Phillip agreed that this is a bug.
>
> Ben then asked what happens if such a worktree is passed to "git
> worktree remove". Today it is removed, and the rebase or bisect state
> is lost with it, if the files happen to be clean. Phillip called that
> a bug too, and more likely to be hit once %(worktreepath) reports
> these worktrees, so it is fixed first.
All of these address great points. Thanks for working on it.
^ permalink raw reply [flat|nested] 33+ messages in thread
* Re: [PATCH 0/2] worktree: treat a branch being rebased or bisected as checked out
2026-10-30 22:55 ` [PATCH 0/2] worktree: treat a branch being rebased or bisected as checked out Junio C Hamano
@ 2026-10-30 23:06 ` Jasper Mayone
0 siblings, 0 replies; 33+ messages in thread
From: Jasper Mayone @ 2026-10-30 23:06 UTC (permalink / raw)
To: gitster
Cc: git, phillip.wood123, sunshine, kristofferhaugsbakk, ben.knoble,
haraldnordgren, Jasper Mayone
Junio C Hamano <gitster@pobox.com> writes:
> Jasper Mayone <me@jaspermayone.com> writes:
>
>> This replaces my "git worktree list --branch" patch [1].
>
> Then please label it as v3 as the old one is v2, or it breaks
> automation.
Sorry about that. I will treat this round as v3, and the next round
will be v4.
> All of these address great points. Thanks for working on it.
Thanks.
Jasper
^ permalink raw reply [flat|nested] 33+ messages in thread
* Re: [PATCH 2/2] ref-filter: name the worktree of a branch being rebased or bisected
2026-10-30 14:21 ` [PATCH 2/2] ref-filter: name the worktree of a branch being rebased or bisected Jasper Mayone
@ 2026-11-02 14:06 ` Phillip Wood
2026-11-02 17:57 ` Jasper Mayone
0 siblings, 1 reply; 33+ messages in thread
From: Phillip Wood @ 2026-11-02 14:06 UTC (permalink / raw)
To: Jasper Mayone, git
Cc: gitster, sunshine, kristofferhaugsbakk, ben.knoble,
haraldnordgren
Hi Jasper
On 30/10/2026 14:21, Jasper Mayone wrote:
> %(worktreepath) gives the worktree that has a branch checked out, and
> "git branch" uses it to mark such branches with "+". It only looks at
> the HEAD of each worktree, so while a worktree is rebasing or
> bisecting a branch, its HEAD is detached and the branch shows no
> worktree at all.
>
> The rest of Git already treats that branch as checked out: "git
> worktree add" and "git branch -d" refuse it, because finishing the
> rebase or bisect will check it out again.
Those commands also reject branches that are being updated by a rebase
via "update-ref" commands. If we're interested in checkouts we don't
want to list those, but it means that "%(worktree)" will still give
different results.
> "git worktree list" shows
> such a worktree as "(detached HEAD)", so %(worktreepath) is the only
> place a script could find out which worktree is working on the
> branch.
I don't really follow this logic. At the moment neither "git
for-each-ref", nor "git worktree list" show this - there is no way for
a script to find out which worktree is working on a given branch. We can
choose to add it to one or both commands.
> Also map the branch that a worktree is rebasing or bisecting to that
> worktree, as prepare_checked_out_branches() in branch.c does.
If we want it to behave like prepare_checked_out_branches() we should
use that function, rather than copying some of it to a separate file. If
we want to only list checked out branches we could do as I previously
suggested and add a function to branch.c that lists the worktrees where
a branch is checked out.
> Branches that "rebase --update-refs" will update are not mapped: that
> worktree has not checked them out, it only moves them when the rebase
> is done.
I think that's sensible
> As a result "git branch" marks such a branch with "+", also when it
> runs in the worktree that is rebasing or bisecting it, where it shows
> both "* (no branch, rebasing topic)" and "+ topic". Document that.
That sounds a bit confusing - "* (no branch, rebasing topic)" already
tells us what we need to know, why do we need "+ topic" as well? The
output of git branch is intended for human consumption, not scripts.
> diff --git a/Documentation/git-branch.adoc b/Documentation/git-branch.adoc
> index bfdf459329..d499d7e0d4 100644
> --- a/Documentation/git-branch.adoc
> +++ b/Documentation/git-branch.adoc
> @@ -33,7 +33,10 @@ DESCRIPTION
> If `--list` is given, or if there are no non-option arguments, existing
> branches are listed; the current branch will be highlighted in green and
> marked with an asterisk. Any branches checked out in linked worktrees will
> -be highlighted in cyan and marked with a plus sign. Option `-r` causes the
> +be highlighted in cyan and marked with a plus sign. A branch that a
> +worktree is rebasing or bisecting counts as checked out there, so it is
> +marked in the same way,
I follow this up to here
> also in the worktree that is rebasing or bisecting it.
but what is this trying to say?
> diff --git a/Documentation/git-for-each-ref.adoc b/Documentation/git-for-each-ref.adoc
> index a7b31e8aaf..90ff37259e 100644
> --- a/Documentation/git-for-each-ref.adoc
> +++ b/Documentation/git-for-each-ref.adoc
> @@ -182,7 +182,9 @@ but if used together the last option is selected.
>
> `worktreepath`::
> The absolute path to the worktree in which the ref is checked
> - out, if it is checked out in any linked worktree. Empty string
> + out, if it is checked out in any linked worktree. A branch that
> + a worktree is rebasing or bisecting counts as checked out there,
> + although that worktree's `HEAD` is detached. Empty string
> otherwise.
It is not a new problem, but I think it would be helpful to add
something like
As the path may contain a newline, it is recommended to either use
one of the quoting options, or follow `%(worktreepath)` with `%00`
and parse the output by looking for the `NUL` after the worktree
path and then the LF that terminates each output record.
Note that if a branch checked out in more than one worktree,
`%(worktreepath)` will only show one of those worktrees.
> diff --git a/ref-filter.c b/ref-filter.c
> index bdf54f6f59..1aebea243e 100644
> --- a/ref-filter.c
> +++ b/ref-filter.c
> [...]
> +/*
> + * Map each branch to the worktree that has it checked out. A worktree
> + * that is rebasing or bisecting a branch has it checked out, too,
> + * even though its HEAD may be detached, as for
> + * prepare_checked_out_branches() in branch.c.
> + */
> static void populate_worktree_map(struct hashmap *map, struct worktree **worktrees)
> {
> - int i;
> + struct strbuf ref = STRBUF_INIT;
>
> - for (i = 0; worktrees[i]; i++) {
> - if (worktrees[i]->head_ref) {
> - struct ref_to_worktree_entry *entry;
> - entry = xmalloc(sizeof(*entry));
> - entry->wt = worktrees[i];
> - hashmap_entry_init(&entry->ent,
> - strhash(worktrees[i]->head_ref));
> + for (size_t i = 0; worktrees[i]; i++) {
> + struct worktree *wt = worktrees[i];
> + struct wt_status_state state = { 0 };
>
> - hashmap_add(map, &entry->ent);
> + if (wt->is_bare)
> + continue;
This is a change in behavior. We currently print the git_dir of bare
worktrees. I think you can argue that this change is a bug fix as the
user asked for the worktree path, but it should be explained in the
commit message.
It is a shame that we end up duplicating code that is already in
branch.c. As I said last time, I think that it would be better to have a
single implementation that maps branches to worktrees in branch.c and
call that function rather than duplicating it here.
Thanks
Phillip
> +
> + if (wt->head_ref)
> + add_worktree_entry(map, wt, wt->head_ref);
> +
> + if (wt_status_check_rebase(wt, &state) &&
> + (state.rebase_in_progress ||
> + state.rebase_interactive_in_progress) &&
> + state.branch) {
> + strbuf_reset(&ref);
> + strbuf_addf(&ref, "refs/heads/%s", state.branch);
> + add_worktree_entry(map, wt, ref.buf);
> }
> + wt_status_state_free_buffers(&state);
> +
> + if (wt_status_check_bisect(wt, &state) &&
> + state.bisecting_from) {
> + strbuf_reset(&ref);
> + strbuf_addf(&ref, "refs/heads/%s", state.bisecting_from);
> + add_worktree_entry(map, wt, ref.buf);
> + }
> + wt_status_state_free_buffers(&state);
> }
> + strbuf_release(&ref);
> }
>
> static void lazy_init_worktree_map(void)
> diff --git a/t/t3203-branch-output.sh b/t/t3203-branch-output.sh
> index 7c44186341..e04599392d 100755
> --- a/t/t3203-branch-output.sh
> +++ b/t/t3203-branch-output.sh
> @@ -442,6 +442,41 @@ test_expect_success 'worktree colors correct' '
> test_cmp expect actual
> '
>
> +test_expect_success 'branch being rebased in a worktree is marked' '
> + test_when_finished "git -C rebase_dir rebase --abort; rm -rf rebase_dir; git worktree prune; git branch -D rebasing" &&
> + git worktree add -b rebasing rebase_dir branch-two &&
> + git -C rebase_dir commit --allow-empty -m rebase-me &&
> + test_must_fail git -C rebase_dir rebase -f -x false HEAD~1 &&
> + test_must_fail git -C rebase_dir symbolic-ref -q HEAD &&
> + git branch --list rebasing >actual &&
> + echo "+ rebasing" >expect &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success 'branch being rebased is marked in its own worktree' '
> + test_when_finished "git -C rebase_dir rebase --abort; rm -rf rebase_dir; git worktree prune; git branch -D rebasing" &&
> + git worktree add -b rebasing rebase_dir branch-two &&
> + git -C rebase_dir commit --allow-empty -m rebase-me &&
> + test_must_fail git -C rebase_dir rebase -f -x false HEAD~1 &&
> + git -C rebase_dir branch --list rebasing >actual &&
> + echo "+ rebasing" >expect &&
> + test_cmp expect actual &&
> + git -C rebase_dir branch >actual &&
> + test_grep "^\* (no branch, rebasing rebasing)" actual
> +'
> +
> +test_expect_success 'branch being bisected in a worktree is marked' '
> + test_when_finished "git -C bisect_dir bisect reset; rm -rf bisect_dir; git worktree prune; git branch -D bisecting" &&
> + git worktree add -b bisecting bisect_dir branch-two &&
> + git -C bisect_dir commit --allow-empty -m bisect-one &&
> + git -C bisect_dir commit --allow-empty -m bisect-two &&
> + git -C bisect_dir bisect start HEAD HEAD~2 &&
> + test_must_fail git -C bisect_dir symbolic-ref -q HEAD &&
> + git branch --list bisecting >actual &&
> + echo "+ bisecting" >expect &&
> + test_cmp expect actual
> +'
> +
> test_expect_success "set up color tests" '
> echo "<RED>main<RESET>" >expect.color &&
> echo "main" >expect.bare &&
> diff --git a/t/t6302-for-each-ref-filter.sh b/t/t6302-for-each-ref-filter.sh
> index 7f060d97bf..d656901f21 100755
> --- a/t/t6302-for-each-ref-filter.sh
> +++ b/t/t6302-for-each-ref-filter.sh
> @@ -3,6 +3,7 @@
> test_description='test for-each-refs usage of ref-filter APIs'
>
> . ./test-lib.sh
> +. "$TEST_DIRECTORY"/lib-rebase.sh
> . "$TEST_DIRECTORY"/lib-gpg.sh
>
> test_expect_success 'setup some history and refs' '
> @@ -541,6 +542,68 @@ test_expect_success 'validate worktree atom' '
> test_cmp expect actual
> '
>
> +test_expect_success 'worktree atom names a worktree rebasing the branch' '
> + test_when_finished "git -C rebasing rebase --abort; rm -rf rebasing; git worktree prune; git branch -D rebasing" &&
> + git worktree add -b rebasing rebasing main &&
> + git -C rebasing commit --allow-empty -m rebase-me &&
> + test_must_fail git -C rebasing rebase -f -x false main &&
> + test_must_fail git -C rebasing symbolic-ref -q HEAD &&
> + echo "$(pwd)/rebasing" >expect &&
> + git for-each-ref --format="%(worktreepath)" refs/heads/rebasing >actual &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success 'worktree atom names a worktree bisecting the branch' '
> + test_when_finished "git -C bisecting bisect reset; rm -rf bisecting; git worktree prune; git branch -D bisecting" &&
> + git worktree add -b bisecting bisecting main &&
> + git -C bisecting commit --allow-empty -m bisect-one &&
> + git -C bisecting commit --allow-empty -m bisect-two &&
> + git -C bisecting bisect start HEAD main &&
> + test_must_fail git -C bisecting symbolic-ref -q HEAD &&
> + echo "$(pwd)/bisecting" >expect &&
> + git for-each-ref --format="%(worktreepath)" refs/heads/bisecting >actual &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success 'worktree atom with an interactive rebase stopped at edit' '
> + test_when_finished "git -C editing rebase --abort; rm -rf editing; git worktree prune; git branch -D editing" &&
> + git worktree add -b editing editing main &&
> + git -C editing commit --allow-empty -m edit-me &&
> + (
> + set_fake_editor &&
> + FAKE_LINES="edit 1" git -C editing rebase -i --keep-empty HEAD~1
> + ) &&
> + test_must_fail git -C editing symbolic-ref -q HEAD &&
> + echo "$(pwd)/editing" >expect &&
> + git for-each-ref --format="%(worktreepath)" refs/heads/editing >actual &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success 'worktree atom ignores branches rebase --update-refs will move' '
> + test_when_finished "git -C updating rebase --abort; rm -rf updating; git worktree prune; git branch -D updating mid" &&
> + git worktree add -b updating updating main &&
> + git -C updating commit --allow-empty -m one &&
> + git -C updating branch mid &&
> + git -C updating commit --allow-empty -m two &&
> + test_must_fail git -C updating rebase -f --update-refs -x false main &&
> + git for-each-ref --format="%(refname:short) [%(worktreepath)]" \
> + refs/heads/updating refs/heads/mid >actual &&
> + cat >expect <<-EOF &&
> + mid []
> + updating [$(pwd)/updating]
> + EOF
> + test_cmp expect actual
> +'
> +
> +test_expect_success 'worktree atom ignores a rebase of a detached HEAD' '
> + test_when_finished "git -C detached-rebase rebase --abort; rm -rf detached-rebase; git worktree prune" &&
> + git worktree add --detach detached-rebase main &&
> + git -C detached-rebase commit --allow-empty -m detached &&
> + test_must_fail git -C detached-rebase rebase -f -x false main &&
> + git for-each-ref --format="%(worktreepath)" refs/heads/ >actual &&
> + test_grep ! detached-rebase actual
> +'
> +
> test_expect_success 'start after with empty value' '
> cat >expect <<-\EOF &&
> refs/heads/main
^ permalink raw reply [flat|nested] 33+ messages in thread
* Re: [PATCH 1/2] worktree remove: refuse a worktree in the middle of a rebase or bisect
2026-10-30 14:21 ` [PATCH 1/2] worktree remove: refuse a worktree in the middle of a rebase or bisect Jasper Mayone
@ 2026-11-02 14:07 ` Phillip Wood
2026-11-02 17:57 ` Jasper Mayone
0 siblings, 1 reply; 33+ messages in thread
From: Phillip Wood @ 2026-11-02 14:07 UTC (permalink / raw)
To: Jasper Mayone, git
Cc: gitster, sunshine, kristofferhaugsbakk, ben.knoble,
haraldnordgren
Hi Jasper
On 30/10/2026 14:21, Jasper Mayone wrote:
> "git worktree remove" refuses a worktree with modified or untracked
"refuses to move" would be clearer
> files unless --force is given, but it removes a worktree that is in
> the middle of a rebase, a "git am" session, a bisect or a cherry-pick
> or revert sequence when its files happen to be clean. The state of
> that operation lives in the worktree's own $GIT_DIR, so it is lost
> with the worktree. A rebase stopped at an "edit" or "break" line, an
> "exec" that failed, or a cherry-pick sequence after a conflict was
> resolved and committed is clean in this sense and still has work in
> progress.
Cherry picking or reverting a single commit that contains no new changes
also looks clean, but there is still a cherry-pick operation in progress
as CHERRY_PICK_HEAD exists. One could argue that, because the user
doesn't really lose any work, we don't lose much by deleting such a
worktree but we're still deleting a worktree that contains an
in-progress operation. Fixing that case simply requires us to see if
CHERRY_PICK_HEAD, or REVERT_HEAD exist.
> Treat such a worktree as unclean: refuse to remove it unless --force
> is given, and name the operation in the message.
>
> A worktree whose directory is already gone is still removed without
> --force, as before: the check is part of the check for a clean
> worktree, which is only done when the directory exists.
Sensible
> Reported-by: D. Ben Knoble <ben.knoble@gmail.com>
> Helped-by: Phillip Wood <phillip.wood123@gmail.com>
I'm not sure what I've done anything to help with this patch
> diff --git a/Documentation/git-worktree.adoc b/Documentation/git-worktree.adoc
> index e6e77252ab..da4c38f909 100644
> --- a/Documentation/git-worktree.adoc
> +++ b/Documentation/git-worktree.adoc
> @@ -142,9 +142,10 @@ the new working tree location.
> `remove`::
>
> Remove a worktree. Only clean worktrees (no untracked files and no
> -modification in tracked files) can be removed. Unclean worktrees or ones
> -with submodules can be removed with `--force`. The main worktree cannot be
> -removed.
> +modification in tracked files) can be removed. A worktree in the middle
> +of a rebase, a `git am` session, a bisect or a cherry-pick or revert
"bisect, cherry-pick or revert"
> +sequence is not clean either,
"either" doesn't make sense, because you've not described the unclean
case yet. This line needs folding as well.
> because removing it loses that state. Unclean worktrees or ones with submodules
> +can be removed with `--force`. The main worktree cannot be removed.
A list of the conditions that require --force might make this section
clearer.
> `repair [<path>...]`::
>
> @@ -189,7 +190,9 @@ twice. If the destination is already assigned to some other worktree but is
> missing (for instance, if _<new-path>_ was deleted manually), then `--force`
> allows the move to proceed; use `--force` twice if the destination is locked.
> +
> -`remove` refuses to remove an unclean worktree unless `--force` is used.
> +`remove` refuses to remove an unclean worktree, or one in the middle of a
> +rebase, `git am` session, bisect, or cherry-pick or revert sequence,
too many "or"s as above
> +unless `--force` is used.
> To remove a locked worktree, specify `--force` twice.
>
> `-b <new-branch>`::
> diff --git a/builtin/worktree.c b/builtin/worktree.c
> index 77ecd0f71f..ed08de1f43 100644
> --- a/builtin/worktree.c
> +++ b/builtin/worktree.c
> @@ -26,6 +26,7 @@
> #include "submodule.h"
> #include "utf8.h"
> #include "worktree.h"
> +#include "wt-status.h"
> #include "quote.h"
>
> #define BUILTIN_WORKTREE_ADD_USAGE \
> @@ -1408,6 +1409,32 @@ static void check_clean_worktree(struct worktree *wt,
> original_path, ret);
> }
>
> +/*
> + * A rebase, "git am", bisect or cherry-pick/revert sequence keeps its
> + * state in the worktree's own $GIT_DIR, and removing the worktree
> + * throws that away even when "git status" says the files are clean.
> + */
> +static void check_no_operation_in_progress(struct worktree *wt,
> + const char *original_path)
> +{
> + struct wt_status_state state = { 0 };
> +
> + if (wt_status_check_rebase(wt, &state)) {
> + if (state.am_in_progress)
> + die(_("'%s' is in the middle of an am session, use --force to delete it"),
> + original_path);
> + die(_("'%s' is in the middle of a rebase, use --force to delete it"),
> + original_path);
> + }
> + if (wt_status_check_bisect(wt, &state))
> + die(_("'%s' is in the middle of a bisect, use --force to delete it"),
> + original_path);
> + wt_status_state_free_buffers(&state);
> + if (is_directory(worktree_git_path(wt, "sequencer")))
> + die(_("'%s' is in the middle of a cherry-pick or revert, use --force to delete it"),
> + original_path);
> +}
This looks correct, though I wish we had a standard function that
returned an enum describing which operation was in progress, rather than
having to open code this sort of thing.
> static int delete_git_work_tree(struct worktree *wt)
> {
> struct strbuf sb = STRBUF_INIT;
> @@ -1461,8 +1488,10 @@ static int remove_worktree(int ac, const char **av, const char *prefix,
> strbuf_release(&errmsg);
>
> if (file_exists(wt->path)) {
> - if (!force)
> + if (!force) {
> + check_no_operation_in_progress(wt, av[0]);
This triggers the new check when the worktree directory exists - good.
> check_clean_worktree(wt, av[0]);
> + }
>
> ret |= delete_git_work_tree(wt);
> }
> diff --git a/t/t2403-worktree-move.sh b/t/t2403-worktree-move.sh
> index 69768c1207..4eb56a8911 100755
> --- a/t/t2403-worktree-move.sh
> +++ b/t/t2403-worktree-move.sh
> @@ -271,4 +271,63 @@ test_expect_success 'move worktree with relative path to absolute path' '
> test_cmp expect .git/worktrees/absolute/gitdir
> '
>
> +test_expect_success 'remove worktree in the middle of a rebase' '
> + test_when_finished "rm -rf mid-rebase; git worktree prune" &&
> + git worktree add --detach mid-rebase init &&
> + test_commit -C mid-rebase rebase-me &&
> + test_must_fail git -C mid-rebase rebase -f -x false init &&
> + test_must_fail git worktree remove mid-rebase 2>err &&
> + test_grep "in the middle of a rebase" err &&
> + test_path_is_dir mid-rebase &&
> + git worktree remove --force mid-rebase &&
> + test_path_is_missing mid-rebase
> +'
> +
> +test_expect_success 'remove worktree in the middle of an am session' '
> + test_when_finished "rm -rf mid-am patch; git worktree prune" &&
> + git worktree add --detach mid-am init &&
> + test_commit -C mid-am am-me &&
> + git -C mid-am format-patch -1 --stdout >patch &&
> + git -C mid-am reset --hard init &&
> + echo conflict >mid-am/am-me.t &&
> + git -C mid-am add am-me.t &&
> + git -C mid-am commit -m conflict &&
> + test_must_fail git -C mid-am am ../patch &&
> + git -C mid-am reset --hard &&
If we're running a lot of consecutive commands in a worktree, it is
clearer to use a subshell
(
cd mid-am &&
...
) &&
I wonder how hard it would be to extend the "setup" test to create a
branch that has conflicts when it is applied with am, cherry-pick, or
rebase, so that we don't have to set up the conflicts in each test.
Thanks
Phillip
> + test_must_fail git worktree remove mid-am 2>err &&
> + test_grep "in the middle of an am session" err &&
> + git worktree remove --force mid-am
> +'
> +
> +test_expect_success 'remove worktree in the middle of a bisect' '
> + test_when_finished "rm -rf mid-bisect; git worktree prune" &&
> + git worktree add --detach mid-bisect init &&
> + test_commit -C mid-bisect bisect-one &&
> + test_commit -C mid-bisect bisect-two &&
> + git -C mid-bisect bisect start HEAD init &&
> + test_must_fail git worktree remove mid-bisect 2>err &&
> + test_grep "in the middle of a bisect" err &&
> + git worktree remove --force mid-bisect
> +'
> +
> +test_expect_success 'remove worktree in the middle of a cherry-pick sequence' '
> + test_when_finished "rm -rf mid-pick; git worktree prune" &&
> + git worktree add --detach mid-pick init &&
> + git -C mid-pick switch -c pick-src &&
> + test_commit -C mid-pick pick-one &&
> + test_commit -C mid-pick pick-two &&
> + git -C mid-pick switch --detach init &&
> + echo conflict >mid-pick/pick-one.t &&
> + git -C mid-pick add pick-one.t &&
> + git -C mid-pick commit -m conflict &&
> + test_must_fail git -C mid-pick cherry-pick pick-src~1 pick-src &&
> + git -C mid-pick checkout --theirs pick-one.t &&
> + git -C mid-pick add pick-one.t &&
> + git -C mid-pick commit --no-edit &&
> + test_path_is_dir .git/worktrees/mid-pick/sequencer &&
> + test_must_fail git worktree remove mid-pick 2>err &&
> + test_grep "in the middle of a cherry-pick or revert" err &&
> + git worktree remove --force mid-pick
> +'
> +
> test_done
^ permalink raw reply [flat|nested] 33+ messages in thread
* Re: [PATCH 1/2] worktree remove: refuse a worktree in the middle of a rebase or bisect
2026-11-02 14:07 ` Phillip Wood
@ 2026-11-02 17:57 ` Jasper Mayone
0 siblings, 0 replies; 33+ messages in thread
From: Jasper Mayone @ 2026-11-02 17:57 UTC (permalink / raw)
To: phillip.wood123, git
Cc: gitster, sunshine, kristofferhaugsbakk, ben.knoble,
haraldnordgren, Jasper Mayone
Phillip Wood <phillip.wood123@gmail.com> writes:
> Cherry picking or reverting a single commit that contains no new
> changes also looks clean, but there is still a cherry-pick operation
> in progress as CHERRY_PICK_HEAD exists.
[...]
> Fixing that case simply requires us to see if CHERRY_PICK_HEAD, or
> REVERT_HEAD exist.
Good catch. v4 will check for CHERRY_PICK_HEAD and REVERT_HEAD in the
worktree's $GIT_DIR as well as the sequencer directory, with a test for
a single empty cherry-pick.
>> Helped-by: Phillip Wood <phillip.wood123@gmail.com>
>
> I'm not sure what I've done anything to help with this patch
You are right, your help was on the other patch. I will drop it here.
> A list of the conditions that require --force might make this section
> clearer.
Agreed. I will turn the `remove` description into a list of what makes
a worktree unclean (modified or untracked files, submodules, and an
operation in progress), and fix the "or" chains and "either" you
pointed out.
> If we're running a lot of consecutive commands in a worktree, it is
> clearer to use a subshell
Will do.
> I wonder how hard it would be to extend the "setup" test to create a
> branch that has conflicts when it is applied with am, cherry-pick, or
> rebase, so that we don't have to set up the conflicts in each test.
I will try that for v4. A branch that changes the same line as a
commit on top of "init" should give a conflict for all three.
Thanks for the review.
Jasper
^ permalink raw reply [flat|nested] 33+ messages in thread
* Re: [PATCH 2/2] ref-filter: name the worktree of a branch being rebased or bisected
2026-11-02 14:06 ` Phillip Wood
@ 2026-11-02 17:57 ` Jasper Mayone
0 siblings, 0 replies; 33+ messages in thread
From: Jasper Mayone @ 2026-11-02 17:57 UTC (permalink / raw)
To: phillip.wood123, git
Cc: gitster, sunshine, kristofferhaugsbakk, ben.knoble,
haraldnordgren, Jasper Mayone
Phillip Wood <phillip.wood123@gmail.com> writes:
> Those commands also reject branches that are being updated by a rebase
> via "update-ref" commands. If we're interested in checkouts we don't
> want to list those, but it means that "%(worktree)" will still give
> different results.
Yes, that difference is on purpose. I will say so in the commit message
and the documentation.
> I don't really follow this logic. At the moment neither "git
> for-each-ref", nor "git worktree list" show this - there is no way for
> a script to find out which worktree is working on a given branch. We
> can choose to add it to one or both commands.
You are right, that paragraph is wrong. No command shows it today, and
this patch adds it to %(worktreepath) only. I will rewrite it.
> If we want it to behave like prepare_checked_out_branches() we should
> use that function, rather than copying some of it to a separate file.
Agreed. For v4 I have added a small function to branch.c,
const char *branch_checked_out_worktree(const char *refname);
which reads the list that prepare_checked_out_branches() builds and
skips the BRANCH_CHECKOUT_KIND_UPDATE_REF entries. ref-filter calls it
and drops its own hashmap, so the patch now removes more code than it
adds in ref-filter.c. t6302, t3203, t3200, t2400 and t2403 pass with it.
> That sounds a bit confusing - "* (no branch, rebasing topic)" already
> tells us what we need to know, why do we need "+ topic" as well?
I agree. In v4 "git branch" will not mark the branch with "+" in the
worktree that is rebasing or bisecting it, since the "*" line already
names it. The "also in the worktree" sentence in git-branch.adoc goes
away with that.
> It is not a new problem, but I think it would be helpful to add
> something like
>
> As the path may contain a newline, [...]
>
> Note that if a branch checked out in more than one worktree,
> `%(worktreepath)` will only show one of those worktrees.
Thanks, I will add that as a separate documentation patch before this
one.
>> + if (wt->is_bare)
>> + continue;
>
> This is a change in behavior. We currently print the git_dir of bare
> worktrees. I think you can argue that this change is a bug fix as the
> user asked for the worktree path, but it should be explained in the
> commit message.
Agreed. With a bare repository and a linked worktree, today "git
branch" in the linked worktree shows "+ main" for the bare
repository's HEAD, and %(worktreepath) gives the path of the bare
repository. main is not checked out anywhere, and "git worktree add"
already lets you check it out. I will explain that in the commit
message and add a test for it.
Thanks,
Jasper
^ permalink raw reply [flat|nested] 33+ messages in thread
* [PATCH v4 0/4] worktree: treat a branch being rebased or bisected as checked out
2026-10-30 14:21 ` [PATCH 0/2] worktree: treat a branch being rebased or bisected as checked out Jasper Mayone
` (2 preceding siblings ...)
2026-10-30 22:55 ` [PATCH 0/2] worktree: treat a branch being rebased or bisected as checked out Junio C Hamano
@ 2026-11-02 18:29 ` Jasper Mayone
2026-11-02 18:29 ` [PATCH v4 1/4] worktree remove: refuse a worktree in the middle of an operation Jasper Mayone
` (3 more replies)
3 siblings, 4 replies; 33+ messages in thread
From: Jasper Mayone @ 2026-11-02 18:29 UTC (permalink / raw)
To: git
Cc: gitster, phillip.wood123, sunshine, kristofferhaugsbakk,
ben.knoble, haraldnordgren, Jasper Mayone
While a worktree is rebasing or bisecting a branch, its HEAD is
detached. "git worktree add" and "git branch -d" already treat the
branch as checked out there, but "git worktree remove" deletes such a
worktree when its files are clean, and %(worktreepath) and "git
branch" do not show which worktree has the branch.
Patch 1 makes "git worktree remove" refuse a worktree in the middle of
an operation unless --force is given. Patch 2 documents how to read
%(worktreepath) safely. Patch 3 makes %(worktreepath) name the
worktree that is rebasing or bisecting a branch, and patch 4 keeps
"git branch" from marking that branch with "+" in that worktree.
Changes since v3 [1]:
* Patch 1: also refuse a worktree that has CHERRY_PICK_HEAD or
REVERT_HEAD, for a single cherry-pick or revert whose files are
clean, with tests. The git-worktree documentation now lists the
cases that need --force. The tests use a shared setup and
subshells. Dropped the Helped-by trailer, as Phillip asked.
* Patch 2 is new. It adds the notes that Phillip suggested to the
documentation of %(worktreepath): the path can contain a newline,
and only one worktree is shown for a branch that is checked out in
more than one.
* Patch 3: %(worktreepath) now uses a new branch.c function,
branch_checked_out_worktree(), that reads the list that
prepare_checked_out_branches() builds, in place of a copy of that
code in ref-filter.c. ref-filter.c loses its own worktree map. The
commit message explains why branches that "rebase --update-refs"
will update are not included, and the change for the HEAD of a
bare repository, which now has a test.
* Patch 4 is new. "git branch" no longer marks a branch with "+" in
the worktree that is rebasing or bisecting it, because the
"(no branch, ...)" line already names it.
[1] https://lore.kernel.org/git/20261030142137.54750-1-me@jaspermayone.com/
Jasper Mayone (4):
worktree remove: refuse a worktree in the middle of an operation
doc: for-each-ref: say how to read %(worktreepath) safely
ref-filter: name the worktree of a branch being rebased or bisected
branch: do not mark the branch the current worktree is rebasing
Documentation/git-branch.adoc | 6 +-
Documentation/git-for-each-ref.adoc | 12 +++-
Documentation/git-worktree.adoc | 17 +++--
branch.c | 13 ++++
branch.h | 9 +++
builtin/branch.c | 103 +++++++++++++++++++++-------
builtin/worktree.c | 37 +++++++++-
ref-filter.c | 74 +-------------------
t/t2403-worktree-move.sh | 94 +++++++++++++++++++++++++
t/t3203-branch-output.sh | 49 +++++++++++++
t/t6302-for-each-ref-filter.sh | 76 ++++++++++++++++++++
11 files changed, 386 insertions(+), 104 deletions(-)
Range-diff against v3:
1: a79b69a0e4 ! 1: bca692b844 worktree remove: refuse a worktree in the middle of a rebase or bisect
@@ Metadata
Author: Jasper Mayone <me@jaspermayone.com>
## Commit message ##
- worktree remove: refuse a worktree in the middle of a rebase or bisect
+ worktree remove: refuse a worktree in the middle of an operation
- "git worktree remove" refuses a worktree with modified or untracked
- files unless --force is given, but it removes a worktree that is in
- the middle of a rebase, a "git am" session, a bisect or a cherry-pick
- or revert sequence when its files happen to be clean. The state of
- that operation lives in the worktree's own $GIT_DIR, so it is lost
- with the worktree. A rebase stopped at an "edit" or "break" line, an
- "exec" that failed, or a cherry-pick sequence after a conflict was
- resolved and committed is clean in this sense and still has work in
- progress.
+ "git worktree remove" refuses to remove a worktree with modified or
+ untracked files unless --force is given, but it removes a worktree
+ that is in the middle of a rebase, a "git am" session, a bisect, a
+ cherry-pick or a revert when its files happen to be clean. The state
+ of that operation lives in the worktree's own $GIT_DIR, so it is lost
+ with the worktree. Examples of such a clean worktree are a rebase
+ stopped at an "edit" or "break" line or after a failed "exec", a
+ cherry-pick sequence after a conflict was resolved and committed, and
+ a single cherry-pick or revert whose conflict was resolved to the
+ current contents, which leaves CHERRY_PICK_HEAD or REVERT_HEAD behind.
Treat such a worktree as unclean: refuse to remove it unless --force
is given, and name the operation in the message.
@@ Commit message
worktree, which is only done when the directory exists.
Reported-by: D. Ben Knoble <ben.knoble@gmail.com>
- Helped-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Jasper Mayone <me@jaspermayone.com>
## Documentation/git-worktree.adoc ##
@@ Documentation/git-worktree.adoc: the new working tree location.
+
`remove`::
- Remove a worktree. Only clean worktrees (no untracked files and no
+-Remove a worktree. Only clean worktrees (no untracked files and no
-modification in tracked files) can be removed. Unclean worktrees or ones
-with submodules can be removed with `--force`. The main worktree cannot be
-removed.
-+modification in tracked files) can be removed. A worktree in the middle
-+of a rebase, a `git am` session, a bisect or a cherry-pick or revert
-+sequence is not clean either, because removing it loses that state. Unclean worktrees or ones with submodules
-+can be removed with `--force`. The main worktree cannot be removed.
++Remove a worktree. The main worktree cannot be removed. Without
++`--force`, a worktree is not removed when:
+++
++--
++* it has untracked files or modifications in tracked files,
++* it has submodules, or
++* it is in the middle of a rebase, a `git am` session, a bisect, a
++ cherry-pick or a revert, because removing it loses the state of that
++ operation.
++--
`repair [<path>...]`::
@@ Documentation/git-worktree.adoc: twice. If the destination is already assigned t
allows the move to proceed; use `--force` twice if the destination is locked.
+
-`remove` refuses to remove an unclean worktree unless `--force` is used.
-+`remove` refuses to remove an unclean worktree, or one in the middle of a
-+rebase, `git am` session, bisect, or cherry-pick or revert sequence,
-+unless `--force` is used.
++`remove` refuses to remove a worktree in the cases listed under
++`remove` above unless `--force` is used.
To remove a locked worktree, specify `--force` twice.
`-b <new-branch>`::
@@ builtin/worktree.c: static void check_clean_worktree(struct worktree *wt,
}
+/*
-+ * A rebase, "git am", bisect or cherry-pick/revert sequence keeps its
-+ * state in the worktree's own $GIT_DIR, and removing the worktree
-+ * throws that away even when "git status" says the files are clean.
++ * A rebase, "git am", bisect, cherry-pick or revert keeps its state in
++ * the worktree's own $GIT_DIR, and removing the worktree throws that
++ * away even when "git status" says the files are clean.
+ */
+static void check_no_operation_in_progress(struct worktree *wt,
+ const char *original_path)
@@ builtin/worktree.c: static void check_clean_worktree(struct worktree *wt,
+ die(_("'%s' is in the middle of a bisect, use --force to delete it"),
+ original_path);
+ wt_status_state_free_buffers(&state);
++ if (refs_ref_exists(get_worktree_ref_store(wt), "CHERRY_PICK_HEAD"))
++ die(_("'%s' is in the middle of a cherry-pick, use --force to delete it"),
++ original_path);
++ if (refs_ref_exists(get_worktree_ref_store(wt), "REVERT_HEAD"))
++ die(_("'%s' is in the middle of a revert, use --force to delete it"),
++ original_path);
+ if (is_directory(worktree_git_path(wt, "sequencer")))
+ die(_("'%s' is in the middle of a cherry-pick or revert, use --force to delete it"),
+ original_path);
@@ t/t2403-worktree-move.sh: test_expect_success 'move worktree with relative path
test_cmp expect .git/worktrees/absolute/gitdir
'
++test_expect_success 'setup commits that conflict for am, cherry-pick and revert' '
++ git worktree add --detach conflict-setup init &&
++ (
++ cd conflict-setup &&
++ test_commit base conflict.t base &&
++ git switch --detach init &&
++ test_commit side conflict.t side &&
++ test_commit side-two
++ ) &&
++ git worktree remove conflict-setup
++'
++
+test_expect_success 'remove worktree in the middle of a rebase' '
+ test_when_finished "rm -rf mid-rebase; git worktree prune" &&
-+ git worktree add --detach mid-rebase init &&
-+ test_commit -C mid-rebase rebase-me &&
++ git worktree add --detach mid-rebase side &&
+ test_must_fail git -C mid-rebase rebase -f -x false init &&
+ test_must_fail git worktree remove mid-rebase 2>err &&
+ test_grep "in the middle of a rebase" err &&
@@ t/t2403-worktree-move.sh: test_expect_success 'move worktree with relative path
+
+test_expect_success 'remove worktree in the middle of an am session' '
+ test_when_finished "rm -rf mid-am patch; git worktree prune" &&
-+ git worktree add --detach mid-am init &&
-+ test_commit -C mid-am am-me &&
-+ git -C mid-am format-patch -1 --stdout >patch &&
-+ git -C mid-am reset --hard init &&
-+ echo conflict >mid-am/am-me.t &&
-+ git -C mid-am add am-me.t &&
-+ git -C mid-am commit -m conflict &&
-+ test_must_fail git -C mid-am am ../patch &&
-+ git -C mid-am reset --hard &&
++ git format-patch -1 --stdout side >patch &&
++ git worktree add --detach mid-am base &&
++ (
++ cd mid-am &&
++ test_must_fail git am ../patch &&
++ git reset --hard
++ ) &&
+ test_must_fail git worktree remove mid-am 2>err &&
+ test_grep "in the middle of an am session" err &&
+ git worktree remove --force mid-am
@@ t/t2403-worktree-move.sh: test_expect_success 'move worktree with relative path
+
+test_expect_success 'remove worktree in the middle of a bisect' '
+ test_when_finished "rm -rf mid-bisect; git worktree prune" &&
-+ git worktree add --detach mid-bisect init &&
-+ test_commit -C mid-bisect bisect-one &&
-+ test_commit -C mid-bisect bisect-two &&
-+ git -C mid-bisect bisect start HEAD init &&
++ git worktree add --detach mid-bisect side-two &&
++ git -C mid-bisect bisect start side-two init &&
+ test_must_fail git worktree remove mid-bisect 2>err &&
+ test_grep "in the middle of a bisect" err &&
+ git worktree remove --force mid-bisect
+'
+
-+test_expect_success 'remove worktree in the middle of a cherry-pick sequence' '
++test_expect_success 'remove worktree in the middle of a cherry-pick' '
+ test_when_finished "rm -rf mid-pick; git worktree prune" &&
-+ git worktree add --detach mid-pick init &&
-+ git -C mid-pick switch -c pick-src &&
-+ test_commit -C mid-pick pick-one &&
-+ test_commit -C mid-pick pick-two &&
-+ git -C mid-pick switch --detach init &&
-+ echo conflict >mid-pick/pick-one.t &&
-+ git -C mid-pick add pick-one.t &&
-+ git -C mid-pick commit -m conflict &&
-+ test_must_fail git -C mid-pick cherry-pick pick-src~1 pick-src &&
-+ git -C mid-pick checkout --theirs pick-one.t &&
-+ git -C mid-pick add pick-one.t &&
-+ git -C mid-pick commit --no-edit &&
-+ test_path_is_dir .git/worktrees/mid-pick/sequencer &&
++ git worktree add --detach mid-pick base &&
++ (
++ cd mid-pick &&
++ test_must_fail git cherry-pick side &&
++ git checkout --ours conflict.t &&
++ git add conflict.t &&
++ git rev-parse --verify CHERRY_PICK_HEAD
++ ) &&
++ test_path_is_missing .git/worktrees/mid-pick/sequencer &&
+ test_must_fail git worktree remove mid-pick 2>err &&
-+ test_grep "in the middle of a cherry-pick or revert" err &&
++ test_grep "in the middle of a cherry-pick," err &&
+ git worktree remove --force mid-pick
+'
++
++test_expect_success 'remove worktree in the middle of a revert' '
++ test_when_finished "rm -rf mid-revert; git worktree prune" &&
++ git worktree add --detach mid-revert base &&
++ (
++ cd mid-revert &&
++ test_must_fail git revert side &&
++ git checkout --ours conflict.t &&
++ git add conflict.t &&
++ git rev-parse --verify REVERT_HEAD
++ ) &&
++ test_path_is_missing .git/worktrees/mid-revert/sequencer &&
++ test_must_fail git worktree remove mid-revert 2>err &&
++ test_grep "in the middle of a revert" err &&
++ git worktree remove --force mid-revert
++'
++
++test_expect_success 'remove worktree in the middle of a cherry-pick sequence' '
++ test_when_finished "rm -rf mid-pick-seq; git worktree prune" &&
++ git worktree add --detach mid-pick-seq base &&
++ (
++ cd mid-pick-seq &&
++ test_must_fail git cherry-pick side side-two &&
++ git checkout --theirs conflict.t &&
++ git add conflict.t &&
++ git commit --no-edit
++ ) &&
++ test_path_is_dir .git/worktrees/mid-pick-seq/sequencer &&
++ test_must_fail git worktree remove mid-pick-seq 2>err &&
++ test_grep "in the middle of a cherry-pick or revert" err &&
++ git worktree remove --force mid-pick-seq
++'
+
test_done
-: ---------- > 2: 39eb92d441 doc: for-each-ref: say how to read %(worktreepath) safely
2: 0cd13347e1 ! 3: 990a19fd11 ref-filter: name the worktree of a branch being rebased or bisected
@@ Commit message
The rest of Git already treats that branch as checked out: "git
worktree add" and "git branch -d" refuse it, because finishing the
- rebase or bisect will check it out again. "git worktree list" shows
- such a worktree as "(detached HEAD)", so %(worktreepath) is the only
- place a script could find out which worktree is working on the
- branch.
+ rebase or bisect will check it out again. But no command shows which
+ worktree is working on the branch: "git worktree list" shows such a
+ worktree as "(detached HEAD)", and %(worktreepath) is empty.
- Also map the branch that a worktree is rebasing or bisecting to that
- worktree, as prepare_checked_out_branches() in branch.c does.
- Branches that "rebase --update-refs" will update are not mapped: that
- worktree has not checked them out, it only moves them when the rebase
- is done.
+ branch.c already knows the answer, as prepare_checked_out_branches()
+ records the branch that each worktree has checked out, is rebasing or
+ is bisecting. Add branch_checked_out_worktree() to look a branch up in
+ that list, and use it for %(worktreepath) in place of the map that
+ ref-filter.c builds from the HEAD of each worktree.
- As a result "git branch" marks such a branch with "+", also when it
- runs in the worktree that is rebasing or bisecting it, where it shows
- both "* (no branch, rebasing topic)" and "+ topic". Document that.
+ The new function skips the branches that "rebase --update-refs" will
+ update. branch_checked_out() includes them, so that they cannot be
+ checked out or deleted during the rebase, but the worktree that runs
+ the rebase has not checked them out. %(worktreepath) therefore stays
+ empty for them, unlike branch_checked_out().
+
+ This also changes the result for a bare repository: its HEAD names a
+ branch, and %(worktreepath) used to give the path of the bare
+ repository for that branch. "git branch" in a linked worktree marked
+ the branch with "+", although no worktree has it checked out and "git
+ worktree add" lets you check it out. prepare_checked_out_branches()
+ skips bare repositories, so %(worktreepath) is now empty for that
+ branch and "git branch" no longer marks it.
Reported-by: Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com>
Helped-by: Phillip Wood <phillip.wood123@gmail.com>
@@ Documentation/git-branch.adoc: DESCRIPTION
branches are listed; the current branch will be highlighted in green and
marked with an asterisk. Any branches checked out in linked worktrees will
-be highlighted in cyan and marked with a plus sign. Option `-r` causes the
+-remote-tracking branches to be listed,
+be highlighted in cyan and marked with a plus sign. A branch that a
-+worktree is rebasing or bisecting counts as checked out there, so it is
-+marked in the same way, also in the worktree that is rebasing or
-+bisecting it. Option `-r` causes the
- remote-tracking branches to be listed,
++worktree is rebasing or bisecting counts as checked out in that
++worktree, so the other worktrees mark it in the same way. Option `-r`
++causes the remote-tracking branches to be listed,
and option `-a` shows both local and remote branches.
+ If a `<pattern>`
## Documentation/git-for-each-ref.adoc ##
@@ Documentation/git-for-each-ref.adoc: but if used together the last option is selected.
@@ Documentation/git-for-each-ref.adoc: but if used together the last option is sel
- out, if it is checked out in any linked worktree. Empty string
+ out, if it is checked out in any linked worktree. A branch that
+ a worktree is rebasing or bisecting counts as checked out there,
-+ although that worktree's `HEAD` is detached. Empty string
- otherwise.
-
- `ahead-behind:<commit-ish>`::
++ although that worktree's `HEAD` is detached. A branch that
++ `git rebase --update-refs` will update does not. Empty string
+ otherwise. If the ref is checked out in more than one worktree,
+ only one of them is shown. The path can contain a newline, so
+ use one of the quoting options such as `--shell`, or follow
- ## ref-filter.c ##
-@@ ref-filter.c: static struct expand_data {
-
- struct ref_to_worktree_entry {
- struct hashmap_entry ent;
-- struct worktree *wt; /* key is wt->head_ref */
-+ struct worktree *wt;
-+ char refname[FLEX_ARRAY]; /* the key */
- };
-
- static int ref_to_worktree_map_cmpfnc(const void *lookupdata UNUSED,
-@@ ref-filter.c: static int ref_to_worktree_map_cmpfnc(const void *lookupdata UNUSED,
- e = container_of(eptr, const struct ref_to_worktree_entry, ent);
- k = container_of(kptr, const struct ref_to_worktree_entry, ent);
-
-- return strcmp(e->wt->head_ref,
-- keydata_aka_refname ? keydata_aka_refname : k->wt->head_ref);
-+ return strcmp(e->refname,
-+ keydata_aka_refname ? keydata_aka_refname : k->refname);
+ ## branch.c ##
+@@ branch.c: const char *branch_checked_out(const char *refname)
+ return strmap_get(¤t_checked_out_branches, refname);
}
- static struct ref_to_worktree_map {
++const char *branch_checked_out_worktree(const char *refname)
++{
++ const char *path = NULL;
++
++ prepare_checked_out_branches();
++ for (size_t i = 0; i < checked_out_branches_nr; i++) {
++ if (checked_out_branches[i].kind != BRANCH_CHECKOUT_KIND_UPDATE_REF &&
++ !strcmp(refname, checked_out_branches[i].refname))
++ path = checked_out_branches[i].path;
++ }
++ return path;
++}
++
+ const char *branch_bisecting(const char *refname)
+ {
+ prepare_checked_out_branches();
+
+ ## branch.h ##
+@@ branch.h: void create_branches_recursively(struct repository *r, const char *name,
+ */
+ const char *branch_checked_out(const char *refname);
+
++/*
++ * If a worktree has the branch 'refname' checked out, or is rebasing or
++ * bisecting it, return the path of that worktree, or of the last one if
++ * there are more. Otherwise return NULL. Unlike branch_checked_out(),
++ * this ignores the branches that "git rebase --update-refs" will
++ * update, because no worktree has them checked out.
++ */
++const char *branch_checked_out_worktree(const char *refname);
++
+ /*
+ * If the branch at 'refname' is currently used for bisecting in a
+ * worktree, then return the path to that worktree.
+
+ ## ref-filter.c ##
+@@
+ #define DISABLE_SIGN_COMPARE_WARNINGS
+
+ #include "git-compat-util.h"
++#include "branch.h"
+ #include "environment.h"
+ #include "gettext.h"
+ #include "config.h"
+@@ ref-filter.c: static struct expand_data {
+ struct object_info info;
+ } oi, oi_deref;
+
+-struct ref_to_worktree_entry {
+- struct hashmap_entry ent;
+- struct worktree *wt; /* key is wt->head_ref */
+-};
+-
+-static int ref_to_worktree_map_cmpfnc(const void *lookupdata UNUSED,
+- const struct hashmap_entry *eptr,
+- const struct hashmap_entry *kptr,
+- const void *keydata_aka_refname)
+-{
+- const struct ref_to_worktree_entry *e, *k;
+-
+- e = container_of(eptr, const struct ref_to_worktree_entry, ent);
+- k = container_of(kptr, const struct ref_to_worktree_entry, ent);
+-
+- return strcmp(e->wt->head_ref,
+- keydata_aka_refname ? keydata_aka_refname : k->wt->head_ref);
+-}
+-
+-static struct ref_to_worktree_map {
+- struct hashmap map;
+- struct worktree **worktrees;
+-} ref_to_worktree_map;
+-
+ /*
+ * The enum atom_type is used as the index of valid_atom array.
+ * In the atom parsing stage, it will be passed to used_atom.atom_type
@@ ref-filter.c: static int get_object(struct ref_array_item *ref, int deref,
return ret;
}
-+static void add_worktree_entry(struct hashmap *map, struct worktree *wt,
-+ const char *refname)
-+{
-+ struct ref_to_worktree_entry *entry;
-+
-+ FLEX_ALLOC_STR(entry, refname, refname);
-+ entry->wt = wt;
-+ hashmap_entry_init(&entry->ent, strhash(refname));
-+ hashmap_add(map, &entry->ent);
-+}
-+
-+/*
-+ * Map each branch to the worktree that has it checked out. A worktree
-+ * that is rebasing or bisecting a branch has it checked out, too,
-+ * even though its HEAD may be detached, as for
-+ * prepare_checked_out_branches() in branch.c.
-+ */
- static void populate_worktree_map(struct hashmap *map, struct worktree **worktrees)
- {
+-static void populate_worktree_map(struct hashmap *map, struct worktree **worktrees)
+-{
- int i;
-+ struct strbuf ref = STRBUF_INIT;
-
+-
- for (i = 0; worktrees[i]; i++) {
- if (worktrees[i]->head_ref) {
- struct ref_to_worktree_entry *entry;
@@ ref-filter.c: static int get_object(struct ref_array_item *ref, int deref,
- entry->wt = worktrees[i];
- hashmap_entry_init(&entry->ent,
- strhash(worktrees[i]->head_ref));
-+ for (size_t i = 0; worktrees[i]; i++) {
-+ struct worktree *wt = worktrees[i];
-+ struct wt_status_state state = { 0 };
-
+-
- hashmap_add(map, &entry->ent);
-+ if (wt->is_bare)
-+ continue;
-+
-+ if (wt->head_ref)
-+ add_worktree_entry(map, wt, wt->head_ref);
-+
-+ if (wt_status_check_rebase(wt, &state) &&
-+ (state.rebase_in_progress ||
-+ state.rebase_interactive_in_progress) &&
-+ state.branch) {
-+ strbuf_reset(&ref);
-+ strbuf_addf(&ref, "refs/heads/%s", state.branch);
-+ add_worktree_entry(map, wt, ref.buf);
- }
-+ wt_status_state_free_buffers(&state);
-+
-+ if (wt_status_check_bisect(wt, &state) &&
-+ state.bisecting_from) {
-+ strbuf_reset(&ref);
-+ strbuf_addf(&ref, "refs/heads/%s", state.bisecting_from);
-+ add_worktree_entry(map, wt, ref.buf);
-+ }
-+ wt_status_state_free_buffers(&state);
- }
-+ strbuf_release(&ref);
+- }
+- }
+-}
+-
+-static void lazy_init_worktree_map(void)
+-{
+- if (ref_to_worktree_map.worktrees)
+- return;
+-
+- ref_to_worktree_map.worktrees = get_worktrees(the_repository);
+- hashmap_init(&(ref_to_worktree_map.map), ref_to_worktree_map_cmpfnc, NULL, 0);
+- populate_worktree_map(&(ref_to_worktree_map.map), ref_to_worktree_map.worktrees);
+-}
+-
+ static char *get_worktree_path(const struct ref_array_item *ref)
+ {
+- struct hashmap_entry entry, *e;
+- struct ref_to_worktree_entry *lookup_result;
++ const char *path = branch_checked_out_worktree(ref->refname);
+
+- lazy_init_worktree_map();
+-
+- hashmap_entry_init(&entry, strhash(ref->refname));
+- e = hashmap_get(&(ref_to_worktree_map.map), &entry, ref->refname);
+-
+- if (!e)
+- return xstrdup("");
+-
+- lookup_result = container_of(e, struct ref_to_worktree_entry, ent);
+-
+- return xstrdup(lookup_result->wt->path);
++ return xstrdup(path ? path : "");
+ }
+
+ /*
+@@ ref-filter.c: void ref_array_clear(struct ref_array *array)
+ FREE_AND_NULL(used_atom);
+ used_atom_cnt = 0;
+
+- if (ref_to_worktree_map.worktrees) {
+- hashmap_clear_and_free(&(ref_to_worktree_map.map),
+- struct ref_to_worktree_entry, ent);
+- free_worktrees(ref_to_worktree_map.worktrees);
+- ref_to_worktree_map.worktrees = NULL;
+- }
+-
+ FREE_AND_NULL(array->counts);
}
- static void lazy_init_worktree_map(void)
## t/t3203-branch-output.sh ##
@@ t/t3203-branch-output.sh: test_expect_success 'worktree colors correct' '
@@ t/t3203-branch-output.sh: test_expect_success 'worktree colors correct' '
+test_expect_success 'branch being rebased in a worktree is marked' '
+ test_when_finished "git -C rebase_dir rebase --abort; rm -rf rebase_dir; git worktree prune; git branch -D rebasing" &&
+ git worktree add -b rebasing rebase_dir branch-two &&
-+ git -C rebase_dir commit --allow-empty -m rebase-me &&
-+ test_must_fail git -C rebase_dir rebase -f -x false HEAD~1 &&
-+ test_must_fail git -C rebase_dir symbolic-ref -q HEAD &&
++ (
++ cd rebase_dir &&
++ git commit --allow-empty -m rebase-me &&
++ test_must_fail git rebase -f -x false HEAD~1 &&
++ test_must_fail git symbolic-ref -q HEAD
++ ) &&
+ git branch --list rebasing >actual &&
+ echo "+ rebasing" >expect &&
+ test_cmp expect actual
+'
+
-+test_expect_success 'branch being rebased is marked in its own worktree' '
-+ test_when_finished "git -C rebase_dir rebase --abort; rm -rf rebase_dir; git worktree prune; git branch -D rebasing" &&
-+ git worktree add -b rebasing rebase_dir branch-two &&
-+ git -C rebase_dir commit --allow-empty -m rebase-me &&
-+ test_must_fail git -C rebase_dir rebase -f -x false HEAD~1 &&
-+ git -C rebase_dir branch --list rebasing >actual &&
-+ echo "+ rebasing" >expect &&
-+ test_cmp expect actual &&
-+ git -C rebase_dir branch >actual &&
-+ test_grep "^\* (no branch, rebasing rebasing)" actual
-+'
-+
+test_expect_success 'branch being bisected in a worktree is marked' '
+ test_when_finished "git -C bisect_dir bisect reset; rm -rf bisect_dir; git worktree prune; git branch -D bisecting" &&
+ git worktree add -b bisecting bisect_dir branch-two &&
-+ git -C bisect_dir commit --allow-empty -m bisect-one &&
-+ git -C bisect_dir commit --allow-empty -m bisect-two &&
-+ git -C bisect_dir bisect start HEAD HEAD~2 &&
-+ test_must_fail git -C bisect_dir symbolic-ref -q HEAD &&
++ (
++ cd bisect_dir &&
++ git commit --allow-empty -m bisect-one &&
++ git commit --allow-empty -m bisect-two &&
++ git bisect start HEAD HEAD~2 &&
++ test_must_fail git symbolic-ref -q HEAD
++ ) &&
+ git branch --list bisecting >actual &&
+ echo "+ bisecting" >expect &&
+ test_cmp expect actual
@@ t/t6302-for-each-ref-filter.sh: test_expect_success 'validate worktree atom' '
+ git for-each-ref --format="%(worktreepath)" refs/heads/ >actual &&
+ test_grep ! detached-rebase actual
+'
++
++test_expect_success 'worktree atom ignores the HEAD of a bare repository' '
++ test_when_finished "rm -rf bare.git bare-wt" &&
++ git clone --bare . bare.git &&
++ git -C bare.git symbolic-ref HEAD refs/heads/main &&
++ git -C bare.git worktree add --detach ../bare-wt main &&
++ git -C bare.git for-each-ref --format="%(worktreepath)" refs/heads/main >actual &&
++ echo >expect &&
++ test_cmp expect actual &&
++ git -C bare-wt branch --list main >actual &&
++ echo " main" >expect &&
++ test_cmp expect actual
++'
+
test_expect_success 'start after with empty value' '
cat >expect <<-\EOF &&
-: ---------- > 4: 56e29c448f branch: do not mark the branch the current worktree is rebasing
--
2.56.0
^ permalink raw reply [flat|nested] 33+ messages in thread
* [PATCH v4 1/4] worktree remove: refuse a worktree in the middle of an operation
2026-11-02 18:29 ` [PATCH v4 0/4] " Jasper Mayone
@ 2026-11-02 18:29 ` Jasper Mayone
2026-11-02 18:29 ` [PATCH v4 2/4] doc: for-each-ref: say how to read %(worktreepath) safely Jasper Mayone
` (2 subsequent siblings)
3 siblings, 0 replies; 33+ messages in thread
From: Jasper Mayone @ 2026-11-02 18:29 UTC (permalink / raw)
To: git
Cc: gitster, phillip.wood123, sunshine, kristofferhaugsbakk,
ben.knoble, haraldnordgren, Jasper Mayone
"git worktree remove" refuses to remove a worktree with modified or
untracked files unless --force is given, but it removes a worktree
that is in the middle of a rebase, a "git am" session, a bisect, a
cherry-pick or a revert when its files happen to be clean. The state
of that operation lives in the worktree's own $GIT_DIR, so it is lost
with the worktree. Examples of such a clean worktree are a rebase
stopped at an "edit" or "break" line or after a failed "exec", a
cherry-pick sequence after a conflict was resolved and committed, and
a single cherry-pick or revert whose conflict was resolved to the
current contents, which leaves CHERRY_PICK_HEAD or REVERT_HEAD behind.
Treat such a worktree as unclean: refuse to remove it unless --force
is given, and name the operation in the message.
A worktree whose directory is already gone is still removed without
--force, as before: the check is part of the check for a clean
worktree, which is only done when the directory exists.
Reported-by: D. Ben Knoble <ben.knoble@gmail.com>
Signed-off-by: Jasper Mayone <me@jaspermayone.com>
---
Documentation/git-worktree.adoc | 17 ++++--
builtin/worktree.c | 37 ++++++++++++-
t/t2403-worktree-move.sh | 94 +++++++++++++++++++++++++++++++++
3 files changed, 142 insertions(+), 6 deletions(-)
diff --git a/Documentation/git-worktree.adoc b/Documentation/git-worktree.adoc
index e6e77252ab..4a7dc47dd8 100644
--- a/Documentation/git-worktree.adoc
+++ b/Documentation/git-worktree.adoc
@@ -141,10 +141,16 @@ the new working tree location.
`remove`::
-Remove a worktree. Only clean worktrees (no untracked files and no
-modification in tracked files) can be removed. Unclean worktrees or ones
-with submodules can be removed with `--force`. The main worktree cannot be
-removed.
+Remove a worktree. The main worktree cannot be removed. Without
+`--force`, a worktree is not removed when:
++
+--
+* it has untracked files or modifications in tracked files,
+* it has submodules, or
+* it is in the middle of a rebase, a `git am` session, a bisect, a
+ cherry-pick or a revert, because removing it loses the state of that
+ operation.
+--
`repair [<path>...]`::
@@ -189,7 +195,8 @@ twice. If the destination is already assigned to some other worktree but is
missing (for instance, if _<new-path>_ was deleted manually), then `--force`
allows the move to proceed; use `--force` twice if the destination is locked.
+
-`remove` refuses to remove an unclean worktree unless `--force` is used.
+`remove` refuses to remove a worktree in the cases listed under
+`remove` above unless `--force` is used.
To remove a locked worktree, specify `--force` twice.
`-b <new-branch>`::
diff --git a/builtin/worktree.c b/builtin/worktree.c
index 77ecd0f71f..7f53215cda 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -26,6 +26,7 @@
#include "submodule.h"
#include "utf8.h"
#include "worktree.h"
+#include "wt-status.h"
#include "quote.h"
#define BUILTIN_WORKTREE_ADD_USAGE \
@@ -1408,6 +1409,38 @@ static void check_clean_worktree(struct worktree *wt,
original_path, ret);
}
+/*
+ * A rebase, "git am", bisect, cherry-pick or revert keeps its state in
+ * the worktree's own $GIT_DIR, and removing the worktree throws that
+ * away even when "git status" says the files are clean.
+ */
+static void check_no_operation_in_progress(struct worktree *wt,
+ const char *original_path)
+{
+ struct wt_status_state state = { 0 };
+
+ if (wt_status_check_rebase(wt, &state)) {
+ if (state.am_in_progress)
+ die(_("'%s' is in the middle of an am session, use --force to delete it"),
+ original_path);
+ die(_("'%s' is in the middle of a rebase, use --force to delete it"),
+ original_path);
+ }
+ if (wt_status_check_bisect(wt, &state))
+ die(_("'%s' is in the middle of a bisect, use --force to delete it"),
+ original_path);
+ wt_status_state_free_buffers(&state);
+ if (refs_ref_exists(get_worktree_ref_store(wt), "CHERRY_PICK_HEAD"))
+ die(_("'%s' is in the middle of a cherry-pick, use --force to delete it"),
+ original_path);
+ if (refs_ref_exists(get_worktree_ref_store(wt), "REVERT_HEAD"))
+ die(_("'%s' is in the middle of a revert, use --force to delete it"),
+ original_path);
+ if (is_directory(worktree_git_path(wt, "sequencer")))
+ die(_("'%s' is in the middle of a cherry-pick or revert, use --force to delete it"),
+ original_path);
+}
+
static int delete_git_work_tree(struct worktree *wt)
{
struct strbuf sb = STRBUF_INIT;
@@ -1461,8 +1494,10 @@ static int remove_worktree(int ac, const char **av, const char *prefix,
strbuf_release(&errmsg);
if (file_exists(wt->path)) {
- if (!force)
+ if (!force) {
+ check_no_operation_in_progress(wt, av[0]);
check_clean_worktree(wt, av[0]);
+ }
ret |= delete_git_work_tree(wt);
}
diff --git a/t/t2403-worktree-move.sh b/t/t2403-worktree-move.sh
index 69768c1207..2e45cd77f9 100755
--- a/t/t2403-worktree-move.sh
+++ b/t/t2403-worktree-move.sh
@@ -271,4 +271,98 @@ test_expect_success 'move worktree with relative path to absolute path' '
test_cmp expect .git/worktrees/absolute/gitdir
'
+test_expect_success 'setup commits that conflict for am, cherry-pick and revert' '
+ git worktree add --detach conflict-setup init &&
+ (
+ cd conflict-setup &&
+ test_commit base conflict.t base &&
+ git switch --detach init &&
+ test_commit side conflict.t side &&
+ test_commit side-two
+ ) &&
+ git worktree remove conflict-setup
+'
+
+test_expect_success 'remove worktree in the middle of a rebase' '
+ test_when_finished "rm -rf mid-rebase; git worktree prune" &&
+ git worktree add --detach mid-rebase side &&
+ test_must_fail git -C mid-rebase rebase -f -x false init &&
+ test_must_fail git worktree remove mid-rebase 2>err &&
+ test_grep "in the middle of a rebase" err &&
+ test_path_is_dir mid-rebase &&
+ git worktree remove --force mid-rebase &&
+ test_path_is_missing mid-rebase
+'
+
+test_expect_success 'remove worktree in the middle of an am session' '
+ test_when_finished "rm -rf mid-am patch; git worktree prune" &&
+ git format-patch -1 --stdout side >patch &&
+ git worktree add --detach mid-am base &&
+ (
+ cd mid-am &&
+ test_must_fail git am ../patch &&
+ git reset --hard
+ ) &&
+ test_must_fail git worktree remove mid-am 2>err &&
+ test_grep "in the middle of an am session" err &&
+ git worktree remove --force mid-am
+'
+
+test_expect_success 'remove worktree in the middle of a bisect' '
+ test_when_finished "rm -rf mid-bisect; git worktree prune" &&
+ git worktree add --detach mid-bisect side-two &&
+ git -C mid-bisect bisect start side-two init &&
+ test_must_fail git worktree remove mid-bisect 2>err &&
+ test_grep "in the middle of a bisect" err &&
+ git worktree remove --force mid-bisect
+'
+
+test_expect_success 'remove worktree in the middle of a cherry-pick' '
+ test_when_finished "rm -rf mid-pick; git worktree prune" &&
+ git worktree add --detach mid-pick base &&
+ (
+ cd mid-pick &&
+ test_must_fail git cherry-pick side &&
+ git checkout --ours conflict.t &&
+ git add conflict.t &&
+ git rev-parse --verify CHERRY_PICK_HEAD
+ ) &&
+ test_path_is_missing .git/worktrees/mid-pick/sequencer &&
+ test_must_fail git worktree remove mid-pick 2>err &&
+ test_grep "in the middle of a cherry-pick," err &&
+ git worktree remove --force mid-pick
+'
+
+test_expect_success 'remove worktree in the middle of a revert' '
+ test_when_finished "rm -rf mid-revert; git worktree prune" &&
+ git worktree add --detach mid-revert base &&
+ (
+ cd mid-revert &&
+ test_must_fail git revert side &&
+ git checkout --ours conflict.t &&
+ git add conflict.t &&
+ git rev-parse --verify REVERT_HEAD
+ ) &&
+ test_path_is_missing .git/worktrees/mid-revert/sequencer &&
+ test_must_fail git worktree remove mid-revert 2>err &&
+ test_grep "in the middle of a revert" err &&
+ git worktree remove --force mid-revert
+'
+
+test_expect_success 'remove worktree in the middle of a cherry-pick sequence' '
+ test_when_finished "rm -rf mid-pick-seq; git worktree prune" &&
+ git worktree add --detach mid-pick-seq base &&
+ (
+ cd mid-pick-seq &&
+ test_must_fail git cherry-pick side side-two &&
+ git checkout --theirs conflict.t &&
+ git add conflict.t &&
+ git commit --no-edit
+ ) &&
+ test_path_is_dir .git/worktrees/mid-pick-seq/sequencer &&
+ test_must_fail git worktree remove mid-pick-seq 2>err &&
+ test_grep "in the middle of a cherry-pick or revert" err &&
+ git worktree remove --force mid-pick-seq
+'
+
test_done
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH v4 2/4] doc: for-each-ref: say how to read %(worktreepath) safely
2026-11-02 18:29 ` [PATCH v4 0/4] " Jasper Mayone
2026-11-02 18:29 ` [PATCH v4 1/4] worktree remove: refuse a worktree in the middle of an operation Jasper Mayone
@ 2026-11-02 18:29 ` Jasper Mayone
2026-11-02 18:29 ` [PATCH v4 3/4] ref-filter: name the worktree of a branch being rebased or bisected Jasper Mayone
2026-11-02 18:29 ` [PATCH v4 4/4] branch: do not mark the branch the current worktree is rebasing Jasper Mayone
3 siblings, 0 replies; 33+ messages in thread
From: Jasper Mayone @ 2026-11-02 18:29 UTC (permalink / raw)
To: git
Cc: gitster, phillip.wood123, sunshine, kristofferhaugsbakk,
ben.knoble, haraldnordgren, Jasper Mayone
The path that %(worktreepath) gives can contain a newline, which
breaks a script that reads one record per line. Say how to avoid
that: use a quoting option, or end the atom with %00 and look for the
NUL before the LF that ends the record.
Also say that only one worktree is shown when a branch is checked out
in more than one worktree.
Suggested-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Jasper Mayone <me@jaspermayone.com>
---
Documentation/git-for-each-ref.adoc | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/Documentation/git-for-each-ref.adoc b/Documentation/git-for-each-ref.adoc
index a7b31e8aaf..9247737aa7 100644
--- a/Documentation/git-for-each-ref.adoc
+++ b/Documentation/git-for-each-ref.adoc
@@ -183,7 +183,12 @@ but if used together the last option is selected.
`worktreepath`::
The absolute path to the worktree in which the ref is checked
out, if it is checked out in any linked worktree. Empty string
- otherwise.
+ otherwise. If the ref is checked out in more than one worktree,
+ only one of them is shown. The path can contain a newline, so
+ use one of the quoting options such as `--shell`, or follow
+ `%(worktreepath)` with `%00` and parse the output by looking for
+ the `NUL` after the path and then for the `LF` that ends each
+ record.
`ahead-behind:<commit-ish>`::
Two integers, separated by a space, demonstrating the number of
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH v4 3/4] ref-filter: name the worktree of a branch being rebased or bisected
2026-11-02 18:29 ` [PATCH v4 0/4] " Jasper Mayone
2026-11-02 18:29 ` [PATCH v4 1/4] worktree remove: refuse a worktree in the middle of an operation Jasper Mayone
2026-11-02 18:29 ` [PATCH v4 2/4] doc: for-each-ref: say how to read %(worktreepath) safely Jasper Mayone
@ 2026-11-02 18:29 ` Jasper Mayone
2026-11-02 18:29 ` [PATCH v4 4/4] branch: do not mark the branch the current worktree is rebasing Jasper Mayone
3 siblings, 0 replies; 33+ messages in thread
From: Jasper Mayone @ 2026-11-02 18:29 UTC (permalink / raw)
To: git
Cc: gitster, phillip.wood123, sunshine, kristofferhaugsbakk,
ben.knoble, haraldnordgren, Jasper Mayone
%(worktreepath) gives the worktree that has a branch checked out, and
"git branch" uses it to mark such branches with "+". It only looks at
the HEAD of each worktree, so while a worktree is rebasing or
bisecting a branch, its HEAD is detached and the branch shows no
worktree at all.
The rest of Git already treats that branch as checked out: "git
worktree add" and "git branch -d" refuse it, because finishing the
rebase or bisect will check it out again. But no command shows which
worktree is working on the branch: "git worktree list" shows such a
worktree as "(detached HEAD)", and %(worktreepath) is empty.
branch.c already knows the answer, as prepare_checked_out_branches()
records the branch that each worktree has checked out, is rebasing or
is bisecting. Add branch_checked_out_worktree() to look a branch up in
that list, and use it for %(worktreepath) in place of the map that
ref-filter.c builds from the HEAD of each worktree.
The new function skips the branches that "rebase --update-refs" will
update. branch_checked_out() includes them, so that they cannot be
checked out or deleted during the rebase, but the worktree that runs
the rebase has not checked them out. %(worktreepath) therefore stays
empty for them, unlike branch_checked_out().
This also changes the result for a bare repository: its HEAD names a
branch, and %(worktreepath) used to give the path of the bare
repository for that branch. "git branch" in a linked worktree marked
the branch with "+", although no worktree has it checked out and "git
worktree add" lets you check it out. prepare_checked_out_branches()
skips bare repositories, so %(worktreepath) is now empty for that
branch and "git branch" no longer marks it.
Reported-by: Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com>
Helped-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Jasper Mayone <me@jaspermayone.com>
---
Documentation/git-branch.adoc | 6 ++-
Documentation/git-for-each-ref.adoc | 5 +-
branch.c | 13 +++++
branch.h | 9 ++++
ref-filter.c | 74 ++--------------------------
t/t3203-branch-output.sh | 29 +++++++++++
t/t6302-for-each-ref-filter.sh | 76 +++++++++++++++++++++++++++++
7 files changed, 138 insertions(+), 74 deletions(-)
diff --git a/Documentation/git-branch.adoc b/Documentation/git-branch.adoc
index bfdf459329..510b0afc74 100644
--- a/Documentation/git-branch.adoc
+++ b/Documentation/git-branch.adoc
@@ -33,8 +33,10 @@ DESCRIPTION
If `--list` is given, or if there are no non-option arguments, existing
branches are listed; the current branch will be highlighted in green and
marked with an asterisk. Any branches checked out in linked worktrees will
-be highlighted in cyan and marked with a plus sign. Option `-r` causes the
-remote-tracking branches to be listed,
+be highlighted in cyan and marked with a plus sign. A branch that a
+worktree is rebasing or bisecting counts as checked out in that
+worktree, so the other worktrees mark it in the same way. Option `-r`
+causes the remote-tracking branches to be listed,
and option `-a` shows both local and remote branches.
If a `<pattern>`
diff --git a/Documentation/git-for-each-ref.adoc b/Documentation/git-for-each-ref.adoc
index 9247737aa7..4456b4809a 100644
--- a/Documentation/git-for-each-ref.adoc
+++ b/Documentation/git-for-each-ref.adoc
@@ -182,7 +182,10 @@ but if used together the last option is selected.
`worktreepath`::
The absolute path to the worktree in which the ref is checked
- out, if it is checked out in any linked worktree. Empty string
+ out, if it is checked out in any linked worktree. A branch that
+ a worktree is rebasing or bisecting counts as checked out there,
+ although that worktree's `HEAD` is detached. A branch that
+ `git rebase --update-refs` will update does not. Empty string
otherwise. If the ref is checked out in more than one worktree,
only one of them is shown. The path can contain a newline, so
use one of the quoting options such as `--shell`, or follow
diff --git a/branch.c b/branch.c
index 5bb1c28915..8dab72d857 100644
--- a/branch.c
+++ b/branch.c
@@ -485,6 +485,19 @@ const char *branch_checked_out(const char *refname)
return strmap_get(¤t_checked_out_branches, refname);
}
+const char *branch_checked_out_worktree(const char *refname)
+{
+ const char *path = NULL;
+
+ prepare_checked_out_branches();
+ for (size_t i = 0; i < checked_out_branches_nr; i++) {
+ if (checked_out_branches[i].kind != BRANCH_CHECKOUT_KIND_UPDATE_REF &&
+ !strcmp(refname, checked_out_branches[i].refname))
+ path = checked_out_branches[i].path;
+ }
+ return path;
+}
+
const char *branch_bisecting(const char *refname)
{
prepare_checked_out_branches();
diff --git a/branch.h b/branch.h
index 95b2431f24..60cb92a3e4 100644
--- a/branch.h
+++ b/branch.h
@@ -106,6 +106,15 @@ void create_branches_recursively(struct repository *r, const char *name,
*/
const char *branch_checked_out(const char *refname);
+/*
+ * If a worktree has the branch 'refname' checked out, or is rebasing or
+ * bisecting it, return the path of that worktree, or of the last one if
+ * there are more. Otherwise return NULL. Unlike branch_checked_out(),
+ * this ignores the branches that "git rebase --update-refs" will
+ * update, because no worktree has them checked out.
+ */
+const char *branch_checked_out_worktree(const char *refname);
+
/*
* If the branch at 'refname' is currently used for bisecting in a
* worktree, then return the path to that worktree.
diff --git a/ref-filter.c b/ref-filter.c
index bdf54f6f59..b636e56199 100644
--- a/ref-filter.c
+++ b/ref-filter.c
@@ -2,6 +2,7 @@
#define DISABLE_SIGN_COMPARE_WARNINGS
#include "git-compat-util.h"
+#include "branch.h"
#include "environment.h"
#include "gettext.h"
#include "config.h"
@@ -95,30 +96,6 @@ static struct expand_data {
struct object_info info;
} oi, oi_deref;
-struct ref_to_worktree_entry {
- struct hashmap_entry ent;
- struct worktree *wt; /* key is wt->head_ref */
-};
-
-static int ref_to_worktree_map_cmpfnc(const void *lookupdata UNUSED,
- const struct hashmap_entry *eptr,
- const struct hashmap_entry *kptr,
- const void *keydata_aka_refname)
-{
- const struct ref_to_worktree_entry *e, *k;
-
- e = container_of(eptr, const struct ref_to_worktree_entry, ent);
- k = container_of(kptr, const struct ref_to_worktree_entry, ent);
-
- return strcmp(e->wt->head_ref,
- keydata_aka_refname ? keydata_aka_refname : k->wt->head_ref);
-}
-
-static struct ref_to_worktree_map {
- struct hashmap map;
- struct worktree **worktrees;
-} ref_to_worktree_map;
-
/*
* The enum atom_type is used as the index of valid_atom array.
* In the atom parsing stage, it will be passed to used_atom.atom_type
@@ -2380,49 +2357,11 @@ static int get_object(struct ref_array_item *ref, int deref,
return ret;
}
-static void populate_worktree_map(struct hashmap *map, struct worktree **worktrees)
-{
- int i;
-
- for (i = 0; worktrees[i]; i++) {
- if (worktrees[i]->head_ref) {
- struct ref_to_worktree_entry *entry;
- entry = xmalloc(sizeof(*entry));
- entry->wt = worktrees[i];
- hashmap_entry_init(&entry->ent,
- strhash(worktrees[i]->head_ref));
-
- hashmap_add(map, &entry->ent);
- }
- }
-}
-
-static void lazy_init_worktree_map(void)
-{
- if (ref_to_worktree_map.worktrees)
- return;
-
- ref_to_worktree_map.worktrees = get_worktrees(the_repository);
- hashmap_init(&(ref_to_worktree_map.map), ref_to_worktree_map_cmpfnc, NULL, 0);
- populate_worktree_map(&(ref_to_worktree_map.map), ref_to_worktree_map.worktrees);
-}
-
static char *get_worktree_path(const struct ref_array_item *ref)
{
- struct hashmap_entry entry, *e;
- struct ref_to_worktree_entry *lookup_result;
+ const char *path = branch_checked_out_worktree(ref->refname);
- lazy_init_worktree_map();
-
- hashmap_entry_init(&entry, strhash(ref->refname));
- e = hashmap_get(&(ref_to_worktree_map.map), &entry, ref->refname);
-
- if (!e)
- return xstrdup("");
-
- lookup_result = container_of(e, struct ref_to_worktree_entry, ent);
-
- return xstrdup(lookup_result->wt->path);
+ return xstrdup(path ? path : "");
}
/*
@@ -3195,13 +3134,6 @@ void ref_array_clear(struct ref_array *array)
FREE_AND_NULL(used_atom);
used_atom_cnt = 0;
- if (ref_to_worktree_map.worktrees) {
- hashmap_clear_and_free(&(ref_to_worktree_map.map),
- struct ref_to_worktree_entry, ent);
- free_worktrees(ref_to_worktree_map.worktrees);
- ref_to_worktree_map.worktrees = NULL;
- }
-
FREE_AND_NULL(array->counts);
}
diff --git a/t/t3203-branch-output.sh b/t/t3203-branch-output.sh
index 7c44186341..debfac5f26 100755
--- a/t/t3203-branch-output.sh
+++ b/t/t3203-branch-output.sh
@@ -442,6 +442,35 @@ test_expect_success 'worktree colors correct' '
test_cmp expect actual
'
+test_expect_success 'branch being rebased in a worktree is marked' '
+ test_when_finished "git -C rebase_dir rebase --abort; rm -rf rebase_dir; git worktree prune; git branch -D rebasing" &&
+ git worktree add -b rebasing rebase_dir branch-two &&
+ (
+ cd rebase_dir &&
+ git commit --allow-empty -m rebase-me &&
+ test_must_fail git rebase -f -x false HEAD~1 &&
+ test_must_fail git symbolic-ref -q HEAD
+ ) &&
+ git branch --list rebasing >actual &&
+ echo "+ rebasing" >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success 'branch being bisected in a worktree is marked' '
+ test_when_finished "git -C bisect_dir bisect reset; rm -rf bisect_dir; git worktree prune; git branch -D bisecting" &&
+ git worktree add -b bisecting bisect_dir branch-two &&
+ (
+ cd bisect_dir &&
+ git commit --allow-empty -m bisect-one &&
+ git commit --allow-empty -m bisect-two &&
+ git bisect start HEAD HEAD~2 &&
+ test_must_fail git symbolic-ref -q HEAD
+ ) &&
+ git branch --list bisecting >actual &&
+ echo "+ bisecting" >expect &&
+ test_cmp expect actual
+'
+
test_expect_success "set up color tests" '
echo "<RED>main<RESET>" >expect.color &&
echo "main" >expect.bare &&
diff --git a/t/t6302-for-each-ref-filter.sh b/t/t6302-for-each-ref-filter.sh
index 7f060d97bf..317ed6f6cd 100755
--- a/t/t6302-for-each-ref-filter.sh
+++ b/t/t6302-for-each-ref-filter.sh
@@ -3,6 +3,7 @@
test_description='test for-each-refs usage of ref-filter APIs'
. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-rebase.sh
. "$TEST_DIRECTORY"/lib-gpg.sh
test_expect_success 'setup some history and refs' '
@@ -541,6 +542,81 @@ test_expect_success 'validate worktree atom' '
test_cmp expect actual
'
+test_expect_success 'worktree atom names a worktree rebasing the branch' '
+ test_when_finished "git -C rebasing rebase --abort; rm -rf rebasing; git worktree prune; git branch -D rebasing" &&
+ git worktree add -b rebasing rebasing main &&
+ git -C rebasing commit --allow-empty -m rebase-me &&
+ test_must_fail git -C rebasing rebase -f -x false main &&
+ test_must_fail git -C rebasing symbolic-ref -q HEAD &&
+ echo "$(pwd)/rebasing" >expect &&
+ git for-each-ref --format="%(worktreepath)" refs/heads/rebasing >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'worktree atom names a worktree bisecting the branch' '
+ test_when_finished "git -C bisecting bisect reset; rm -rf bisecting; git worktree prune; git branch -D bisecting" &&
+ git worktree add -b bisecting bisecting main &&
+ git -C bisecting commit --allow-empty -m bisect-one &&
+ git -C bisecting commit --allow-empty -m bisect-two &&
+ git -C bisecting bisect start HEAD main &&
+ test_must_fail git -C bisecting symbolic-ref -q HEAD &&
+ echo "$(pwd)/bisecting" >expect &&
+ git for-each-ref --format="%(worktreepath)" refs/heads/bisecting >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'worktree atom with an interactive rebase stopped at edit' '
+ test_when_finished "git -C editing rebase --abort; rm -rf editing; git worktree prune; git branch -D editing" &&
+ git worktree add -b editing editing main &&
+ git -C editing commit --allow-empty -m edit-me &&
+ (
+ set_fake_editor &&
+ FAKE_LINES="edit 1" git -C editing rebase -i --keep-empty HEAD~1
+ ) &&
+ test_must_fail git -C editing symbolic-ref -q HEAD &&
+ echo "$(pwd)/editing" >expect &&
+ git for-each-ref --format="%(worktreepath)" refs/heads/editing >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'worktree atom ignores branches rebase --update-refs will move' '
+ test_when_finished "git -C updating rebase --abort; rm -rf updating; git worktree prune; git branch -D updating mid" &&
+ git worktree add -b updating updating main &&
+ git -C updating commit --allow-empty -m one &&
+ git -C updating branch mid &&
+ git -C updating commit --allow-empty -m two &&
+ test_must_fail git -C updating rebase -f --update-refs -x false main &&
+ git for-each-ref --format="%(refname:short) [%(worktreepath)]" \
+ refs/heads/updating refs/heads/mid >actual &&
+ cat >expect <<-EOF &&
+ mid []
+ updating [$(pwd)/updating]
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'worktree atom ignores a rebase of a detached HEAD' '
+ test_when_finished "git -C detached-rebase rebase --abort; rm -rf detached-rebase; git worktree prune" &&
+ git worktree add --detach detached-rebase main &&
+ git -C detached-rebase commit --allow-empty -m detached &&
+ test_must_fail git -C detached-rebase rebase -f -x false main &&
+ git for-each-ref --format="%(worktreepath)" refs/heads/ >actual &&
+ test_grep ! detached-rebase actual
+'
+
+test_expect_success 'worktree atom ignores the HEAD of a bare repository' '
+ test_when_finished "rm -rf bare.git bare-wt" &&
+ git clone --bare . bare.git &&
+ git -C bare.git symbolic-ref HEAD refs/heads/main &&
+ git -C bare.git worktree add --detach ../bare-wt main &&
+ git -C bare.git for-each-ref --format="%(worktreepath)" refs/heads/main >actual &&
+ echo >expect &&
+ test_cmp expect actual &&
+ git -C bare-wt branch --list main >actual &&
+ echo " main" >expect &&
+ test_cmp expect actual
+'
+
test_expect_success 'start after with empty value' '
cat >expect <<-\EOF &&
refs/heads/main
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
* [PATCH v4 4/4] branch: do not mark the branch the current worktree is rebasing
2026-11-02 18:29 ` [PATCH v4 0/4] " Jasper Mayone
` (2 preceding siblings ...)
2026-11-02 18:29 ` [PATCH v4 3/4] ref-filter: name the worktree of a branch being rebased or bisected Jasper Mayone
@ 2026-11-02 18:29 ` Jasper Mayone
3 siblings, 0 replies; 33+ messages in thread
From: Jasper Mayone @ 2026-11-02 18:29 UTC (permalink / raw)
To: git
Cc: gitster, phillip.wood123, sunshine, kristofferhaugsbakk,
ben.knoble, haraldnordgren, Jasper Mayone
Since the previous commit, "git branch" marks a branch with "+" when a
worktree is rebasing or bisecting it. In that worktree itself, the
list already starts with "* (no branch, rebasing topic)", so the
"+ topic" line below it says the same thing twice.
The default format cannot tell that apart: %(worktreepath) gives the
same path either way, and comparing it with %(if:equals=...) is not
safe because a path, or a branch name, can contain the ")" that ends
the atom. So build a second format that has no "+" marker and no
"(path)" for -vv, and use it for each branch that
branch_checked_out_worktree() maps to the current worktree. A branch
that is checked out normally in the current worktree also gets that
format, but %(HEAD) still marks it with "*" and -vv already hides
its path, so its output does not change.
To pick a format for each branch, print_ref_list() now formats the
branches itself in place of print_formatted_ref_array(), keeping
--omit-empty. A format given with --format is used for every branch,
as before.
Suggested-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Jasper Mayone <me@jaspermayone.com>
---
builtin/branch.c | 103 ++++++++++++++++++++++++++++++---------
t/t3203-branch-output.sh | 22 ++++++++-
2 files changed, 101 insertions(+), 24 deletions(-)
diff --git a/builtin/branch.c b/builtin/branch.c
index a613148fc7..b55a400217 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -418,16 +418,28 @@ static const char *quote_literal_for_format(const char *s)
return buf.buf;
}
-static char *build_format(struct ref_filter *filter, int maxwidth, const char *remote_prefix)
+/*
+ * With "show_worktree" unset, the format does not mark a branch that is
+ * checked out in a worktree, nor show the path of that worktree. It is
+ * used for a branch that the current worktree is rebasing or bisecting,
+ * because the "(no branch, ...)" line already names it.
+ */
+static char *build_format(struct ref_filter *filter, int maxwidth,
+ const char *remote_prefix, int show_worktree)
{
struct strbuf fmt = STRBUF_INIT;
struct strbuf local = STRBUF_INIT;
struct strbuf remote = STRBUF_INIT;
- strbuf_addf(&local, "%%(if)%%(HEAD)%%(then)* %s%%(else)%%(if)%%(worktreepath)%%(then)+ %s%%(else) %s%%(end)%%(end)",
- branch_get_color(BRANCH_COLOR_CURRENT),
- branch_get_color(BRANCH_COLOR_WORKTREE),
- branch_get_color(BRANCH_COLOR_LOCAL));
+ if (show_worktree)
+ strbuf_addf(&local, "%%(if)%%(HEAD)%%(then)* %s%%(else)%%(if)%%(worktreepath)%%(then)+ %s%%(else) %s%%(end)%%(end)",
+ branch_get_color(BRANCH_COLOR_CURRENT),
+ branch_get_color(BRANCH_COLOR_WORKTREE),
+ branch_get_color(BRANCH_COLOR_LOCAL));
+ else
+ strbuf_addf(&local, "%%(if)%%(HEAD)%%(then)* %s%%(else) %s%%(end)",
+ branch_get_color(BRANCH_COLOR_CURRENT),
+ branch_get_color(BRANCH_COLOR_LOCAL));
strbuf_addf(&remote, " %s",
branch_get_color(BRANCH_COLOR_REMOTE));
@@ -447,8 +459,9 @@ static char *build_format(struct ref_filter *filter, int maxwidth, const char *r
if (filter->verbose > 1)
{
- strbuf_addf(&local, "%%(if:notequals=*)%%(HEAD)%%(then)%%(if)%%(worktreepath)%%(then)(%s%%(worktreepath)%s) %%(end)%%(end)",
- branch_get_color(BRANCH_COLOR_WORKTREE), branch_get_color(BRANCH_COLOR_RESET));
+ if (show_worktree)
+ strbuf_addf(&local, "%%(if:notequals=*)%%(HEAD)%%(then)%%(if)%%(worktreepath)%%(then)(%s%%(worktreepath)%s) %%(end)%%(end)",
+ branch_get_color(BRANCH_COLOR_WORKTREE), branch_get_color(BRANCH_COLOR_RESET));
strbuf_addf(&local, "%%(if)%%(upstream)%%(then)[%s%%(upstream:short)%s%%(if)%%(upstream:track)"
"%%(then): %%(upstream:track,nobracket)%%(end)] %%(end)%%(contents:subject)",
branch_get_color(BRANCH_COLOR_UPSTREAM), branch_get_color(BRANCH_COLOR_RESET));
@@ -477,6 +490,32 @@ static char *build_format(struct ref_filter *filter, int maxwidth, const char *r
return strbuf_detach(&fmt, NULL);
}
+static char *current_worktree_path(void)
+{
+ struct worktree **worktrees = get_worktrees(the_repository);
+ char *path = NULL;
+
+ for (size_t i = 0; worktrees[i]; i++) {
+ if (worktrees[i]->is_current) {
+ path = xstrdup(worktrees[i]->path);
+ break;
+ }
+ }
+ free_worktrees(worktrees);
+ return path;
+}
+
+/*
+ * Is "refname" checked out in the worktree at "here", or is that
+ * worktree rebasing or bisecting it?
+ */
+static int checked_out_here(const char *refname, const char *here)
+{
+ const char *path = branch_checked_out_worktree(refname);
+
+ return path && !strcmp(path, here);
+}
+
static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sorting,
struct ref_format *format, struct string_list *output)
{
@@ -485,6 +524,9 @@ static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sortin
int maxwidth = 0;
const char *remote_prefix = "";
char *to_free = NULL;
+ struct ref_format here_format = REF_FORMAT_INIT;
+ char *here_to_free = NULL, *here = NULL;
+ struct strbuf out = STRBUF_INIT, err = STRBUF_INIT;
/*
* If we are listing more than just remote branches,
@@ -501,39 +543,54 @@ static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sortin
if (filter->verbose)
maxwidth = calc_maxwidth(&array, strlen(remote_prefix));
- if (!format->format)
- format->format = to_free = build_format(filter, maxwidth, remote_prefix);
+ if (!format->format) {
+ format->format = to_free = build_format(filter, maxwidth,
+ remote_prefix, 1);
+ here = current_worktree_path();
+ here_format = *format;
+ here_format.format = here_to_free = build_format(filter, maxwidth,
+ remote_prefix, 0);
+ }
format->use_color = branch_use_color;
+ here_format.use_color = branch_use_color;
- if (verify_ref_format(format))
+ if (verify_ref_format(format) ||
+ (here && verify_ref_format(&here_format)))
die(_("unable to parse format string"));
filter_ahead_behind(the_repository, &array);
ref_array_sort(sorting, &array);
- if (column_active(colopts)) {
- struct strbuf out = STRBUF_INIT, err = STRBUF_INIT;
-
+ if (column_active(colopts))
assert(!filter->verbose && "--column and --verbose are incompatible");
- for (i = 0; i < array.nr; i++) {
- strbuf_reset(&err);
- strbuf_reset(&out);
- if (format_ref_array_item(array.items[i], format, &out, &err))
- die("%s", err.buf);
+ for (i = 0; i < array.nr; i++) {
+ struct ref_array_item *item = array.items[i];
+ struct ref_format *f = format;
+ if (here && checked_out_here(item->refname, here))
+ f = &here_format;
+
+ strbuf_reset(&err);
+ strbuf_reset(&out);
+ if (format_ref_array_item(item, f, &out, &err))
+ die("%s", err.buf);
+
+ if (column_active(colopts))
/* format to a string_list to let print_columns() do its job */
string_list_append(output, out.buf);
+ else if (out.len || !format->array_opts.omit_empty) {
+ fwrite(out.buf, 1, out.len, stdout);
+ putchar('\n');
}
-
- strbuf_release(&err);
- strbuf_release(&out);
- } else {
- print_formatted_ref_array(&array, format);
}
+ strbuf_release(&err);
+ strbuf_release(&out);
ref_array_clear(&array);
free(to_free);
+ free(here_to_free);
+ free(here);
}
static void print_current_branch_name(void)
diff --git a/t/t3203-branch-output.sh b/t/t3203-branch-output.sh
index debfac5f26..5541f1429b 100755
--- a/t/t3203-branch-output.sh
+++ b/t/t3203-branch-output.sh
@@ -456,6 +456,23 @@ test_expect_success 'branch being rebased in a worktree is marked' '
test_cmp expect actual
'
+test_expect_success 'branch being rebased is not marked in its own worktree' '
+ test_when_finished "git -C rebase_dir rebase --abort; rm -rf rebase_dir; git worktree prune; git branch -D rebasing" &&
+ git worktree add -b rebasing rebase_dir branch-two &&
+ (
+ cd rebase_dir &&
+ git commit --allow-empty -m rebase-me &&
+ test_must_fail git rebase -f -x false HEAD~1 &&
+ git branch --list rebasing >actual &&
+ echo " rebasing" >expect &&
+ test_cmp expect actual &&
+ git branch -vv --list rebasing >actual &&
+ test_grep ! "$(pwd)" actual &&
+ git branch >actual &&
+ test_grep "^\* (no branch, rebasing rebasing)" actual
+ )
+'
+
test_expect_success 'branch being bisected in a worktree is marked' '
test_when_finished "git -C bisect_dir bisect reset; rm -rf bisect_dir; git worktree prune; git branch -D bisecting" &&
git worktree add -b bisecting bisect_dir branch-two &&
@@ -464,7 +481,10 @@ test_expect_success 'branch being bisected in a worktree is marked' '
git commit --allow-empty -m bisect-one &&
git commit --allow-empty -m bisect-two &&
git bisect start HEAD HEAD~2 &&
- test_must_fail git symbolic-ref -q HEAD
+ test_must_fail git symbolic-ref -q HEAD &&
+ git branch --list bisecting >actual &&
+ echo " bisecting" >expect &&
+ test_cmp expect actual
) &&
git branch --list bisecting >actual &&
echo "+ bisecting" >expect &&
--
2.56.0
^ permalink raw reply related [flat|nested] 33+ messages in thread
end of thread, other threads:[~2026-11-02 18:29 UTC | newest]
Thread overview: 33+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-21 17:33 [RFC PATCH 0/1] worktree list: add --merged and --no-merged Jasper Mayone
2026-10-21 17:33 ` [RFC PATCH 1/1] " Jasper Mayone
2026-10-23 18:30 ` Junio C Hamano
2026-10-24 6:25 ` Eric Sunshine
2026-10-24 16:53 ` [PATCH v2] worktree list: add --branch to select worktrees by branch Jasper Mayone
2026-10-25 14:47 ` Phillip Wood
2026-10-26 11:02 ` Phillip Wood
2026-10-28 6:02 ` Eric Sunshine
2026-10-28 15:27 ` Junio C Hamano
2026-10-28 11:32 ` Kristoffer Haugsbakk
2026-10-29 4:46 ` Jasper Mayone
2026-10-29 5:01 ` Eric Sunshine
2026-10-29 11:14 ` Phillip Wood
2026-10-29 14:23 ` Ben Knoble
2026-10-29 16:10 ` Phillip Wood
2026-10-30 14:21 ` [PATCH 0/2] worktree: treat a branch being rebased or bisected as checked out Jasper Mayone
2026-10-30 14:21 ` [PATCH 1/2] worktree remove: refuse a worktree in the middle of a rebase or bisect Jasper Mayone
2026-11-02 14:07 ` Phillip Wood
2026-11-02 17:57 ` Jasper Mayone
2026-10-30 14:21 ` [PATCH 2/2] ref-filter: name the worktree of a branch being rebased or bisected Jasper Mayone
2026-11-02 14:06 ` Phillip Wood
2026-11-02 17:57 ` Jasper Mayone
2026-10-30 22:55 ` [PATCH 0/2] worktree: treat a branch being rebased or bisected as checked out Junio C Hamano
2026-10-30 23:06 ` Jasper Mayone
2026-11-02 18:29 ` [PATCH v4 0/4] " Jasper Mayone
2026-11-02 18:29 ` [PATCH v4 1/4] worktree remove: refuse a worktree in the middle of an operation Jasper Mayone
2026-11-02 18:29 ` [PATCH v4 2/4] doc: for-each-ref: say how to read %(worktreepath) safely Jasper Mayone
2026-11-02 18:29 ` [PATCH v4 3/4] ref-filter: name the worktree of a branch being rebased or bisected Jasper Mayone
2026-11-02 18:29 ` [PATCH v4 4/4] branch: do not mark the branch the current worktree is rebasing Jasper Mayone
2026-10-21 21:36 ` [RFC PATCH 0/1] worktree list: add --merged and --no-merged Harald Nordgren
2026-10-21 22:12 ` Junio C Hamano
2026-10-22 7:21 ` Harald Nordgren
2026-10-22 17:58 ` Kristoffer Haugsbakk
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox