* [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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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
2026-10-28 11:32 ` Kristoffer Haugsbakk
0 siblings, 2 replies; 17+ 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] 17+ 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
1 sibling, 2 replies; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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
1 sibling, 1 reply; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ 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; 17+ 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] 17+ messages in thread
end of thread, other threads:[~2026-10-29 11:14 UTC | newest]
Thread overview: 17+ 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-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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.