* [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; 11+ 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] 11+ 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; 11+ 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] 11+ 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; 11+ 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] 11+ 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; 11+ 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] 11+ 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; 11+ 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] 11+ 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; 11+ 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] 11+ 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; 11+ 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] 11+ 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; 11+ 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] 11+ 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
0 siblings, 1 reply; 11+ 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] 11+ 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
0 siblings, 1 reply; 11+ 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] 11+ 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
0 siblings, 0 replies; 11+ 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] 11+ messages in thread
end of thread, other threads:[~2026-10-26 11:02 UTC | newest]
Thread overview: 11+ 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-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