{"thread":{"id":"66626","subject":"[RFC PATCH 0/1] worktree list: add --merged and --no-merged","startedAt":"2026-10-21T17:33:36Z","lastAt":"2026-11-02T18:29:30Z","messageCount":33,"participants":["Jasper Mayone","Harald Nordgren","Junio C Hamano","Kristoffer Haugsbakk","Eric Sunshine","Phillip Wood","Ben Knoble"],"isPatch":true,"patchVersion":1,"patchTotal":1},"messages":[{"id":"555761","messageId":"20261021173350.67854-1-me@jaspermayone.com","threadId":"66626","inReplyTo":null,"subject":"[RFC PATCH 0/1] worktree list: add --merged and --no-merged","fromName":"Jasper Mayone","fromEmail":"me@jaspermayone.com","sentAt":"2026-10-21T17:33:36Z","receivedAt":"2026-10-21T17:33:36Z","isPatch":true,"sender":{"key":"me@jaspermayone.com","avatar":null},"body":"I keep one worktree per topic. When a topic lands, its branch stays\nchecked out in that worktree, so \"git branch --delete-merged\" skips\nit, and there is no direct way to ask which worktrees are done. In\nthe --delete-merged squash thread [1] I said I would send an RFC for\nthis. Here it is.\n\nThis patch teaches \"git worktree list\" the --merged and --no-merged\nfilters that \"git branch\", \"git tag\" and \"git for-each-ref\" already\nhave, applied to the HEAD of each worktree:\n\n    $ git worktree list --merged origin/main\n    /src/proj            1a2b3c4 [main]\n    /src/proj-topic-a    5d6e7f8 [topic-a]\n    /src/proj-fix-b      9a8b7c6 (detached HEAD)\n\nIt only lists. Removing stays a separate step with \"git worktree\nremove\", which already refuses a dirty or locked worktree and the\nmain worktree, so a script can do:\n\n    git worktree list --porcelain --merged origin/main |\n    sed -n \"s/^worktree //p\" |\n    while read -r wt; do git worktree remove \"$wt\"; done\n\nThe test is strict reachability, the same as \"git branch --merged\".\nA worktree whose topic was squash or rebase merged upstream is not\nlisted. If Harald's \"has this landed\" check ends up as a helper, a\nlater option could use it here too, but I kept that out of this RFC.\n\nQuestions for the list:\n\n 1. The main worktree is filtered like any other, so it is usually\n    listed. \"git branch --merged\" also lists the current branch, so\n    I kept that. Should it be left out instead?\n\n 2. The commit defaults to HEAD, like the ref filters. For cleanup,\n    each worktree's branch upstream would often be the more useful\n    default, but that is a different meaning for the same option\n    name. Is HEAD the right default?\n\n 3. I reused struct ref_filter and OPT_MERGED from ref-filter.h, so\n    the parsing, the default and the error messages match the other\n    commands. It only needs the two commit lists. Is that acceptable,\n    or would you prefer a small local option callback? If the reuse\n    is fine, --contains and --points-at could follow the same way.\n\n 4. Is a later \"git worktree remove --merged <commit>\" (with\n    --dry-run) wanted, or should removal stay in scripts?\n\n[1] https://lore.kernel.org/git/pull.2425.v3.git.git.1792585717.gitgitgadget@gmail.com/\n\nJasper Mayone (1):\n  worktree list: add --merged and --no-merged\n\n Documentation/git-worktree.adoc |  24 +++++-\n builtin/worktree.c              |  80 +++++++++++++++++++-\n t/t2402-worktree-list.sh        | 125 ++++++++++++++++++++++++++++++++\n 3 files changed, 226 insertions(+), 3 deletions(-)\n\n\nbase-commit: 47fdf2208c5b94c8cc01c106b8f7c85efb91c926\n-- \n2.56.0\n\n\n"},{"id":"555762","messageId":"20261021173350.67854-2-me@jaspermayone.com","threadId":"66626","inReplyTo":"20261021173350.67854-1-me@jaspermayone.com","subject":"[RFC PATCH 1/1] worktree list: add --merged and --no-merged","fromName":"Jasper Mayone","fromEmail":"me@jaspermayone.com","sentAt":"2026-10-21T17:33:37Z","receivedAt":"2026-10-21T17:33:37Z","isPatch":true,"sender":{"key":"me@jaspermayone.com","avatar":null},"body":"Users who keep one worktree per topic have no direct way to find the\nworktrees whose work has already landed. \"git branch --merged\" lists\nthe branches, but the user then has to match each branch to a\nworktree by hand, and a detached worktree has no branch to match.\n\"git branch --delete-merged\" skips any branch that is checked out in\na worktree, so these are exactly the branches it leaves behind.\n\nTeach \"git worktree list\" the same --merged and --no-merged filters\nthat \"git branch\", \"git tag\" and \"git for-each-ref\" have, applied to\nthe HEAD of each worktree. The options are parsed by the same\nOPT_MERGED and OPT_NO_MERGED helpers, so the default (the HEAD of the\ncurrent worktree), the error messages and the rules for combining\nthem are the same: a worktree is shown when its HEAD is reachable\nfrom at least one of the --merged commits and from none of the\n--no-merged commits.\n\nA bare worktree, a worktree on an unborn branch, and a worktree whose\nHEAD cannot be read have no HEAD commit, so the filters leave them\nout. Both output formats are kept, so the result can be used directly\nwith \"--porcelain -z\":\n\n    git worktree list --porcelain -z --merged origin/main\n\nNothing is removed. Removing the worktrees stays a separate step with\n\"git worktree remove\", which already refuses a dirty or locked\nworktree.\n\nSigned-off-by: Jasper Mayone <me@jaspermayone.com>\n---\n Documentation/git-worktree.adoc |  24 +++++-\n builtin/worktree.c              |  80 +++++++++++++++++++-\n t/t2402-worktree-list.sh        | 125 ++++++++++++++++++++++++++++++++\n 3 files changed, 226 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/git-worktree.adoc b/Documentation/git-worktree.adoc\nindex e6e77252ab..817766b07c 100644\n--- a/Documentation/git-worktree.adoc\n+++ b/Documentation/git-worktree.adoc\n@@ -11,7 +11,7 @@ SYNOPSIS\n [synopsis]\n git worktree add [-f] [--detach] [--checkout] [--lock [--reason <string>]]\n \t\t [--orphan] [(-b | -B) <new-branch>] <path> [<commit-ish>]\n-git worktree list [-v | --porcelain [-z]]\n+git worktree list [-v | --porcelain [-z]] [--merged [<commit>]] [--no-merged [<commit>]]\n git worktree lock [--reason <string>] <worktree>\n git worktree move <worktree> <new-path>\n git worktree prune [-n] [-v] [--expire <expire>]\n@@ -114,6 +114,11 @@ whether the worktree is bare, the revision currently checked out, the\n branch currently checked out (or \"detached HEAD\" if none), \"locked\" if\n the worktree is locked, \"prunable\" if the worktree can be pruned by the\n `prune` command.\n++\n+With `--merged` or `--no-merged`, only the worktrees whose `HEAD` is\n+(or is not) reachable from the given commit are listed. This finds the\n+worktrees whose work has landed, for example to remove them with\n+`git worktree remove`.\n \n `lock`::\n \n@@ -268,6 +273,23 @@ mismatch, even if the links are correct.\n \tto parse the output when a worktree path contains a newline\n \tcharacter.\n \n+`--merged [<commit>]`::\n+\tWith `list`, only list worktrees whose `HEAD` is reachable from\n+\t_<commit>_. _<commit>_ defaults to the `HEAD` of the current\n+\tworktree.\n+\n+`--no-merged [<commit>]`::\n+\tWith `list`, only list worktrees whose `HEAD` is not reachable\n+\tfrom _<commit>_. _<commit>_ defaults to the `HEAD` of the current\n+\tworktree.\n++\n+When `--merged` and `--no-merged` are combined or repeated, only\n+worktrees whose `HEAD` is reachable from at least one of the\n+`--merged` commits and from none of the `--no-merged` commits are\n+listed. With either option, a bare worktree, a worktree on an unborn\n+branch, and a worktree whose `HEAD` cannot be read are not listed,\n+because they have no `HEAD` commit.\n+\n `-q`::\n `--quiet`::\n \tWith `add`, suppress feedback messages.\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 77ecd0f71f..5db9efb435 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -5,6 +5,7 @@\n #include \"abspath.h\"\n #include \"advice.h\"\n #include \"checkout.h\"\n+#include \"commit-reach.h\"\n #include \"config.h\"\n #include \"copy.h\"\n #include \"dir.h\"\n@@ -18,8 +19,10 @@\n #include \"strvec.h\"\n #include \"branch.h\"\n #include \"read-cache-ll.h\"\n+#include \"ref-filter.h\"\n #include \"refs.h\"\n #include \"remote.h\"\n+#include \"revision.h\"\n #include \"run-command.h\"\n #include \"hook.h\"\n #include \"sigchain.h\"\n@@ -33,7 +36,7 @@\n \t   \"                 [--orphan] [(-b | -B) <new-branch>] <path> [<commit-ish>]\")\n \n #define BUILTIN_WORKTREE_LIST_USAGE \\\n-\tN_(\"git worktree list [-v | --porcelain [-z]]\")\n+\tN_(\"git worktree list [-v | --porcelain [-z]] [--merged [<commit>]] [--no-merged [<commit>]]\")\n #define BUILTIN_WORKTREE_LOCK_USAGE \\\n \tN_(\"git worktree lock [--reason <string>] <worktree>\")\n #define BUILTIN_WORKTREE_MOVE_USAGE \\\n@@ -1125,11 +1128,78 @@ static void pathsort(struct worktree **wt)\n \tQSORT(wt, n, pathcmp);\n }\n \n+/*\n+ * Set result[i] when tips[i] is reachable from one of the commits in\n+ * \"bases\".\n+ */\n+static void mark_reachable(struct repository *repo,\n+\t\t\t   struct commit **tips, size_t nr,\n+\t\t\t   struct commit_list *bases, int *result)\n+{\n+\ttips_reachable_from_bases(repo, bases, tips, nr, UNINTERESTING);\n+\tfor (size_t i = 0; i < nr; i++)\n+\t\tresult[i] = !!(tips[i]->object.flags & UNINTERESTING);\n+\tclear_commit_marks_many(nr, tips, ALL_REV_FLAGS);\n+\tfor (; bases; bases = bases->next)\n+\t\tclear_commit_marks(bases->item, ALL_REV_FLAGS);\n+}\n+\n+/*\n+ * Keep only the worktrees whose HEAD is reachable from one of the\n+ * \"--merged\" commits and from none of the \"--no-merged\" commits, like\n+ * the ref filters of \"git branch\". A bare or unborn worktree has no\n+ * HEAD commit, and neither has a broken one, so these are dropped.\n+ */\n+static void filter_merged(struct repository *repo, struct worktree **wt,\n+\t\t\t  struct ref_filter *filter)\n+{\n+\tstruct commit **tips = NULL;\n+\tint *merged, *unmerged;\n+\tsize_t nr = 0, alloc = 0, kept = 0;\n+\n+\tfor (size_t i = 0; wt[i]; i++) {\n+\t\tstruct commit *commit = NULL;\n+\n+\t\tif (!wt[i]->is_bare && !is_null_oid(&wt[i]->head_oid))\n+\t\t\tcommit = lookup_commit_reference_gently(repo,\n+\t\t\t\t\t\t\t\t&wt[i]->head_oid, 1);\n+\t\tif (!commit) {\n+\t\t\tfree_worktree(wt[i]);\n+\t\t\tcontinue;\n+\t\t}\n+\t\tALLOC_GROW(tips, nr + 1, alloc);\n+\t\ttips[nr] = commit;\n+\t\twt[nr++] = wt[i];\n+\t}\n+\twt[nr] = NULL;\n+\n+\tCALLOC_ARRAY(merged, nr);\n+\tCALLOC_ARRAY(unmerged, nr);\n+\tif (filter->reachable_from)\n+\t\tmark_reachable(repo, tips, nr, filter->reachable_from, merged);\n+\tif (filter->unreachable_from)\n+\t\tmark_reachable(repo, tips, nr, filter->unreachable_from,\n+\t\t\t       unmerged);\n+\n+\tfor (size_t i = 0; i < nr; i++) {\n+\t\tif ((!filter->reachable_from || merged[i]) && !unmerged[i])\n+\t\t\twt[kept++] = wt[i];\n+\t\telse\n+\t\t\tfree_worktree(wt[i]);\n+\t}\n+\twt[kept] = NULL;\n+\n+\tfree(tips);\n+\tfree(merged);\n+\tfree(unmerged);\n+}\n+\n static int list(int ac, const char **av, const char *prefix,\n-\t\tstruct repository *repo UNUSED)\n+\t\tstruct repository *repo)\n {\n \tint porcelain = 0;\n \tint line_terminator = '\\n';\n+\tstruct ref_filter filter = REF_FILTER_INIT;\n \n \tstruct option options[] = {\n \t\tOPT_BOOL(0, \"porcelain\", &porcelain, N_(\"machine-readable output\")),\n@@ -1138,6 +1208,8 @@ static int list(int ac, const char **av, const char *prefix,\n \t\t\t\tN_(\"add 'prunable' annotation to missing worktrees older than <time>\")),\n \t\tOPT_SET_INT('z', NULL, &line_terminator,\n \t\t\t    N_(\"terminate records with a NUL character\"), '\\0'),\n+\t\tOPT_MERGED(&filter, N_(\"show only worktrees whose HEAD is merged\")),\n+\t\tOPT_NO_MERGED(&filter, N_(\"show only worktrees whose HEAD is not merged\")),\n \t\tOPT_END()\n \t};\n \n@@ -1157,6 +1229,9 @@ static int list(int ac, const char **av, const char *prefix,\n \t\t/* sort worktrees by path but keep main worktree at top */\n \t\tpathsort(worktrees + 1);\n \n+\t\tif (filter.reachable_from || filter.unreachable_from)\n+\t\t\tfilter_merged(repo, worktrees, &filter);\n+\n \t\tif (!porcelain)\n \t\t\tmeasure_widths(worktrees, &abbrev,\n \t\t\t\t       &display, &path_maxwidth);\n@@ -1174,6 +1249,7 @@ static int list(int ac, const char **av, const char *prefix,\n \t\tfree(display);\n \t\tfree_worktrees(worktrees);\n \t}\n+\tref_filter_clear(&filter);\n \treturn 0;\n }\n \ndiff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh\nindex eeaf800d74..0e0e3d7f06 100755\n--- a/t/t2402-worktree-list.sh\n+++ b/t/t2402-worktree-list.sh\n@@ -318,4 +318,129 @@ test_expect_success 'worktree path when called in .git directory' '\n \ttest_cmp list1 list2\n '\n \n+test_expect_success 'setup for \"list --merged\"' '\n+\tgit init merged &&\n+\t(\n+\t\tcd merged &&\n+\t\ttest_commit base &&\n+\t\tgit branch landed &&\n+\t\tgit branch pending &&\n+\t\tgit worktree add ../wt-landed landed &&\n+\t\tgit worktree add ../wt-pending pending &&\n+\t\tgit worktree add --detach ../wt-detached main &&\n+\t\tgit -C ../wt-landed commit --allow-empty -m landed &&\n+\t\tgit -C ../wt-pending commit --allow-empty -m pending &&\n+\t\tgit merge --ff-only landed\n+\t)\n+'\n+\n+list_paths () {\n+\tgit -C merged worktree list --porcelain \"$@\" >out &&\n+\tsed -n \"s/^worktree //p\" out >actual\n+}\n+\n+test_expect_success '\"list --merged\" shows only merged worktrees' '\n+\tlist_paths --merged main &&\n+\tcat >expect <<-EOF &&\n+\t$(pwd)/merged\n+\t$(pwd)/wt-detached\n+\t$(pwd)/wt-landed\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '\"list --merged\" defaults to the current worktree HEAD' '\n+\tgit -C wt-pending worktree list --porcelain --merged >out &&\n+\tsed -n \"s/^worktree //p\" out >actual &&\n+\tcat >expect <<-EOF &&\n+\t$(pwd)/wt-detached\n+\t$(pwd)/wt-pending\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '\"list --no-merged\" shows only unmerged worktrees' '\n+\tlist_paths --no-merged main &&\n+\techo \"$(pwd)/wt-pending\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '\"list --merged\" with several commits' '\n+\tlist_paths --merged main --merged pending --no-merged base &&\n+\tcat >expect <<-EOF &&\n+\t$(pwd)/merged\n+\t$(pwd)/wt-landed\n+\t$(pwd)/wt-pending\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '\"list --merged\" keeps the normal output format' '\n+\tgit -C merged worktree list --merged main >out &&\n+\tsed \"s/  */ /g\" <out >actual &&\n+\tcat >expect <<-EOF &&\n+\t$(pwd)/merged $(git -C merged rev-parse --short main) [main]\n+\t$(pwd)/wt-detached $(git -C merged rev-parse --short base) (detached HEAD)\n+\t$(pwd)/wt-landed $(git -C merged rev-parse --short landed) [landed]\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '\"list --merged\" rejects a bad commit' '\n+\ttest_must_fail git -C merged worktree list --merged no-such-ref 2>err &&\n+\ttest_grep \"malformed object name\" err &&\n+\ttest_must_fail git -C merged worktree list --merged \"main^{tree}\" 2>err &&\n+\ttest_grep \"must point to a commit\" err\n+'\n+\n+test_expect_success '\"list --merged\" can list nothing' '\n+\tgit -C merged worktree list --merged base --no-merged base >actual &&\n+\ttest_must_be_empty actual\n+'\n+\n+test_expect_success '\"list --merged\" works with -z' '\n+\tgit -C merged worktree list --porcelain -z --no-merged main >out &&\n+\ttr \"\\0\" \"\\n\" <out >actual &&\n+\tcat >expect <<-EOF &&\n+\tworktree $(pwd)/wt-pending\n+\tHEAD $(git -C merged rev-parse pending)\n+\tbranch refs/heads/pending\n+\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '\"list --merged\" works with -v' '\n+\ttest_when_finished \"git -C merged worktree unlock wt-landed\" &&\n+\tgit -C merged worktree lock --reason \"keep it\" wt-landed &&\n+\tgit -C merged worktree list -v --merged main >out &&\n+\ttest_grep \"^\tlocked: keep it\" out\n+'\n+\n+test_expect_success '\"list --[no-]merged\" omits unborn and broken worktrees' '\n+\ttest_when_finished \"rm -rf wt-unborn wt-broken && git -C merged worktree prune\" &&\n+\tgit -C merged worktree add --orphan -b unborn ../wt-unborn &&\n+\tgit -C merged worktree add --detach ../wt-broken main &&\n+\techo \"$ZERO_OID\" >merged/.git/worktrees/wt-broken/HEAD &&\n+\tgit -C merged worktree list --porcelain >out &&\n+\ttest_grep \"^worktree $(pwd)/wt-unborn\" out &&\n+\ttest_grep \"^worktree $(pwd)/wt-broken\" out &&\n+\tgit -C merged worktree list --porcelain --merged main >out &&\n+\ttest_grep ! \"wt-unborn\" out &&\n+\ttest_grep ! \"wt-broken\" out &&\n+\tgit -C merged worktree list --porcelain --no-merged main >out &&\n+\ttest_grep ! \"wt-unborn\" out &&\n+\ttest_grep ! \"wt-broken\" out\n+'\n+\n+test_expect_success '\"list --merged\" omits a bare main worktree' '\n+\ttest_when_finished \"rm -rf bare-merged there\" &&\n+\tgit clone --bare merged bare-merged &&\n+\tgit -C bare-merged worktree add --detach ../there main &&\n+\tgit -C bare-merged worktree list --porcelain --merged main >out &&\n+\tsed -n \"s/^worktree //p\" out >actual &&\n+\tgit -C there rev-parse --show-toplevel >expect &&\n+\ttest_cmp expect actual\n+'\n+\n test_done\n-- \n2.56.0\n\n\n"},{"id":"555801","messageId":"CAHwyqnXC6--wTx0Mkj9r-9oj4R6ndubjzcucym8y3Hs9CKYfVw@mail.gmail.com","threadId":"66626","inReplyTo":"20261021173350.67854-1-me@jaspermayone.com","subject":"Re: [RFC PATCH 0/1] worktree list: add --merged and --no-merged","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-10-21T21:36:25Z","receivedAt":"2026-10-21T21:36:25Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"I wonder if we should relax (or allow relaxing) the requirements on\nworktree branches overall?\n\nI noticed that I can't check out a branch that is checked out in\nanother worktree. Which seems a bit ridiculous because two regular\nrepos of the same upstream would never enforce that.\n\nAI loves worktress, so I suspect we'll see more of these discussions\nin the future.\n\n\nHarald\n\n"},{"id":"555804","messageId":"xmqqbj8mln3c.fsf@gitster.g","threadId":"66626","inReplyTo":"CAHwyqnXC6--wTx0Mkj9r-9oj4R6ndubjzcucym8y3Hs9CKYfVw@mail.gmail.com","subject":"Re: [RFC PATCH 0/1] worktree list: add --merged and --no-merged","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-10-21T22:12:39Z","receivedAt":"2026-10-21T22:12:39Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Harald Nordgren <haraldnordgren@gmail.com> writes:\n\n> I wonder if we should relax (or allow relaxing) the requirements on\n> worktree branches overall?\n>\n> I noticed that I can't check out a branch that is checked out in\n> another worktree. Which seems a bit ridiculous because two regular\n> repos of the same upstream would never enforce that.\n\nBecause your repository updating what commit your 'main' points at\nwould not affect my repository if they track the same upstream.\n\nWorktrees are different.  If you allow your two worktrees to check\nout the same 'main', and one of them makes a commit on top, you know\nwhat happens, right?\n\n> AI loves worktress, so I suspect we'll see more of these discussions\n> in the future.\n\nI think that was asked and answered already.  \"worktree add -d\" is\nyour friend.\n\n"},{"id":"555831","messageId":"CAHwyqnXRHBTt6AoZH=OBhd9Q4UgCk6vuHqrJbog3ZYguq694HQ@mail.gmail.com","threadId":"66626","inReplyTo":"xmqqbj8mln3c.fsf@gitster.g","subject":"Re: [RFC PATCH 0/1] worktree list: add --merged and --no-merged","fromName":"Harald Nordgren","fromEmail":"haraldnordgren@gmail.com","sentAt":"2026-10-22T07:21:14Z","receivedAt":"2026-10-22T07:21:14Z","isPatch":true,"sender":{"key":"haraldnordgren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/9569897?v=4"},"body":"> > I noticed that I can't check out a branch that is checked out in\n> > another worktree. Which seems a bit ridiculous because two regular\n> > repos of the same upstream would never enforce that.\n>\n> Because your repository updating what commit your 'main' points at\n> would not affect my repository if they track the same upstream.\n>\n> Worktrees are different.  If you allow your two worktrees to check\n> out the same 'main', and one of them makes a commit on top, you know\n> what happens, right?\n\nI didn't. I read up on it now, and yes it's hairy. I withdraw my point here.\n\n> > AI loves worktress, so I suspect we'll see more of these discussions\n> > in the future.\n>\n> I think that was asked and answered already.  \"worktree add -d\" is\n> your friend.\n\nOk.\n\n\n\nHarald\n\n"},{"id":"555893","messageId":"7cc80c0e-4a2a-494f-afe3-bbc07e0d8cf5@app.fastmail.com","threadId":"66626","inReplyTo":"CAHwyqnXC6--wTx0Mkj9r-9oj4R6ndubjzcucym8y3Hs9CKYfVw@mail.gmail.com","subject":"Re: [RFC PATCH 0/1] worktree list: add --merged and --no-merged","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-10-22T17:58:39Z","receivedAt":"2026-10-22T17:58:39Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Wed, Oct 21, 2026, at 23:36, Harald Nordgren wrote:\n>[snip]\n> AI loves worktress, so I suspect we'll see more of these discussions\n> in the future.\n\nIs this what mechanical sympathy has come to?\n\n"},{"id":"556094","messageId":"xmqqqzhg5kyd.fsf@gitster.g","threadId":"66626","inReplyTo":"20261021173350.67854-2-me@jaspermayone.com","subject":"Re: [RFC PATCH 1/1] worktree list: add --merged and --no-merged","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-10-23T18:30:02Z","receivedAt":"2026-10-23T18:30:02Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jasper Mayone <me@jaspermayone.com> writes:\n\n> Users who keep one worktree per topic have no direct way to find the\n> worktrees whose work has already landed. \"git branch --merged\" lists\n> the branches, but the user then has to match each branch to a\n> worktree by hand, and a detached worktree has no branch to match.\n> \"git branch --delete-merged\" skips any branch that is checked out in\n> a worktree, so these are exactly the branches it leaves behind.\n\nReal review on this patch never happened, it seems, perhaps because\na discussion on a side issue distracted everybody?\n\nIt looks like the intro unfairly narrows the problem space.  Even\nfor those users who do not use one worktree per topic do need to\nfind worktrees based on what branch they check out.  While I agree\nthat among the criteria to choose these branches to filter worktrees\non, \"--merged\" and \"--not-merged\" may be two of the most useful ones,\nthe feature does not have to be limited to them.\n\nI wonder if \"git worktree list --branch=...\", where --branch= can be\ngiven multiple times, is a better primitive?  With that you could do\nsomething like\n\n  $ git worktree list $(git branch --contains HEAD | sed -e 's/^/--branch=/')\n\nto reuse the same machinery without any extra code.\n\n"},{"id":"556142","messageId":"CAPig+cTZEcn_5TG0iMVJJym8Zhu1KYP+y9Oc1J=JJgDYSzNSdg@mail.gmail.com","threadId":"66626","inReplyTo":"xmqqqzhg5kyd.fsf@gitster.g","subject":"Re: [RFC PATCH 1/1] worktree list: add --merged and --no-merged","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2026-10-24T06:25:20Z","receivedAt":"2026-10-24T06:25:20Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Fri, Oct 23, 2026 at 2:30 PM Junio C Hamano <gitster@pobox.com> wrote:\n> Jasper Mayone <me@jaspermayone.com> writes:\n> > Users who keep one worktree per topic have no direct way to find the\n> > worktrees whose work has already landed. \"git branch --merged\" lists\n> > the branches, but the user then has to match each branch to a\n> > worktree by hand, and a detached worktree has no branch to match.\n> > \"git branch --delete-merged\" skips any branch that is checked out in\n> > a worktree, so these are exactly the branches it leaves behind.\n>\n> Real review on this patch never happened, it seems, perhaps because\n> a discussion on a side issue distracted everybody?\n>\n> It looks like the intro unfairly narrows the problem space.  Even\n> for those users who do not use one worktree per topic do need to\n> find worktrees based on what branch they check out.  While I agree\n> that among the criteria to choose these branches to filter worktrees\n> on, \"--merged\" and \"--not-merged\" may be two of the most useful ones,\n> the feature does not have to be limited to them.\n>\n> I wonder if \"git worktree list --branch=...\", where --branch= can be\n> given multiple times, is a better primitive?  With that you could do\n> something like\n>\n>   $ git worktree list $(git branch --contains HEAD | sed -e 's/^/--branch=/')\n>\n> to reuse the same machinery without any extra code.\n\nI've been too busy lately to allocate time to the Git project, so I\ndidn't read the patch body and only quickly skimmed the commit\nmessage, but I experienced the same reaction that this seems overly\nspecial-case, and immediately thought of a more general \"for-each\"\nsort of mechanism. The analogy which immediately comes to mind is `git\nsubmodule foreach <command>`.\n\n"},{"id":"556177","messageId":"20261024165311.57387-1-me@jaspermayone.com","threadId":"66626","inReplyTo":"CAPig+cTZEcn_5TG0iMVJJym8Zhu1KYP+y9Oc1J=JJgDYSzNSdg@mail.gmail.com","subject":"[PATCH v2] worktree list: add --branch to select worktrees by branch","fromName":"Jasper Mayone","fromEmail":"me@jaspermayone.com","sentAt":"2026-10-24T16:53:11Z","receivedAt":"2026-10-24T16:53:11Z","isPatch":true,"sender":{"key":"me@jaspermayone.com","avatar":null},"body":"There is no direct way to ask which worktree has a given branch\nchecked out. \"git worktree list --porcelain\" prints the branch of\nevery worktree, so a script has to parse the whole list and match the\n\"branch\" lines itself.\n\nTeach \"git worktree list\" a \"--branch=<branch>\" option that lists\nonly the worktrees that have <branch> checked out. It can be given\nmore than once. A worktree that is rebasing or bisecting a branch\ncounts as having it checked out, as it does for \"git branch -d\" and\n\"git worktree add\". <branch> is a branch name, or a full name under\nrefs/heads/.\n\nThis keeps the choice of branches in \"git branch\", which already has\n--merged, --contains, --points-at and patterns. For example, to list\nthe worktrees whose branch is merged into origin/main:\n\n    git worktree list \\\n\t$(git branch --format='--branch=%(refname)' --merged origin/main)\n\nThe --format is needed because the default output of \"git branch\"\nmarks the current branch with \"*\" and the branches checked out in\nother worktrees with \"+\". It uses the full name because\n%(refname:short) gives \"heads/topic\" when a tag is also called\n\"topic\".\n\nEach worktree's branch is found once and looked up in a set of the\nrequested branches, so feeding it every branch of a large repository\nstays cheap.\n\nA detached worktree has no branch, so it is never listed.\n\nSuggested-by: Junio C Hamano <gitster@pobox.com>\nSigned-off-by: Jasper Mayone <me@jaspermayone.com>\n---\nChanges since the RFC [1]:\n\n * Junio pointed out that --merged and --no-merged narrow the problem\n   to one workflow, and suggested selecting worktrees by branch\n   instead, with \"git branch\" doing the choosing. Eric had the same\n   reaction. This version does that and drops --merged, --no-merged\n   and the reachability code.\n\n * Junio's example piped \"git branch\" through sed. Its default output\n   prefixes the current branch with \"*\" and branches checked out in\n   other worktrees with \"+\", which are the branches this is for, so\n   the words come out as \"--branch=+\" and \"topic\". The documented form\n   uses --format='--branch=%(refname)'. The full name also avoids\n   \"heads/topic\" from %(refname:short) when a tag has the same name.\n\n * <branch> is an exact name, not a pattern, because \"git branch\"\n   already has --list <pattern>, --merged, --contains and friends.\n   An invalid name dies, as for \"git branch -d\". A valid name that no\n   worktree has checked out lists nothing.\n\n * Eric, a \"git worktree foreach\" would be nice, and it could select\n   worktrees with this same --branch. I left it out of this patch.\n\n[1] https://lore.kernel.org/git/20261021173350.67854-1-me@jaspermayone.com/\n\n Documentation/git-worktree.adoc |  21 ++++-\n builtin/worktree.c              |  79 ++++++++++++++++++-\n t/t2402-worktree-list.sh        | 131 ++++++++++++++++++++++++++++++++\n 3 files changed, 228 insertions(+), 3 deletions(-)\n\ndiff --git a/Documentation/git-worktree.adoc b/Documentation/git-worktree.adoc\nindex e6e77252ab..4db11eab88 100644\n--- a/Documentation/git-worktree.adoc\n+++ b/Documentation/git-worktree.adoc\n@@ -11,7 +11,7 @@ SYNOPSIS\n [synopsis]\n git worktree add [-f] [--detach] [--checkout] [--lock [--reason <string>]]\n \t\t [--orphan] [(-b | -B) <new-branch>] <path> [<commit-ish>]\n-git worktree list [-v | --porcelain [-z]]\n+git worktree list [-v | --porcelain [-z]] [--branch=<branch>...]\n git worktree lock [--reason <string>] <worktree>\n git worktree move <worktree> <new-path>\n git worktree prune [-n] [-v] [--expire <expire>]\n@@ -114,6 +114,9 @@ whether the worktree is bare, the revision currently checked out, the\n branch currently checked out (or \"detached HEAD\" if none), \"locked\" if\n the worktree is locked, \"prunable\" if the worktree can be pruned by the\n `prune` command.\n++\n+With `--branch`, only the worktrees that have one of the given branches\n+checked out are listed.\n \n `lock`::\n \n@@ -268,6 +271,22 @@ mismatch, even if the links are correct.\n \tto parse the output when a worktree path contains a newline\n \tcharacter.\n \n+`--branch=<branch>`::\n+\tWith `list`, only list worktrees that have _<branch>_ checked\n+\tout, including a worktree that is rebasing or bisecting\n+\t_<branch>_. _<branch>_ is a branch name, such as `topic` or\n+\t`@{-1}`, or a full name under `refs/heads/`. The option can be\n+\tgiven more than once to list the worktrees of several branches.\n+\tA detached worktree is never listed.\n++\n+Use linkgit:git-branch[1] to choose the branches. For example, to list\n+the worktrees whose branch is merged into `origin/main`:\n++\n+------------\n+$ git worktree list \\\n+\t$(git branch --format='--branch=%(refname)' --merged origin/main)\n+------------\n+\n `-q`::\n `--quiet`::\n \tWith `add`, suppress feedback messages.\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 77ecd0f71f..1d7db5f7fc 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -15,6 +15,7 @@\n #include \"object-name.h\"\n #include \"parse-options.h\"\n #include \"path.h\"\n+#include \"strmap.h\"\n #include \"strvec.h\"\n #include \"branch.h\"\n #include \"read-cache-ll.h\"\n@@ -26,6 +27,7 @@\n #include \"submodule.h\"\n #include \"utf8.h\"\n #include \"worktree.h\"\n+#include \"wt-status.h\"\n #include \"quote.h\"\n \n #define BUILTIN_WORKTREE_ADD_USAGE \\\n@@ -33,7 +35,7 @@\n \t   \"                 [--orphan] [(-b | -B) <new-branch>] <path> [<commit-ish>]\")\n \n #define BUILTIN_WORKTREE_LIST_USAGE \\\n-\tN_(\"git worktree list [-v | --porcelain [-z]]\")\n+\tN_(\"git worktree list [-v | --porcelain [-z]] [--branch=<branch>...]\")\n #define BUILTIN_WORKTREE_LOCK_USAGE \\\n \tN_(\"git worktree lock [--reason <string>] <worktree>\")\n #define BUILTIN_WORKTREE_MOVE_USAGE \\\n@@ -1125,11 +1127,78 @@ static void pathsort(struct worktree **wt)\n \tQSORT(wt, n, pathcmp);\n }\n \n+/*\n+ * Return the full name of the branch that \"wt\" has checked out, or\n+ * NULL. A worktree that is rebasing or bisecting a branch has it\n+ * checked out, too, as for is_shared_symref().\n+ */\n+static char *checked_out_branch(const struct worktree *wt)\n+{\n+\tstruct wt_status_state state = { 0 };\n+\tchar *ref = NULL;\n+\n+\tif (wt->is_bare)\n+\t\treturn NULL;\n+\tif (!wt->is_detached)\n+\t\treturn xstrdup_or_null(wt->head_ref);\n+\n+\tif (wt_status_check_rebase(wt, &state) &&\n+\t    (state.rebase_in_progress || state.rebase_interactive_in_progress) &&\n+\t    state.branch)\n+\t\tref = xstrfmt(\"refs/heads/%s\", state.branch);\n+\telse if (wt_status_check_bisect(wt, &state) && state.bisecting_from)\n+\t\tref = xstrfmt(\"refs/heads/%s\", state.bisecting_from);\n+\twt_status_state_free_buffers(&state);\n+\treturn ref;\n+}\n+\n+/*\n+ * Keep only the worktrees that have one of the \"--branch\" branches\n+ * checked out. A branch is given by its name, or by its full name\n+ * under refs/heads/.\n+ */\n+static void filter_by_branch(struct repository *repo, struct worktree **wt,\n+\t\t\t     const struct string_list *branches)\n+{\n+\tstruct strset refnames = STRSET_INIT;\n+\tstruct string_list_item *item;\n+\tstruct strbuf ref = STRBUF_INIT;\n+\tsize_t kept = 0;\n+\n+\tfor_each_string_list_item(item, branches) {\n+\t\tstrbuf_reset(&ref);\n+\t\tif (starts_with(item->string, \"refs/heads/\")) {\n+\t\t\tstrbuf_addstr(&ref, item->string);\n+\t\t\tif (check_refname_format(ref.buf, 0))\n+\t\t\t\tdie(_(\"'%s' is not a valid branch name\"),\n+\t\t\t\t    item->string);\n+\t\t} else if (check_branch_ref(repo, &ref, item->string)) {\n+\t\t\tdie(_(\"'%s' is not a valid branch name\"), item->string);\n+\t\t}\n+\t\tstrset_add(&refnames, ref.buf);\n+\t}\n+\n+\tfor (size_t i = 0; wt[i]; i++) {\n+\t\tchar *branch = checked_out_branch(wt[i]);\n+\n+\t\tif (branch && strset_contains(&refnames, branch))\n+\t\t\twt[kept++] = wt[i];\n+\t\telse\n+\t\t\tfree_worktree(wt[i]);\n+\t\tfree(branch);\n+\t}\n+\twt[kept] = NULL;\n+\n+\tstrbuf_release(&ref);\n+\tstrset_clear(&refnames);\n+}\n+\n static int list(int ac, const char **av, const char *prefix,\n-\t\tstruct repository *repo UNUSED)\n+\t\tstruct repository *repo)\n {\n \tint porcelain = 0;\n \tint line_terminator = '\\n';\n+\tstruct string_list branches = STRING_LIST_INIT_DUP;\n \n \tstruct option options[] = {\n \t\tOPT_BOOL(0, \"porcelain\", &porcelain, N_(\"machine-readable output\")),\n@@ -1138,6 +1207,8 @@ static int list(int ac, const char **av, const char *prefix,\n \t\t\t\tN_(\"add 'prunable' annotation to missing worktrees older than <time>\")),\n \t\tOPT_SET_INT('z', NULL, &line_terminator,\n \t\t\t    N_(\"terminate records with a NUL character\"), '\\0'),\n+\t\tOPT_STRING_LIST(0, \"branch\", &branches, N_(\"branch\"),\n+\t\t\t\tN_(\"only show worktrees that have <branch> checked out\")),\n \t\tOPT_END()\n \t};\n \n@@ -1157,6 +1228,9 @@ static int list(int ac, const char **av, const char *prefix,\n \t\t/* sort worktrees by path but keep main worktree at top */\n \t\tpathsort(worktrees + 1);\n \n+\t\tif (branches.nr)\n+\t\t\tfilter_by_branch(repo, worktrees, &branches);\n+\n \t\tif (!porcelain)\n \t\t\tmeasure_widths(worktrees, &abbrev,\n \t\t\t\t       &display, &path_maxwidth);\n@@ -1174,6 +1248,7 @@ static int list(int ac, const char **av, const char *prefix,\n \t\tfree(display);\n \t\tfree_worktrees(worktrees);\n \t}\n+\tstring_list_clear(&branches, 0);\n \treturn 0;\n }\n \ndiff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh\nindex eeaf800d74..ea08bf79fa 100755\n--- a/t/t2402-worktree-list.sh\n+++ b/t/t2402-worktree-list.sh\n@@ -318,4 +318,135 @@ test_expect_success 'worktree path when called in .git directory' '\n \ttest_cmp list1 list2\n '\n \n+test_expect_success 'setup for \"list --branch\"' '\n+\tgit init by-branch &&\n+\t(\n+\t\tcd by-branch &&\n+\t\ttest_commit base &&\n+\t\tgit branch topic-a &&\n+\t\tgit branch topic-b &&\n+\t\tgit branch unused &&\n+\t\tgit worktree add ../wt-a topic-a &&\n+\t\tgit worktree add ../wt-b topic-b &&\n+\t\tgit worktree add --detach ../wt-detached main &&\n+\t\tgit -C ../wt-a commit --allow-empty -m a &&\n+\t\tgit -C ../wt-b commit --allow-empty -m b &&\n+\t\tgit merge --ff-only topic-a\n+\t)\n+'\n+\n+list_paths () {\n+\tgit -C by-branch worktree list --porcelain \"$@\" >out &&\n+\tsed -n \"s/^worktree //p\" out >actual\n+}\n+\n+test_expect_success '\"list --branch\" shows the worktree of that branch' '\n+\tlist_paths --branch=topic-a &&\n+\techo \"$(pwd)/wt-a\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '\"list --branch\" can be repeated' '\n+\tlist_paths --branch=topic-b --branch=main --branch=topic-a &&\n+\tcat >expect <<-EOF &&\n+\t$(pwd)/by-branch\n+\t$(pwd)/wt-a\n+\t$(pwd)/wt-b\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '\"list --branch\" with a branch that is not checked out' '\n+\tlist_paths --branch=unused --branch=no-such-branch &&\n+\ttest_must_be_empty actual\n+'\n+\n+test_expect_success '\"list --branch\" never shows a detached worktree' '\n+\tlist_paths --branch=main &&\n+\techo \"$(pwd)/by-branch\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '\"list --branch\" rejects an invalid branch name' '\n+\ttest_must_fail git -C by-branch worktree list --branch=-bad 2>err &&\n+\ttest_grep \"is not a valid branch name\" err\n+'\n+\n+test_expect_success '\"list --branch\" keeps the normal output format' '\n+\tgit -C by-branch worktree list --branch=topic-a >out &&\n+\tsed \"s/  */ /g\" <out >actual &&\n+\techo \"$(pwd)/wt-a $(git -C by-branch rev-parse --short topic-a) [topic-a]\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '\"list --branch\" works with -z' '\n+\tgit -C by-branch worktree list --porcelain -z --branch=topic-b >out &&\n+\ttr \"\\0\" \"\\n\" <out >actual &&\n+\tcat >expect <<-EOF &&\n+\tworktree $(pwd)/wt-b\n+\tHEAD $(git -C by-branch rev-parse topic-b)\n+\tbranch refs/heads/topic-b\n+\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '\"list --branch\" shows a worktree bisecting that branch' '\n+\ttest_when_finished \"git -C wt-b bisect reset && git -C wt-b reset --hard HEAD~2\" &&\n+\tgit -C wt-b commit --allow-empty -m b1 &&\n+\tgit -C wt-b commit --allow-empty -m b2 &&\n+\tgit -C wt-b bisect start topic-b topic-b~2 &&\n+\ttest_must_fail git -C wt-b symbolic-ref -q HEAD &&\n+\tlist_paths --branch=topic-b &&\n+\techo \"$(pwd)/wt-b\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '\"list --branch\" shows a worktree rebasing that branch' '\n+\ttest_when_finished \"git -C wt-b rebase --abort\" &&\n+\ttest_must_fail git -C wt-b rebase -f -x false base &&\n+\ttest_must_fail git -C wt-b symbolic-ref -q HEAD &&\n+\tlist_paths --branch=topic-b &&\n+\techo \"$(pwd)/wt-b\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '\"list --branch\" accepts a full branch name' '\n+\tlist_paths --branch=refs/heads/topic-a &&\n+\techo \"$(pwd)/wt-a\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '\"list --branch\" with a tag of the same name' '\n+\ttest_when_finished \"git -C by-branch tag -d topic-a\" &&\n+\tgit -C by-branch tag topic-a base &&\n+\tlist_paths --branch=topic-a &&\n+\techo \"$(pwd)/wt-a\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '\"list --branch\" shows unborn and prunable worktrees' '\n+\ttest_when_finished \"rm -rf wt-unborn wt-gone && git -C by-branch worktree prune\" &&\n+\tgit -C by-branch worktree add --orphan -b unborn ../wt-unborn &&\n+\tgit -C by-branch branch gone &&\n+\tgit -C by-branch worktree add ../wt-gone gone &&\n+\trm -rf wt-gone &&\n+\tgit -C by-branch worktree list -v --branch=unborn --branch=gone >out &&\n+\ttest_grep \"^$(pwd)/wt-unborn .*\\[unborn\\]$\" out &&\n+\ttest_grep \"^$(pwd)/wt-gone .*\\[gone\\]$\" out &&\n+\ttest_grep \"prunable: gitdir file points to non-existent location\" out\n+'\n+\n+test_expect_success '\"list --branch\" with \"git branch --merged\"' '\n+\tgit -C by-branch worktree list --porcelain \\\n+\t\t$(git -C by-branch branch --format=\"--branch=%(refname)\" \\\n+\t\t\t--merged main) >out &&\n+\tsed -n \"s/^worktree //p\" out >actual &&\n+\tcat >expect <<-EOF &&\n+\t$(pwd)/by-branch\n+\t$(pwd)/wt-a\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n test_done\n\nbase-commit: 7e34d8174dcda3435d1404c9a30bef46afe6c13e\n-- \n2.56.0\n\n\n"},{"id":"556212","messageId":"e08fd267-7017-493f-8f1b-d8dfb568b5fd@gmail.com","threadId":"66626","inReplyTo":"20261024165311.57387-1-me@jaspermayone.com","subject":"Re: [PATCH v2] worktree list: add --branch to select worktrees by branch","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-10-25T14:47:13Z","receivedAt":"2026-10-25T14:47:13Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Jasper\n\nOn 24/10/2026 17:53, Jasper Mayone wrote:\n> There is no direct way to ask which worktree has a given branch\n> checked out. \"git worktree list --porcelain\" prints the branch of\n> every worktree, so a script has to parse the whole list and match the\n> \"branch\" lines itself.\n> > Teach \"git worktree list\" a \"--branch=<branch>\" option that lists\n> only the worktrees that have <branch> checked out. It can be given\n> more than once.\n\nPlaying 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.\n\n> A worktree that is rebasing or bisecting a branch\n> counts as having it checked out, as it does for \"git branch -d\" and\n> \"git worktree add\". <branch> is a branch name, or a full name under\n> refs/heads/.\n\nThat'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.\n\n> This keeps the choice of branches in \"git branch\", which already has\n> --merged, --contains, --points-at and patterns.\n\nThis sentence seems to assume the reader has some context from the previous iteration\n\n> For example, to list\n> the worktrees whose branch is merged into origin/main:\n> >      git worktree list \\\n> \t$(git branch --format='--branch=%(refname)' --merged origin/main)\n> > The --format is needed because the default output of \"git branch\"\n> marks the current branch with \"*\" and the branches checked out in\n> other worktrees with \"+\". It uses the full name because\n> %(refname:short) gives \"heads/topic\" when a tag is also called\n> \"topic\".\n> > Each worktree's branch is found once and looked up in a set of the\n> requested branches, so feeding it every branch of a large repository\n> stays cheap.\n\nThe 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\n\nconst struct worktree *branch_find_checkout(const char *branch);\n\nin 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.\n\nAnother 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.\n\nIs there a reason we reject syntactically invalid branch names, but accept the names of branches that do not exist?\n\t\n> A detached worktree has no branch, so it is never listed.\n\nI'm not sure that sentence is really needed.\n\nThanks\n\nPhillip\n\n> Suggested-by: Junio C Hamano <gitster@pobox.com>\n> Signed-off-by: Jasper Mayone <me@jaspermayone.com>\n> ---\n> Changes since the RFC [1]:\n> >   * Junio pointed out that --merged and --no-merged narrow the problem\n>     to one workflow, and suggested selecting worktrees by branch\n>     instead, with \"git branch\" doing the choosing. Eric had the same\n>     reaction. This version does that and drops --merged, --no-merged\n>     and the reachability code.\n> >   * Junio's example piped \"git branch\" through sed. Its default output\n>     prefixes the current branch with \"*\" and branches checked out in\n>     other worktrees with \"+\", which are the branches this is for, so\n>     the words come out as \"--branch=+\" and \"topic\". The documented form\n>     uses --format='--branch=%(refname)'. The full name also avoids\n>     \"heads/topic\" from %(refname:short) when a tag has the same name.\n> >   * <branch> is an exact name, not a pattern, because \"git branch\"\n>     already has --list <pattern>, --merged, --contains and friends.\n>     An invalid name dies, as for \"git branch -d\". A valid name that no\n>     worktree has checked out lists nothing.\n> >   * Eric, a \"git worktree foreach\" would be nice, and it could select\n>     worktrees with this same --branch. I left it out of this patch.\n> > [1] https://lore.kernel.org/git/20261021173350.67854-1-me@jaspermayone.com/\n> >   Documentation/git-worktree.adoc |  21 ++++-\n>   builtin/worktree.c              |  79 ++++++++++++++++++-\n>   t/t2402-worktree-list.sh        | 131 ++++++++++++++++++++++++++++++++\n>   3 files changed, 228 insertions(+), 3 deletions(-)\n> > diff --git a/Documentation/git-worktree.adoc b/Documentation/git-worktree.adoc\n> index e6e77252ab..4db11eab88 100644\n> --- a/Documentation/git-worktree.adoc\n> +++ b/Documentation/git-worktree.adoc\n> @@ -11,7 +11,7 @@ SYNOPSIS\n>   [synopsis]\n>   git worktree add [-f] [--detach] [--checkout] [--lock [--reason <string>]]\n>   \t\t [--orphan] [(-b | -B) <new-branch>] <path> [<commit-ish>]\n> -git worktree list [-v | --porcelain [-z]]\n> +git worktree list [-v | --porcelain [-z]] [--branch=<branch>...]\n>   git worktree lock [--reason <string>] <worktree>\n>   git worktree move <worktree> <new-path>\n>   git worktree prune [-n] [-v] [--expire <expire>]\n> @@ -114,6 +114,9 @@ whether the worktree is bare, the revision currently checked out, the\n>   branch currently checked out (or \"detached HEAD\" if none), \"locked\" if\n>   the worktree is locked, \"prunable\" if the worktree can be pruned by the\n>   `prune` command.\n> ++\n> +With `--branch`, only the worktrees that have one of the given branches\n> +checked out are listed.\n>   >   `lock`::\n>   > @@ -268,6 +271,22 @@ mismatch, even if the links are correct.\n>   \tto parse the output when a worktree path contains a newline\n>   \tcharacter.\n>   > +`--branch=<branch>`::\n> +\tWith `list`, only list worktrees that have _<branch>_ checked\n> +\tout, including a worktree that is rebasing or bisecting\n> +\t_<branch>_. _<branch>_ is a branch name, such as `topic` or\n> +\t`@{-1}`, or a full name under `refs/heads/`. The option can be\n> +\tgiven more than once to list the worktrees of several branches.\n> +\tA detached worktree is never listed.\n> ++\n> +Use linkgit:git-branch[1] to choose the branches. For example, to list\n> +the worktrees whose branch is merged into `origin/main`:\n> ++\n> +------------\n> +$ git worktree list \\\n> +\t$(git branch --format='--branch=%(refname)' --merged origin/main)\n> +------------\n> +\n>   `-q`::\n>   `--quiet`::\n>   \tWith `add`, suppress feedback messages.\n> diff --git a/builtin/worktree.c b/builtin/worktree.c\n> index 77ecd0f71f..1d7db5f7fc 100644\n> --- a/builtin/worktree.c\n> +++ b/builtin/worktree.c\n> @@ -15,6 +15,7 @@\n>   #include \"object-name.h\"\n>   #include \"parse-options.h\"\n>   #include \"path.h\"\n> +#include \"strmap.h\"\n>   #include \"strvec.h\"\n>   #include \"branch.h\"\n>   #include \"read-cache-ll.h\"\n> @@ -26,6 +27,7 @@\n>   #include \"submodule.h\"\n>   #include \"utf8.h\"\n>   #include \"worktree.h\"\n> +#include \"wt-status.h\"\n>   #include \"quote.h\"\n>   >   #define BUILTIN_WORKTREE_ADD_USAGE \\\n> @@ -33,7 +35,7 @@\n>   \t   \"                 [--orphan] [(-b | -B) <new-branch>] <path> [<commit-ish>]\")\n>   >   #define BUILTIN_WORKTREE_LIST_USAGE \\\n> -\tN_(\"git worktree list [-v | --porcelain [-z]]\")\n> +\tN_(\"git worktree list [-v | --porcelain [-z]] [--branch=<branch>...]\")\n>   #define BUILTIN_WORKTREE_LOCK_USAGE \\\n>   \tN_(\"git worktree lock [--reason <string>] <worktree>\")\n>   #define BUILTIN_WORKTREE_MOVE_USAGE \\\n> @@ -1125,11 +1127,78 @@ static void pathsort(struct worktree **wt)\n>   \tQSORT(wt, n, pathcmp);\n>   }\n>   > +/*\n> + * Return the full name of the branch that \"wt\" has checked out, or\n> + * NULL. A worktree that is rebasing or bisecting a branch has it\n> + * checked out, too, as for is_shared_symref().\n> + */\n> +static char *checked_out_branch(const struct worktree *wt)\n> +{\n> +\tstruct wt_status_state state = { 0 };\n> +\tchar *ref = NULL;\n> +\n> +\tif (wt->is_bare)\n> +\t\treturn NULL;\n> +\tif (!wt->is_detached)\n> +\t\treturn xstrdup_or_null(wt->head_ref);\n> +\n> +\tif (wt_status_check_rebase(wt, &state) &&\n> +\t    (state.rebase_in_progress || state.rebase_interactive_in_progress) &&\n> +\t    state.branch)\n> +\t\tref = xstrfmt(\"refs/heads/%s\", state.branch);\n> +\telse if (wt_status_check_bisect(wt, &state) && state.bisecting_from)\n> +\t\tref = xstrfmt(\"refs/heads/%s\", state.bisecting_from);\n> +\twt_status_state_free_buffers(&state);\n> +\treturn ref;\n> +}\n> +\n> +/*\n> + * Keep only the worktrees that have one of the \"--branch\" branches\n> + * checked out. A branch is given by its name, or by its full name\n> + * under refs/heads/.\n> + */\n> +static void filter_by_branch(struct repository *repo, struct worktree **wt,\n> +\t\t\t     const struct string_list *branches)\n> +{\n> +\tstruct strset refnames = STRSET_INIT;\n> +\tstruct string_list_item *item;\n> +\tstruct strbuf ref = STRBUF_INIT;\n> +\tsize_t kept = 0;\n> +\n> +\tfor_each_string_list_item(item, branches) {\n> +\t\tstrbuf_reset(&ref);\n> +\t\tif (starts_with(item->string, \"refs/heads/\")) {\n> +\t\t\tstrbuf_addstr(&ref, item->string);\n> +\t\t\tif (check_refname_format(ref.buf, 0))\n> +\t\t\t\tdie(_(\"'%s' is not a valid branch name\"),\n> +\t\t\t\t    item->string);\n> +\t\t} else if (check_branch_ref(repo, &ref, item->string)) {\n> +\t\t\tdie(_(\"'%s' is not a valid branch name\"), item->string);\n> +\t\t}\n> +\t\tstrset_add(&refnames, ref.buf);\n> +\t}\n> +\n> +\tfor (size_t i = 0; wt[i]; i++) {\n> +\t\tchar *branch = checked_out_branch(wt[i]);\n> +\n> +\t\tif (branch && strset_contains(&refnames, branch))\n> +\t\t\twt[kept++] = wt[i];\n> +\t\telse\n> +\t\t\tfree_worktree(wt[i]);\n> +\t\tfree(branch);\n> +\t}\n> +\twt[kept] = NULL;\n> +\n> +\tstrbuf_release(&ref);\n> +\tstrset_clear(&refnames);\n> +}\n> +\n>   static int list(int ac, const char **av, const char *prefix,\n> -\t\tstruct repository *repo UNUSED)\n> +\t\tstruct repository *repo)\n>   {\n>   \tint porcelain = 0;\n>   \tint line_terminator = '\\n';\n> +\tstruct string_list branches = STRING_LIST_INIT_DUP;\n>   >   \tstruct option options[] = {\n>   \t\tOPT_BOOL(0, \"porcelain\", &porcelain, N_(\"machine-readable output\")),\n> @@ -1138,6 +1207,8 @@ static int list(int ac, const char **av, const char *prefix,\n>   \t\t\t\tN_(\"add 'prunable' annotation to missing worktrees older than <time>\")),\n>   \t\tOPT_SET_INT('z', NULL, &line_terminator,\n>   \t\t\t    N_(\"terminate records with a NUL character\"), '\\0'),\n> +\t\tOPT_STRING_LIST(0, \"branch\", &branches, N_(\"branch\"),\n> +\t\t\t\tN_(\"only show worktrees that have <branch> checked out\")),\n>   \t\tOPT_END()\n>   \t};\n>   > @@ -1157,6 +1228,9 @@ static int list(int ac, const char **av, const char *prefix,\n>   \t\t/* sort worktrees by path but keep main worktree at top */\n>   \t\tpathsort(worktrees + 1);\n>   > +\t\tif (branches.nr)\n> +\t\t\tfilter_by_branch(repo, worktrees, &branches);\n> +\n>   \t\tif (!porcelain)\n>   \t\t\tmeasure_widths(worktrees, &abbrev,\n>   \t\t\t\t       &display, &path_maxwidth);\n> @@ -1174,6 +1248,7 @@ static int list(int ac, const char **av, const char *prefix,\n>   \t\tfree(display);\n>   \t\tfree_worktrees(worktrees);\n>   \t}\n> +\tstring_list_clear(&branches, 0);\n>   \treturn 0;\n>   }\n>   > diff --git a/t/t2402-worktree-list.sh b/t/t2402-worktree-list.sh\n> index eeaf800d74..ea08bf79fa 100755\n> --- a/t/t2402-worktree-list.sh\n> +++ b/t/t2402-worktree-list.sh\n> @@ -318,4 +318,135 @@ test_expect_success 'worktree path when called in .git directory' '\n>   \ttest_cmp list1 list2\n>   '\n>   > +test_expect_success 'setup for \"list --branch\"' '\n> +\tgit init by-branch &&\n> +\t(\n> +\t\tcd by-branch &&\n> +\t\ttest_commit base &&\n> +\t\tgit branch topic-a &&\n> +\t\tgit branch topic-b &&\n> +\t\tgit branch unused &&\n> +\t\tgit worktree add ../wt-a topic-a &&\n> +\t\tgit worktree add ../wt-b topic-b &&\n> +\t\tgit worktree add --detach ../wt-detached main &&\n> +\t\tgit -C ../wt-a commit --allow-empty -m a &&\n> +\t\tgit -C ../wt-b commit --allow-empty -m b &&\n> +\t\tgit merge --ff-only topic-a\n> +\t)\n> +'\n> +\n> +list_paths () {\n> +\tgit -C by-branch worktree list --porcelain \"$@\" >out &&\n> +\tsed -n \"s/^worktree //p\" out >actual\n> +}\n> +\n> +test_expect_success '\"list --branch\" shows the worktree of that branch' '\n> +\tlist_paths --branch=topic-a &&\n> +\techo \"$(pwd)/wt-a\" >expect &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success '\"list --branch\" can be repeated' '\n> +\tlist_paths --branch=topic-b --branch=main --branch=topic-a &&\n> +\tcat >expect <<-EOF &&\n> +\t$(pwd)/by-branch\n> +\t$(pwd)/wt-a\n> +\t$(pwd)/wt-b\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success '\"list --branch\" with a branch that is not checked out' '\n> +\tlist_paths --branch=unused --branch=no-such-branch &&\n> +\ttest_must_be_empty actual\n> +'\n> +\n> +test_expect_success '\"list --branch\" never shows a detached worktree' '\n> +\tlist_paths --branch=main &&\n> +\techo \"$(pwd)/by-branch\" >expect &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success '\"list --branch\" rejects an invalid branch name' '\n> +\ttest_must_fail git -C by-branch worktree list --branch=-bad 2>err &&\n> +\ttest_grep \"is not a valid branch name\" err\n> +'\n> +\n> +test_expect_success '\"list --branch\" keeps the normal output format' '\n> +\tgit -C by-branch worktree list --branch=topic-a >out &&\n> +\tsed \"s/  */ /g\" <out >actual &&\n> +\techo \"$(pwd)/wt-a $(git -C by-branch rev-parse --short topic-a) [topic-a]\" >expect &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success '\"list --branch\" works with -z' '\n> +\tgit -C by-branch worktree list --porcelain -z --branch=topic-b >out &&\n> +\ttr \"\\0\" \"\\n\" <out >actual &&\n> +\tcat >expect <<-EOF &&\n> +\tworktree $(pwd)/wt-b\n> +\tHEAD $(git -C by-branch rev-parse topic-b)\n> +\tbranch refs/heads/topic-b\n> +\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success '\"list --branch\" shows a worktree bisecting that branch' '\n> +\ttest_when_finished \"git -C wt-b bisect reset && git -C wt-b reset --hard HEAD~2\" &&\n> +\tgit -C wt-b commit --allow-empty -m b1 &&\n> +\tgit -C wt-b commit --allow-empty -m b2 &&\n> +\tgit -C wt-b bisect start topic-b topic-b~2 &&\n> +\ttest_must_fail git -C wt-b symbolic-ref -q HEAD &&\n> +\tlist_paths --branch=topic-b &&\n> +\techo \"$(pwd)/wt-b\" >expect &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success '\"list --branch\" shows a worktree rebasing that branch' '\n> +\ttest_when_finished \"git -C wt-b rebase --abort\" &&\n> +\ttest_must_fail git -C wt-b rebase -f -x false base &&\n> +\ttest_must_fail git -C wt-b symbolic-ref -q HEAD &&\n> +\tlist_paths --branch=topic-b &&\n> +\techo \"$(pwd)/wt-b\" >expect &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success '\"list --branch\" accepts a full branch name' '\n> +\tlist_paths --branch=refs/heads/topic-a &&\n> +\techo \"$(pwd)/wt-a\" >expect &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success '\"list --branch\" with a tag of the same name' '\n> +\ttest_when_finished \"git -C by-branch tag -d topic-a\" &&\n> +\tgit -C by-branch tag topic-a base &&\n> +\tlist_paths --branch=topic-a &&\n> +\techo \"$(pwd)/wt-a\" >expect &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success '\"list --branch\" shows unborn and prunable worktrees' '\n> +\ttest_when_finished \"rm -rf wt-unborn wt-gone && git -C by-branch worktree prune\" &&\n> +\tgit -C by-branch worktree add --orphan -b unborn ../wt-unborn &&\n> +\tgit -C by-branch branch gone &&\n> +\tgit -C by-branch worktree add ../wt-gone gone &&\n> +\trm -rf wt-gone &&\n> +\tgit -C by-branch worktree list -v --branch=unborn --branch=gone >out &&\n> +\ttest_grep \"^$(pwd)/wt-unborn .*\\[unborn\\]$\" out &&\n> +\ttest_grep \"^$(pwd)/wt-gone .*\\[gone\\]$\" out &&\n> +\ttest_grep \"prunable: gitdir file points to non-existent location\" out\n> +'\n> +\n> +test_expect_success '\"list --branch\" with \"git branch --merged\"' '\n> +\tgit -C by-branch worktree list --porcelain \\\n> +\t\t$(git -C by-branch branch --format=\"--branch=%(refname)\" \\\n> +\t\t\t--merged main) >out &&\n> +\tsed -n \"s/^worktree //p\" out >actual &&\n> +\tcat >expect <<-EOF &&\n> +\t$(pwd)/by-branch\n> +\t$(pwd)/wt-a\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n>   test_done\n> > base-commit: 7e34d8174dcda3435d1404c9a30bef46afe6c13e\n\n\n"},{"id":"556311","messageId":"74ba33b2-60f6-40cf-86e9-dac95876b1be@gmail.com","threadId":"66626","inReplyTo":"e08fd267-7017-493f-8f1b-d8dfb568b5fd@gmail.com","subject":"Re: [PATCH v2] worktree list: add --branch to select worktrees by branch","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-10-26T11:02:12Z","receivedAt":"2026-10-26T11:02:12Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"One further thought ...\n\nOn 25/10/2026 14:47, Phillip Wood wrote:\n> On 24/10/2026 17:53, Jasper Mayone wrote:\n> >> For example, to list\n>> the worktrees whose branch is merged into origin/main:\n>>\n>>      git worktree list \\\n>>     $(git branch --format='--branch=%(refname)' --merged origin/main)\n>>\n>> The --format is needed because the default output of \"git branch\"\n>> marks the current branch with \"*\" and the branches checked out in\n>> other worktrees with \"+\". It uses the full name because\n>> %(refname:short) gives \"heads/topic\" when a tag is also called\n>> \"topic\".\n\nI think you can use \"%(refname:lstrip=2)\" to always remove \"refs/heads/\" which means we could simplify the implementation and just accept branch names.\n\nThanks\n\nPhillip\n\n\n"},{"id":"556601","messageId":"CAPig+cT6rxKptqSEKuVbi2FafBqh+DfcHAkHfz2-=SH3y2hfyw@mail.gmail.com","threadId":"66626","inReplyTo":"e08fd267-7017-493f-8f1b-d8dfb568b5fd@gmail.com","subject":"Re: [PATCH v2] worktree list: add --branch to select worktrees by branch","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2026-10-28T06:02:55Z","receivedAt":"2026-10-28T06:02:55Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Sun, Oct 25, 2026 at 10:47 AM Phillip Wood <phillip.wood123@gmail.com> wrote:\n> On 24/10/2026 17:53, Jasper Mayone wrote:\n> > A worktree that is rebasing or bisecting a branch\n> > counts as having it checked out, as it does for \"git branch -d\" and\n> > \"git worktree add\". <branch> is a branch name, or a full name under\n> > refs/heads/.\n>\n> That's useful because \"git worktree list\" does not show branch names\n> when they're being rebased or bisected. The commit message should spell\n> that out.\n\nAside from Philip's excellent review comments, I have a relatively\nminor question:\n\nRather than naming it `--branch`, under the assumption that users may\neventually want to filter on other worktree properties, should this\nnew option be given a more generic name, such as\n`--filter=branch:<name>`?\n\nIf so, that potentially raises the question about how such filter\noptions should combine. It's obvious that multiple\n`--filter=branch:<name>` options combine via logical \"OR\", but a user\nmight want to combine future filter criteria via other logical\noperations (AND, NOT, grouping). Do other Git commands set precedent\nregarding filter option naming and logical operations?\n\nNote that I'm not suggesting that this patch should implement such\nadditional filtering behavior (it should not), or even that all these\nquestions need to be answered right now. I'm asking only, taking the\nabove into consideration, whether this option should be given a more\ngeneric name to avoid closing the door to those possible future\nenhancements.\n\n"},{"id":"556637","messageId":"ad504925-cdda-4de6-8791-97a51a9d90a3@app.fastmail.com","threadId":"66626","inReplyTo":"20261024165311.57387-1-me@jaspermayone.com","subject":"Re: [PATCH v2] worktree list: add --branch to select worktrees by branch","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2026-10-28T11:32:30Z","receivedAt":"2026-10-28T11:32:30Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Sat, Oct 24, 2026, at 18:53, Jasper Mayone wrote:\n> There is no direct way to ask which worktree has a given branch\n> checked out. \"git worktree list --porcelain\" prints the branch of\n> every worktree, so a script has to parse the whole list and match the\n> \"branch\" lines itself.\n\ngit-for-each-ref(1) (and git-branch(1) etc.) have `worktreepath`.\n\n    git worktree add ../test-branch\n    git worktree remove $(git for-each-ref \\\n        --format='%(worktreepath)' refs/heads/test-branch)\n    git branch -d test-branch\n\nIt will not list the worktree path if that branch is being rebased.\n\n>\n> Teach \"git worktree list\" a \"--branch=<branch>\" option that lists\n> only the worktrees that have <branch> checked out. It can be given\n> more than once. A worktree that is rebasing or bisecting a branch\n> counts as having it checked out, as it does for \"git branch -d\" and\n> \"git worktree add\". <branch> is a branch name, or a full name under\n> refs/heads/.\n>\n> This keeps the choice of branches in \"git branch\", which already has\n> --merged, --contains, --points-at and patterns. For example, to list\n> the worktrees whose branch is merged into origin/main:\n>\n>     git worktree list \\\n> \t$(git branch --format='--branch=%(refname)' --merged origin/main)\n>\n> The --format is needed because the default output of \"git branch\"\n> marks the current branch with \"*\" and the branches checked out in\n> other worktrees with \"+\". It uses the full name because\n> %(refname:short) gives \"heads/topic\" when a tag is also called\n> \"topic\".\n>\n> Each worktree's branch is found once and looked up in a set of the\n> requested branches, so feeding it every branch of a large repository\n> stays cheap.\n>\n> A detached worktree has no branch, so it is never listed.\n>\n> Suggested-by: Junio C Hamano <gitster@pobox.com>\n> Signed-off-by: Jasper Mayone <me@jaspermayone.com>\n> ---\n>[snip]\n\n"},{"id":"556675","messageId":"xmqqik2ldewp.fsf@gitster.g","threadId":"66626","inReplyTo":"CAPig+cT6rxKptqSEKuVbi2FafBqh+DfcHAkHfz2-=SH3y2hfyw@mail.gmail.com","subject":"Re: [PATCH v2] worktree list: add --branch to select worktrees by branch","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-10-28T15:27:02Z","receivedAt":"2026-10-28T15:27:02Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Eric Sunshine <sunshine@sunshineco.com> writes:\n\n> If so, that potentially raises the question about how such filter\n> options should combine. It's obvious that multiple\n> `--filter=branch:<name>` options combine via logical \"OR\", but a user\n> might want to combine future filter criteria via other logical\n> operations (AND, NOT, grouping). Do other Git commands set precedent\n> regarding filter option naming and logical operations?\n>\n> Note that I'm not suggesting that this patch should implement such\n> additional filtering behavior (it should not), or even that all these\n> questions need to be answered right now. I'm asking only, taking the\n> above into consideration, whether this option should be given a more\n> generic name to avoid closing the door to those possible future\n> enhancements.\n\nPerhaps, but at that point I suspect that the most general form is\nto allow you to write a callback function that is given the path to\nthe worktree and determines if you want the worktree to be included\nin the result.  Then\n\n\tgit worktree list --filter-command='\n\t\ttest 5 -lt $(git rev-list --count master..HEAD)\n\t'\n\nmay become a way to list worktrees with work that has more than 5\ncommits to be massaged down to 'master', for example.\n\n\"git branch --contains\" etc. can be expressed inside the framework,\nso \"--branch\" may become a special case for that more generalized\nmechanism.\n\nI dunno.\n\n"},{"id":"556727","messageId":"20261029044633.12429-1-me@jaspermayone.com","threadId":"66626","inReplyTo":"ad504925-cdda-4de6-8791-97a51a9d90a3@app.fastmail.com","subject":"Re: [PATCH v2] worktree list: add --branch to select worktrees by branch","fromName":"Jasper Mayone","fromEmail":"me@jaspermayone.com","sentAt":"2026-10-29T04:46:33Z","receivedAt":"2026-10-29T04:46:33Z","isPatch":true,"sender":{"key":"me@jaspermayone.com","avatar":null},"body":"\"Kristoffer Haugsbakk\" <kristofferhaugsbakk@fastmail.com> writes:\n\n> git-for-each-ref(1) (and git-branch(1) etc.) have `worktreepath`.\n>\n>     git worktree add ../test-branch\n>     git worktree remove $(git for-each-ref \\\n>         --format='%(worktreepath)' refs/heads/test-branch)\n>     git branch -d test-branch\n>\n> It will not list the worktree path if that branch is being rebased.\n\nThanks, I had missed %(worktreepath). With it, what I wanted from the\nstart is already one command:\n\n    git for-each-ref --merged origin/main \\\n        --format='%(worktreepath)' refs/heads/\n\nSo --branch adds little on top of that. The one real gap is the one\nyou name. While a branch is being rebased or bisected,\n%(worktreepath) is empty, \"git branch\" does not mark it with \"+\", and\n\"git worktree list\" shows that worktree as \"(detached HEAD)\". Yet\n\"git worktree add\" and \"git branch -d\" already treat that branch as\nchecked out.\n\nAs Phillip pointed out, branch.c already has this information:\nbranch_checked_out() covers a plain checkout, a rebase, a bisect and\n\"rebase --update-refs\".\n\nSo I would like to drop --branch and instead make %(worktreepath) use\nthe same information, so that it names the worktree of a branch that\nis being rebased or bisected. That is a smaller change with no new\noption, so the naming question Eric raised and the --filter-command\nidea Junio floated do not come up.\n\nIs that change in behavior acceptable? A script that reads\n%(worktreepath) would now get a path for a branch that is being\nrebased. I think that is a fix, since the rest of Git already\nconsiders that branch checked out, but I would like to hear if anyone\nrelies on the empty value.\n\nPhillip, thank you for the review. Your point that \"git worktree\nlist\" output does not say which entry is the main worktree still\nstands, and I can send that as a separate patch if it is wanted.\n\nThanks,\nJasper\n\n"},{"id":"556728","messageId":"CAPig+cS=u5pDyMxkT-cw+Wt3qfxuFuvmFm0Qm19_mg4JK+Mxdg@mail.gmail.com","threadId":"66626","inReplyTo":"20261029044633.12429-1-me@jaspermayone.com","subject":"Re: [PATCH v2] worktree list: add --branch to select worktrees by branch","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2026-10-29T05:01:03Z","receivedAt":"2026-10-29T05:01:03Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Thu, Oct 29, 2026 at 12:46 AM Jasper Mayone <me@jaspermayone.com> wrote:\n> Phillip, thank you for the review. Your point that \"git worktree\n> list\" output does not say which entry is the main worktree still\n> stands, and I can send that as a separate patch if it is wanted.\n\n`git worktree list` guarantees that the main worktree (or bare\nrepository) is the very first item; this behavior is documented. I\nthink Philip's point was that once filtering is engaged, you lose that\nguarantee: in a filtered list, if the main worktree has been filtered\naway, then the first item will not be the main worktree.\n\n"},{"id":"556763","messageId":"d0051192-8e86-403d-9aae-b988883ccfd9@gmail.com","threadId":"66626","inReplyTo":"20261029044633.12429-1-me@jaspermayone.com","subject":"Re: [PATCH v2] worktree list: add --branch to select worktrees by branch","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-10-29T11:14:42Z","receivedAt":"2026-10-29T11:14:42Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Jasper\n\nOn 29/10/2026 04:46, Jasper Mayone wrote:\n> > So I would like to drop --branch and instead make %(worktreepath) use\n> the same information, so that it names the worktree of a branch that\n> is being rebased or bisected. That is a smaller change with no new\n> option, so the naming question Eric raised and the --filter-command\n> idea Junio floated do not come up.\n\nThat would certainly be a useful improvement, there are a few differences compared to adding something to \"git worktree list\" though\n\n - \"git for-each-ref\" does not have an option for NUL terminated output\n   and %(worktreepath) may contain a newline. If is possible to work\n   round that using one of the quoting options, though it is a pain to\n   use those if you're not using one of the supported languages.\n\n - there is no way to know from the output whether deleting a worktree\n   will delete the whole repository.\n\n - I'm not sure that it handles branches that are checked out in more\n   than one worktree. For warning that a branch is in use somewhere that\n   does not matter, but if you want to track down all the places it is\n   checked out it does.\n\n> Is that change in behavior acceptable? A script that reads\n> %(worktreepath) would now get a path for a branch that is being\n> rebased. I think that is a fix, since the rest of Git already\n> considers that branch checked out, but I would like to hear if anyone\n> relies on the empty value.\n\nI think we can consider the change in behavior to be a bug fix.\n> Phillip, thank you for the review. Your point that \"git worktree\n> list\" output does not say which entry is the main worktree still\n> stands, and I can send that as a separate patch if it is wanted.\nAs 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.\n\nThanks\n\nPhillip\n\n"},{"id":"556790","messageId":"A0B73028-E3BF-45CE-8468-AA9B022C41F6@gmail.com","threadId":"66626","inReplyTo":"d0051192-8e86-403d-9aae-b988883ccfd9@gmail.com","subject":"Re: [PATCH v2] worktree list: add --branch to select worktrees by branch","fromName":"Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-10-29T14:23:39Z","receivedAt":"2026-10-29T14:23:39Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"\n> Le 29 oct. 2026 à 07:14, Phillip Wood <phillip.wood123@gmail.com> a écrit :\n> \n> ﻿Hi Jasper\n> \n>> On 29/10/2026 04:46, Jasper Mayone wrote:\n> \n>> Is that change in behavior acceptable? A script that reads\n>> %(worktreepath) would now get a path for a branch that is being\n>> rebased. I think that is a fix, since the rest of Git already\n>> considers that branch checked out, but I would like to hear if anyone\n>> relies on the empty value.\n> \n> I think we can consider the change in behavior to be a bug fix.\n\nIf we’re doing this in order to usefully feed it to worktree-remove, though,\ndo you really want to remove a worktree that is mid-rebase? I\nwould think that could have work you don’t want to throw away\nwithout considering. (Besides which, would it show up\nin --merged=origin? Hm.)\n\nI suppose I would expect remove to fail without a « force » flag if\nthe worktree is mid-rebase, but still.\n\n"},{"id":"556799","messageId":"ef4874c2-c619-4773-9399-08c48503c3e3@gmail.com","threadId":"66626","inReplyTo":"A0B73028-E3BF-45CE-8468-AA9B022C41F6@gmail.com","subject":"Re: [PATCH v2] worktree list: add --branch to select worktrees by branch","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-10-29T16:10:03Z","receivedAt":"2026-10-29T16:10:03Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 29/10/2026 14:23, Ben Knoble wrote:\n> >> Le 29 oct. 2026 à 07:14, Phillip Wood <phillip.wood123@gmail.com> a écrit :\n>>\n>> ﻿Hi Jasper\n>>\n>>> On 29/10/2026 04:46, Jasper Mayone wrote:\n>>\n>>> Is that change in behavior acceptable? A script that reads\n>>> %(worktreepath) would now get a path for a branch that is being\n>>> rebased. I think that is a fix, since the rest of Git already\n>>> considers that branch checked out, but I would like to hear if anyone\n>>> relies on the empty value.\n>>\n>> I think we can consider the change in behavior to be a bug fix.\n> > If we’re doing this in order to usefully feed it to worktree-remove, though,\n> do you really want to remove a worktree that is mid-rebase? I\n> would think that could have work you don’t want to throw away\n> without considering. (Besides which, would it show up\n> in --merged=origin? Hm.)\n> > I suppose I would expect remove to fail without a « force » flag if\n> the worktree is mid-rebase, but still.\n\nThat's an interesting point. I agree \"git worktree remove\" shouldn't remove a worktree that's in the middle of a rebase or bisect. To me that is a bug in \"git worktree remove\" that is more likely to be triggered if we start reporting the worktree where branches are being rebased.\n\nThanks\n\nPhillip\n\n\n"},{"id":"556918","messageId":"20261030142137.54750-1-me@jaspermayone.com","threadId":"66626","inReplyTo":"20261024165311.57387-1-me@jaspermayone.com","subject":"[PATCH 0/2] worktree: treat a branch being rebased or bisected as checked out","fromName":"Jasper Mayone","fromEmail":"me@jaspermayone.com","sentAt":"2026-10-30T14:21:35Z","receivedAt":"2026-10-30T14:21:35Z","isPatch":true,"sender":{"key":"me@jaspermayone.com","avatar":null},"body":"This replaces my \"git worktree list --branch\" patch [1]. In that\nthread Kristoffer pointed out that \"git for-each-ref --merged <commit>\n--format='%(worktreepath)' refs/heads/\" already finds the worktrees of\nmerged branches, so a new option is not needed. What is missing is\nthat %(worktreepath) ignores a branch while a worktree is rebasing or\nbisecting it, although \"git worktree add\" and \"git branch -d\" already\ntreat that branch as checked out. Phillip agreed that this is a bug.\n\nBen then asked what happens if such a worktree is passed to \"git\nworktree remove\". Today it is removed, and the rebase or bisect state\nis lost with it, if the files happen to be clean. Phillip called that\na bug too, and more likely to be hit once %(worktreepath) reports\nthese worktrees, so it is fixed first.\n\n 1/2 makes \"git worktree remove\" refuse a worktree in the middle of a\n     rebase, \"git am\", bisect, or cherry-pick or revert sequence,\n     unless --force is given.\n\n 2/2 makes %(worktreepath), and so the \"+\" marker of \"git branch\",\n     name the worktree that is rebasing or bisecting a branch, as\n     prepare_checked_out_branches() in branch.c does. Branches that\n     \"rebase --update-refs\" will move are not included.\n\nTwo things reviewers may want to decide differently:\n\n * In the worktree that is doing the rebase, \"git branch\" now shows\n   both \"* (no branch, rebasing topic)\" and \"+ topic\". I documented\n   that rather than special-case the current worktree in the format\n   that \"git branch\" builds, because %(worktreepath) itself should not\n   depend on where it is run. If you would rather hide the \"+\" there,\n   I can do that in \"git branch\" instead.\n\n * A merge stopped with --no-commit is not checked by 1/2. Its staged\n   changes already make \"git status\" report the worktree as unclean in\n   the common case.\n\n[1] https://lore.kernel.org/git/20261024165311.57387-1-me@jaspermayone.com/\n\nJasper Mayone (2):\n  worktree remove: refuse a worktree in the middle of a rebase or bisect\n  ref-filter: name the worktree of a branch being rebased or bisected\n\n Documentation/git-branch.adoc       |  5 ++-\n Documentation/git-for-each-ref.adoc |  4 +-\n Documentation/git-worktree.adoc     | 11 +++--\n builtin/worktree.c                  | 31 +++++++++++++-\n ref-filter.c                        | 60 +++++++++++++++++++++------\n t/t2403-worktree-move.sh            | 59 +++++++++++++++++++++++++++\n t/t3203-branch-output.sh            | 35 ++++++++++++++++\n t/t6302-for-each-ref-filter.sh      | 63 +++++++++++++++++++++++++++++\n 8 files changed, 249 insertions(+), 19 deletions(-)\n\n\nbase-commit: b9e5cff1803ab24582201ef059516eb16014b9d9\n-- \n2.56.0\n\n\n"},{"id":"556919","messageId":"20261030142137.54750-2-me@jaspermayone.com","threadId":"66626","inReplyTo":"20261030142137.54750-1-me@jaspermayone.com","subject":"[PATCH 1/2] worktree remove: refuse a worktree in the middle of a rebase or bisect","fromName":"Jasper Mayone","fromEmail":"me@jaspermayone.com","sentAt":"2026-10-30T14:21:36Z","receivedAt":"2026-10-30T14:21:36Z","isPatch":true,"sender":{"key":"me@jaspermayone.com","avatar":null},"body":"\"git worktree remove\" refuses a worktree with modified or untracked\nfiles unless --force is given, but it removes a worktree that is in\nthe middle of a rebase, a \"git am\" session, a bisect or a cherry-pick\nor revert sequence when its files happen to be clean. The state of\nthat operation lives in the worktree's own $GIT_DIR, so it is lost\nwith the worktree. A rebase stopped at an \"edit\" or \"break\" line, an\n\"exec\" that failed, or a cherry-pick sequence after a conflict was\nresolved and committed is clean in this sense and still has work in\nprogress.\n\nTreat such a worktree as unclean: refuse to remove it unless --force\nis given, and name the operation in the message.\n\nA worktree whose directory is already gone is still removed without\n--force, as before: the check is part of the check for a clean\nworktree, which is only done when the directory exists.\n\nReported-by: D. Ben Knoble <ben.knoble@gmail.com>\nHelped-by: Phillip Wood <phillip.wood123@gmail.com>\nSigned-off-by: Jasper Mayone <me@jaspermayone.com>\n---\n Documentation/git-worktree.adoc | 11 +++---\n builtin/worktree.c              | 31 ++++++++++++++++-\n t/t2403-worktree-move.sh        | 59 +++++++++++++++++++++++++++++++++\n 3 files changed, 96 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/git-worktree.adoc b/Documentation/git-worktree.adoc\nindex e6e77252ab..da4c38f909 100644\n--- a/Documentation/git-worktree.adoc\n+++ b/Documentation/git-worktree.adoc\n@@ -142,9 +142,10 @@ the new working tree location.\n `remove`::\n \n Remove a worktree. Only clean worktrees (no untracked files and no\n-modification in tracked files) can be removed. Unclean worktrees or ones\n-with submodules can be removed with `--force`. The main worktree cannot be\n-removed.\n+modification in tracked files) can be removed. A worktree in the middle\n+of a rebase, a `git am` session, a bisect or a cherry-pick or revert\n+sequence is not clean either, because removing it loses that state. Unclean worktrees or ones with submodules\n+can be removed with `--force`. The main worktree cannot be removed.\n \n `repair [<path>...]`::\n \n@@ -189,7 +190,9 @@ twice. If the destination is already assigned to some other worktree but is\n missing (for instance, if _<new-path>_ was deleted manually), then `--force`\n allows the move to proceed; use `--force` twice if the destination is locked.\n +\n-`remove` refuses to remove an unclean worktree unless `--force` is used.\n+`remove` refuses to remove an unclean worktree, or one in the middle of a\n+rebase, `git am` session, bisect, or cherry-pick or revert sequence,\n+unless `--force` is used.\n To remove a locked worktree, specify `--force` twice.\n \n `-b <new-branch>`::\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 77ecd0f71f..ed08de1f43 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -26,6 +26,7 @@\n #include \"submodule.h\"\n #include \"utf8.h\"\n #include \"worktree.h\"\n+#include \"wt-status.h\"\n #include \"quote.h\"\n \n #define BUILTIN_WORKTREE_ADD_USAGE \\\n@@ -1408,6 +1409,32 @@ static void check_clean_worktree(struct worktree *wt,\n \t\t\t  original_path, ret);\n }\n \n+/*\n+ * A rebase, \"git am\", bisect or cherry-pick/revert sequence keeps its\n+ * state in the worktree's own $GIT_DIR, and removing the worktree\n+ * throws that away even when \"git status\" says the files are clean.\n+ */\n+static void check_no_operation_in_progress(struct worktree *wt,\n+\t\t\t\t\t   const char *original_path)\n+{\n+\tstruct wt_status_state state = { 0 };\n+\n+\tif (wt_status_check_rebase(wt, &state)) {\n+\t\tif (state.am_in_progress)\n+\t\t\tdie(_(\"'%s' is in the middle of an am session, use --force to delete it\"),\n+\t\t\t    original_path);\n+\t\tdie(_(\"'%s' is in the middle of a rebase, use --force to delete it\"),\n+\t\t    original_path);\n+\t}\n+\tif (wt_status_check_bisect(wt, &state))\n+\t\tdie(_(\"'%s' is in the middle of a bisect, use --force to delete it\"),\n+\t\t    original_path);\n+\twt_status_state_free_buffers(&state);\n+\tif (is_directory(worktree_git_path(wt, \"sequencer\")))\n+\t\tdie(_(\"'%s' is in the middle of a cherry-pick or revert, use --force to delete it\"),\n+\t\t    original_path);\n+}\n+\n static int delete_git_work_tree(struct worktree *wt)\n {\n \tstruct strbuf sb = STRBUF_INIT;\n@@ -1461,8 +1488,10 @@ static int remove_worktree(int ac, const char **av, const char *prefix,\n \tstrbuf_release(&errmsg);\n \n \tif (file_exists(wt->path)) {\n-\t\tif (!force)\n+\t\tif (!force) {\n+\t\t\tcheck_no_operation_in_progress(wt, av[0]);\n \t\t\tcheck_clean_worktree(wt, av[0]);\n+\t\t}\n \n \t\tret |= delete_git_work_tree(wt);\n \t}\ndiff --git a/t/t2403-worktree-move.sh b/t/t2403-worktree-move.sh\nindex 69768c1207..4eb56a8911 100755\n--- a/t/t2403-worktree-move.sh\n+++ b/t/t2403-worktree-move.sh\n@@ -271,4 +271,63 @@ test_expect_success 'move worktree with relative path to absolute path' '\n \ttest_cmp expect .git/worktrees/absolute/gitdir\n '\n \n+test_expect_success 'remove worktree in the middle of a rebase' '\n+\ttest_when_finished \"rm -rf mid-rebase; git worktree prune\" &&\n+\tgit worktree add --detach mid-rebase init &&\n+\ttest_commit -C mid-rebase rebase-me &&\n+\ttest_must_fail git -C mid-rebase rebase -f -x false init &&\n+\ttest_must_fail git worktree remove mid-rebase 2>err &&\n+\ttest_grep \"in the middle of a rebase\" err &&\n+\ttest_path_is_dir mid-rebase &&\n+\tgit worktree remove --force mid-rebase &&\n+\ttest_path_is_missing mid-rebase\n+'\n+\n+test_expect_success 'remove worktree in the middle of an am session' '\n+\ttest_when_finished \"rm -rf mid-am patch; git worktree prune\" &&\n+\tgit worktree add --detach mid-am init &&\n+\ttest_commit -C mid-am am-me &&\n+\tgit -C mid-am format-patch -1 --stdout >patch &&\n+\tgit -C mid-am reset --hard init &&\n+\techo conflict >mid-am/am-me.t &&\n+\tgit -C mid-am add am-me.t &&\n+\tgit -C mid-am commit -m conflict &&\n+\ttest_must_fail git -C mid-am am ../patch &&\n+\tgit -C mid-am reset --hard &&\n+\ttest_must_fail git worktree remove mid-am 2>err &&\n+\ttest_grep \"in the middle of an am session\" err &&\n+\tgit worktree remove --force mid-am\n+'\n+\n+test_expect_success 'remove worktree in the middle of a bisect' '\n+\ttest_when_finished \"rm -rf mid-bisect; git worktree prune\" &&\n+\tgit worktree add --detach mid-bisect init &&\n+\ttest_commit -C mid-bisect bisect-one &&\n+\ttest_commit -C mid-bisect bisect-two &&\n+\tgit -C mid-bisect bisect start HEAD init &&\n+\ttest_must_fail git worktree remove mid-bisect 2>err &&\n+\ttest_grep \"in the middle of a bisect\" err &&\n+\tgit worktree remove --force mid-bisect\n+'\n+\n+test_expect_success 'remove worktree in the middle of a cherry-pick sequence' '\n+\ttest_when_finished \"rm -rf mid-pick; git worktree prune\" &&\n+\tgit worktree add --detach mid-pick init &&\n+\tgit -C mid-pick switch -c pick-src &&\n+\ttest_commit -C mid-pick pick-one &&\n+\ttest_commit -C mid-pick pick-two &&\n+\tgit -C mid-pick switch --detach init &&\n+\techo conflict >mid-pick/pick-one.t &&\n+\tgit -C mid-pick add pick-one.t &&\n+\tgit -C mid-pick commit -m conflict &&\n+\ttest_must_fail git -C mid-pick cherry-pick pick-src~1 pick-src &&\n+\tgit -C mid-pick checkout --theirs pick-one.t &&\n+\tgit -C mid-pick add pick-one.t &&\n+\tgit -C mid-pick commit --no-edit &&\n+\ttest_path_is_dir .git/worktrees/mid-pick/sequencer &&\n+\ttest_must_fail git worktree remove mid-pick 2>err &&\n+\ttest_grep \"in the middle of a cherry-pick or revert\" err &&\n+\tgit worktree remove --force mid-pick\n+'\n+\n test_done\n-- \n2.56.0\n\n\n"},{"id":"556920","messageId":"20261030142137.54750-3-me@jaspermayone.com","threadId":"66626","inReplyTo":"20261030142137.54750-1-me@jaspermayone.com","subject":"[PATCH 2/2] ref-filter: name the worktree of a branch being rebased or bisected","fromName":"Jasper Mayone","fromEmail":"me@jaspermayone.com","sentAt":"2026-10-30T14:21:37Z","receivedAt":"2026-10-30T14:21:37Z","isPatch":true,"sender":{"key":"me@jaspermayone.com","avatar":null},"body":"%(worktreepath) gives the worktree that has a branch checked out, and\n\"git branch\" uses it to mark such branches with \"+\". It only looks at\nthe HEAD of each worktree, so while a worktree is rebasing or\nbisecting a branch, its HEAD is detached and the branch shows no\nworktree at all.\n\nThe rest of Git already treats that branch as checked out: \"git\nworktree add\" and \"git branch -d\" refuse it, because finishing the\nrebase or bisect will check it out again. \"git worktree list\" shows\nsuch a worktree as \"(detached HEAD)\", so %(worktreepath) is the only\nplace a script could find out which worktree is working on the\nbranch.\n\nAlso map the branch that a worktree is rebasing or bisecting to that\nworktree, as prepare_checked_out_branches() in branch.c does.\nBranches that \"rebase --update-refs\" will update are not mapped: that\nworktree has not checked them out, it only moves them when the rebase\nis done.\n\nAs a result \"git branch\" marks such a branch with \"+\", also when it\nruns in the worktree that is rebasing or bisecting it, where it shows\nboth \"* (no branch, rebasing topic)\" and \"+ topic\". Document that.\n\nReported-by: Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com>\nHelped-by: Phillip Wood <phillip.wood123@gmail.com>\nSigned-off-by: Jasper Mayone <me@jaspermayone.com>\n---\n Documentation/git-branch.adoc       |  5 ++-\n Documentation/git-for-each-ref.adoc |  4 +-\n ref-filter.c                        | 60 +++++++++++++++++++++------\n t/t3203-branch-output.sh            | 35 ++++++++++++++++\n t/t6302-for-each-ref-filter.sh      | 63 +++++++++++++++++++++++++++++\n 5 files changed, 153 insertions(+), 14 deletions(-)\n\ndiff --git a/Documentation/git-branch.adoc b/Documentation/git-branch.adoc\nindex bfdf459329..d499d7e0d4 100644\n--- a/Documentation/git-branch.adoc\n+++ b/Documentation/git-branch.adoc\n@@ -33,7 +33,10 @@ DESCRIPTION\n If `--list` is given, or if there are no non-option arguments, existing\n branches are listed; the current branch will be highlighted in green and\n marked with an asterisk.  Any branches checked out in linked worktrees will\n-be highlighted in cyan and marked with a plus sign. Option `-r` causes the\n+be highlighted in cyan and marked with a plus sign. A branch that a\n+worktree is rebasing or bisecting counts as checked out there, so it is\n+marked in the same way, also in the worktree that is rebasing or\n+bisecting it. Option `-r` causes the\n remote-tracking branches to be listed,\n and option `-a` shows both local and remote branches.\n \ndiff --git a/Documentation/git-for-each-ref.adoc b/Documentation/git-for-each-ref.adoc\nindex a7b31e8aaf..90ff37259e 100644\n--- a/Documentation/git-for-each-ref.adoc\n+++ b/Documentation/git-for-each-ref.adoc\n@@ -182,7 +182,9 @@ but if used together the last option is selected.\n \n `worktreepath`::\n \tThe absolute path to the worktree in which the ref is checked\n-\tout, if it is checked out in any linked worktree. Empty string\n+\tout, if it is checked out in any linked worktree. A branch that\n+\ta worktree is rebasing or bisecting counts as checked out there,\n+\talthough that worktree's `HEAD` is detached. Empty string\n \totherwise.\n \n `ahead-behind:<commit-ish>`::\ndiff --git a/ref-filter.c b/ref-filter.c\nindex bdf54f6f59..1aebea243e 100644\n--- a/ref-filter.c\n+++ b/ref-filter.c\n@@ -97,7 +97,8 @@ static struct expand_data {\n \n struct ref_to_worktree_entry {\n \tstruct hashmap_entry ent;\n-\tstruct worktree *wt; /* key is wt->head_ref */\n+\tstruct worktree *wt;\n+\tchar refname[FLEX_ARRAY]; /* the key */\n };\n \n static int ref_to_worktree_map_cmpfnc(const void *lookupdata UNUSED,\n@@ -110,8 +111,8 @@ static int ref_to_worktree_map_cmpfnc(const void *lookupdata UNUSED,\n \te = container_of(eptr, const struct ref_to_worktree_entry, ent);\n \tk = container_of(kptr, const struct ref_to_worktree_entry, ent);\n \n-\treturn strcmp(e->wt->head_ref,\n-\t\tkeydata_aka_refname ? keydata_aka_refname : k->wt->head_ref);\n+\treturn strcmp(e->refname,\n+\t\tkeydata_aka_refname ? keydata_aka_refname : k->refname);\n }\n \n static struct ref_to_worktree_map {\n@@ -2380,21 +2381,56 @@ static int get_object(struct ref_array_item *ref, int deref,\n \treturn ret;\n }\n \n+static void add_worktree_entry(struct hashmap *map, struct worktree *wt,\n+\t\t\t       const char *refname)\n+{\n+\tstruct ref_to_worktree_entry *entry;\n+\n+\tFLEX_ALLOC_STR(entry, refname, refname);\n+\tentry->wt = wt;\n+\thashmap_entry_init(&entry->ent, strhash(refname));\n+\thashmap_add(map, &entry->ent);\n+}\n+\n+/*\n+ * Map each branch to the worktree that has it checked out. A worktree\n+ * that is rebasing or bisecting a branch has it checked out, too,\n+ * even though its HEAD may be detached, as for\n+ * prepare_checked_out_branches() in branch.c.\n+ */\n static void populate_worktree_map(struct hashmap *map, struct worktree **worktrees)\n {\n-\tint i;\n+\tstruct strbuf ref = STRBUF_INIT;\n \n-\tfor (i = 0; worktrees[i]; i++) {\n-\t\tif (worktrees[i]->head_ref) {\n-\t\t\tstruct ref_to_worktree_entry *entry;\n-\t\t\tentry = xmalloc(sizeof(*entry));\n-\t\t\tentry->wt = worktrees[i];\n-\t\t\thashmap_entry_init(&entry->ent,\n-\t\t\t\t\tstrhash(worktrees[i]->head_ref));\n+\tfor (size_t i = 0; worktrees[i]; i++) {\n+\t\tstruct worktree *wt = worktrees[i];\n+\t\tstruct wt_status_state state = { 0 };\n \n-\t\t\thashmap_add(map, &entry->ent);\n+\t\tif (wt->is_bare)\n+\t\t\tcontinue;\n+\n+\t\tif (wt->head_ref)\n+\t\t\tadd_worktree_entry(map, wt, wt->head_ref);\n+\n+\t\tif (wt_status_check_rebase(wt, &state) &&\n+\t\t    (state.rebase_in_progress ||\n+\t\t     state.rebase_interactive_in_progress) &&\n+\t\t    state.branch) {\n+\t\t\tstrbuf_reset(&ref);\n+\t\t\tstrbuf_addf(&ref, \"refs/heads/%s\", state.branch);\n+\t\t\tadd_worktree_entry(map, wt, ref.buf);\n \t\t}\n+\t\twt_status_state_free_buffers(&state);\n+\n+\t\tif (wt_status_check_bisect(wt, &state) &&\n+\t\t    state.bisecting_from) {\n+\t\t\tstrbuf_reset(&ref);\n+\t\t\tstrbuf_addf(&ref, \"refs/heads/%s\", state.bisecting_from);\n+\t\t\tadd_worktree_entry(map, wt, ref.buf);\n+\t\t}\n+\t\twt_status_state_free_buffers(&state);\n \t}\n+\tstrbuf_release(&ref);\n }\n \n static void lazy_init_worktree_map(void)\ndiff --git a/t/t3203-branch-output.sh b/t/t3203-branch-output.sh\nindex 7c44186341..e04599392d 100755\n--- a/t/t3203-branch-output.sh\n+++ b/t/t3203-branch-output.sh\n@@ -442,6 +442,41 @@ test_expect_success 'worktree colors correct' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'branch being rebased in a worktree is marked' '\n+\ttest_when_finished \"git -C rebase_dir rebase --abort; rm -rf rebase_dir; git worktree prune; git branch -D rebasing\" &&\n+\tgit worktree add -b rebasing rebase_dir branch-two &&\n+\tgit -C rebase_dir commit --allow-empty -m rebase-me &&\n+\ttest_must_fail git -C rebase_dir rebase -f -x false HEAD~1 &&\n+\ttest_must_fail git -C rebase_dir symbolic-ref -q HEAD &&\n+\tgit branch --list rebasing >actual &&\n+\techo \"+ rebasing\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'branch being rebased is marked in its own worktree' '\n+\ttest_when_finished \"git -C rebase_dir rebase --abort; rm -rf rebase_dir; git worktree prune; git branch -D rebasing\" &&\n+\tgit worktree add -b rebasing rebase_dir branch-two &&\n+\tgit -C rebase_dir commit --allow-empty -m rebase-me &&\n+\ttest_must_fail git -C rebase_dir rebase -f -x false HEAD~1 &&\n+\tgit -C rebase_dir branch --list rebasing >actual &&\n+\techo \"+ rebasing\" >expect &&\n+\ttest_cmp expect actual &&\n+\tgit -C rebase_dir branch >actual &&\n+\ttest_grep \"^\\* (no branch, rebasing rebasing)\" actual\n+'\n+\n+test_expect_success 'branch being bisected in a worktree is marked' '\n+\ttest_when_finished \"git -C bisect_dir bisect reset; rm -rf bisect_dir; git worktree prune; git branch -D bisecting\" &&\n+\tgit worktree add -b bisecting bisect_dir branch-two &&\n+\tgit -C bisect_dir commit --allow-empty -m bisect-one &&\n+\tgit -C bisect_dir commit --allow-empty -m bisect-two &&\n+\tgit -C bisect_dir bisect start HEAD HEAD~2 &&\n+\ttest_must_fail git -C bisect_dir symbolic-ref -q HEAD &&\n+\tgit branch --list bisecting >actual &&\n+\techo \"+ bisecting\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success \"set up color tests\" '\n \techo \"<RED>main<RESET>\" >expect.color &&\n \techo \"main\" >expect.bare &&\ndiff --git a/t/t6302-for-each-ref-filter.sh b/t/t6302-for-each-ref-filter.sh\nindex 7f060d97bf..d656901f21 100755\n--- a/t/t6302-for-each-ref-filter.sh\n+++ b/t/t6302-for-each-ref-filter.sh\n@@ -3,6 +3,7 @@\n test_description='test for-each-refs usage of ref-filter APIs'\n \n . ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/lib-rebase.sh\n . \"$TEST_DIRECTORY\"/lib-gpg.sh\n \n test_expect_success 'setup some history and refs' '\n@@ -541,6 +542,68 @@ test_expect_success 'validate worktree atom' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'worktree atom names a worktree rebasing the branch' '\n+\ttest_when_finished \"git -C rebasing rebase --abort; rm -rf rebasing; git worktree prune; git branch -D rebasing\" &&\n+\tgit worktree add -b rebasing rebasing main &&\n+\tgit -C rebasing commit --allow-empty -m rebase-me &&\n+\ttest_must_fail git -C rebasing rebase -f -x false main &&\n+\ttest_must_fail git -C rebasing symbolic-ref -q HEAD &&\n+\techo \"$(pwd)/rebasing\" >expect &&\n+\tgit for-each-ref --format=\"%(worktreepath)\" refs/heads/rebasing >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'worktree atom names a worktree bisecting the branch' '\n+\ttest_when_finished \"git -C bisecting bisect reset; rm -rf bisecting; git worktree prune; git branch -D bisecting\" &&\n+\tgit worktree add -b bisecting bisecting main &&\n+\tgit -C bisecting commit --allow-empty -m bisect-one &&\n+\tgit -C bisecting commit --allow-empty -m bisect-two &&\n+\tgit -C bisecting bisect start HEAD main &&\n+\ttest_must_fail git -C bisecting symbolic-ref -q HEAD &&\n+\techo \"$(pwd)/bisecting\" >expect &&\n+\tgit for-each-ref --format=\"%(worktreepath)\" refs/heads/bisecting >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'worktree atom with an interactive rebase stopped at edit' '\n+\ttest_when_finished \"git -C editing rebase --abort; rm -rf editing; git worktree prune; git branch -D editing\" &&\n+\tgit worktree add -b editing editing main &&\n+\tgit -C editing commit --allow-empty -m edit-me &&\n+\t(\n+\t\tset_fake_editor &&\n+\t\tFAKE_LINES=\"edit 1\" git -C editing rebase -i --keep-empty HEAD~1\n+\t) &&\n+\ttest_must_fail git -C editing symbolic-ref -q HEAD &&\n+\techo \"$(pwd)/editing\" >expect &&\n+\tgit for-each-ref --format=\"%(worktreepath)\" refs/heads/editing >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'worktree atom ignores branches rebase --update-refs will move' '\n+\ttest_when_finished \"git -C updating rebase --abort; rm -rf updating; git worktree prune; git branch -D updating mid\" &&\n+\tgit worktree add -b updating updating main &&\n+\tgit -C updating commit --allow-empty -m one &&\n+\tgit -C updating branch mid &&\n+\tgit -C updating commit --allow-empty -m two &&\n+\ttest_must_fail git -C updating rebase -f --update-refs -x false main &&\n+\tgit for-each-ref --format=\"%(refname:short) [%(worktreepath)]\" \\\n+\t\trefs/heads/updating refs/heads/mid >actual &&\n+\tcat >expect <<-EOF &&\n+\tmid []\n+\tupdating [$(pwd)/updating]\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'worktree atom ignores a rebase of a detached HEAD' '\n+\ttest_when_finished \"git -C detached-rebase rebase --abort; rm -rf detached-rebase; git worktree prune\" &&\n+\tgit worktree add --detach detached-rebase main &&\n+\tgit -C detached-rebase commit --allow-empty -m detached &&\n+\ttest_must_fail git -C detached-rebase rebase -f -x false main &&\n+\tgit for-each-ref --format=\"%(worktreepath)\" refs/heads/ >actual &&\n+\ttest_grep ! detached-rebase actual\n+'\n+\n test_expect_success 'start after with empty value' '\n \tcat >expect <<-\\EOF &&\n \trefs/heads/main\n-- \n2.56.0\n\n\n"},{"id":"556981","messageId":"xmqqmrruu7bs.fsf@gitster.g","threadId":"66626","inReplyTo":"20261030142137.54750-1-me@jaspermayone.com","subject":"Re: [PATCH 0/2] worktree: treat a branch being rebased or bisected as checked out","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-10-30T22:55:35Z","receivedAt":"2026-10-30T22:55:35Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Jasper Mayone <me@jaspermayone.com> writes:\n\n> This replaces my \"git worktree list --branch\" patch [1].\n\nThen please label it as v3 as the old one is v2, or it breaks\nautomation.\n\n> In that\n> thread Kristoffer pointed out that \"git for-each-ref --merged <commit>\n> --format='%(worktreepath)' refs/heads/\" already finds the worktrees of\n> merged branches, so a new option is not needed. What is missing is\n> that %(worktreepath) ignores a branch while a worktree is rebasing or\n> bisecting it, although \"git worktree add\" and \"git branch -d\" already\n> treat that branch as checked out. Phillip agreed that this is a bug.\n>\n> Ben then asked what happens if such a worktree is passed to \"git\n> worktree remove\". Today it is removed, and the rebase or bisect state\n> is lost with it, if the files happen to be clean. Phillip called that\n> a bug too, and more likely to be hit once %(worktreepath) reports\n> these worktrees, so it is fixed first.\n\nAll of these address great points.  Thanks for working on it.\n\n\n\n"},{"id":"556982","messageId":"20261030230626.85196-1-me@jaspermayone.com","threadId":"66626","inReplyTo":"xmqqmrruu7bs.fsf@gitster.g","subject":"Re: [PATCH 0/2] worktree: treat a branch being rebased or bisected as checked out","fromName":"Jasper Mayone","fromEmail":"me@jaspermayone.com","sentAt":"2026-10-30T23:06:26Z","receivedAt":"2026-10-30T23:06:26Z","isPatch":true,"sender":{"key":"me@jaspermayone.com","avatar":null},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Jasper Mayone <me@jaspermayone.com> writes:\n>\n>> This replaces my \"git worktree list --branch\" patch [1].\n>\n> Then please label it as v3 as the old one is v2, or it breaks\n> automation.\n\nSorry about that. I will treat this round as v3, and the next round\nwill be v4.\n\n> All of these address great points.  Thanks for working on it.\n\nThanks.\n\nJasper\n\n"},{"id":"557175","messageId":"37d53d40-66b6-46bb-a38d-0d3534ca112a@gmail.com","threadId":"66626","inReplyTo":"20261030142137.54750-3-me@jaspermayone.com","subject":"Re: [PATCH 2/2] ref-filter: name the worktree of a branch being rebased or bisected","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-11-02T14:06:05Z","receivedAt":"2026-11-02T14:06:05Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Jasper\n\nOn 30/10/2026 14:21, Jasper Mayone wrote:\n> %(worktreepath) gives the worktree that has a branch checked out, and\n> \"git branch\" uses it to mark such branches with \"+\". It only looks at\n> the HEAD of each worktree, so while a worktree is rebasing or\n> bisecting a branch, its HEAD is detached and the branch shows no\n> worktree at all.\n> > The rest of Git already treats that branch as checked out: \"git\n> worktree add\" and \"git branch -d\" refuse it, because finishing the\n> rebase or bisect will check it out again.\n\nThose commands also reject branches that are being updated by a rebase via \"update-ref\" commands. If we're interested in checkouts we don't want to list those, but it means that \"%(worktree)\" will still give different results.\n\n> \"git worktree list\" shows\n> such a worktree as \"(detached HEAD)\", so %(worktreepath) is the only\n> place a script could find out which worktree is working on the\n> branch.\n\nI don't really follow this logic. At the moment neither \"git for-each-ref\", nor \"git worktree list\" show this  - there is no way for a script to find out which worktree is working on a given branch. We can choose to add it to one or both commands.\n\n> Also map the branch that a worktree is rebasing or bisecting to that\n> worktree, as prepare_checked_out_branches() in branch.c does.\n\nIf we want it to behave like prepare_checked_out_branches() we should use that function, rather than copying some of it to a separate file. If we want to only list checked out branches we could do as I previously suggested and add a function to branch.c that lists the worktrees where a branch is checked out.\n\n> Branches that \"rebase --update-refs\" will update are not mapped: that\n> worktree has not checked them out, it only moves them when the rebase\n> is done.\n\nI think that's sensible\n\n> As a result \"git branch\" marks such a branch with \"+\", also when it\n> runs in the worktree that is rebasing or bisecting it, where it shows\n> both \"* (no branch, rebasing topic)\" and \"+ topic\". Document that.\n\nThat sounds a bit confusing - \"* (no branch, rebasing topic)\" already tells us what we need to know, why do we need \"+ topic\" as well? The output of git branch is intended for human consumption, not scripts.\n\n> diff --git a/Documentation/git-branch.adoc b/Documentation/git-branch.adoc\n> index bfdf459329..d499d7e0d4 100644\n> --- a/Documentation/git-branch.adoc\n> +++ b/Documentation/git-branch.adoc\n> @@ -33,7 +33,10 @@ DESCRIPTION\n>   If `--list` is given, or if there are no non-option arguments, existing\n>   branches are listed; the current branch will be highlighted in green and\n>   marked with an asterisk.  Any branches checked out in linked worktrees will\n> -be highlighted in cyan and marked with a plus sign. Option `-r` causes the\n> +be highlighted in cyan and marked with a plus sign. A branch that a\n> +worktree is rebasing or bisecting counts as checked out there, so it is\n> +marked in the same way,\n\nI follow this up to here\n\n> also in the worktree that is rebasing or bisecting it.\n\nbut what is this trying to say?\n\n> diff --git a/Documentation/git-for-each-ref.adoc b/Documentation/git-for-each-ref.adoc\n> index a7b31e8aaf..90ff37259e 100644\n> --- a/Documentation/git-for-each-ref.adoc\n> +++ b/Documentation/git-for-each-ref.adoc\n> @@ -182,7 +182,9 @@ but if used together the last option is selected.\n>   >   `worktreepath`::\n>   \tThe absolute path to the worktree in which the ref is checked\n> -\tout, if it is checked out in any linked worktree. Empty string\n> +\tout, if it is checked out in any linked worktree. A branch that\n> +\ta worktree is rebasing or bisecting counts as checked out there,\n> +\talthough that worktree's `HEAD` is detached. Empty string\n>   \totherwise.\n\nIt is not a new problem, but I think it would be helpful to add something like\n\n    As the path may contain a newline, it is recommended to either use\n    one of the quoting options, or follow `%(worktreepath)` with `%00`\n    and parse the output by looking for the `NUL` after the worktree\n    path and then the LF that terminates each output record.\n\n    Note that if a branch checked out in more than one worktree,\n    `%(worktreepath)` will only show one of those worktrees.\n\n> diff --git a/ref-filter.c b/ref-filter.c\n> index bdf54f6f59..1aebea243e 100644\n> --- a/ref-filter.c\n> +++ b/ref-filter.c\n> [...]\n> +/*\n> + * Map each branch to the worktree that has it checked out. A worktree\n> + * that is rebasing or bisecting a branch has it checked out, too,\n> + * even though its HEAD may be detached, as for\n> + * prepare_checked_out_branches() in branch.c.\n> + */\n>   static void populate_worktree_map(struct hashmap *map, struct worktree **worktrees)\n>   {\n> -\tint i;\n> +\tstruct strbuf ref = STRBUF_INIT;\n>   > -\tfor (i = 0; worktrees[i]; i++) {\n> -\t\tif (worktrees[i]->head_ref) {\n> -\t\t\tstruct ref_to_worktree_entry *entry;\n> -\t\t\tentry = xmalloc(sizeof(*entry));\n> -\t\t\tentry->wt = worktrees[i];\n> -\t\t\thashmap_entry_init(&entry->ent,\n> -\t\t\t\t\tstrhash(worktrees[i]->head_ref));\n> +\tfor (size_t i = 0; worktrees[i]; i++) {\n> +\t\tstruct worktree *wt = worktrees[i];\n> +\t\tstruct wt_status_state state = { 0 };\n>   > -\t\t\thashmap_add(map, &entry->ent);\n> +\t\tif (wt->is_bare)\n> +\t\t\tcontinue;\n\nThis is a change in behavior. We currently print the git_dir of bare worktrees. I think you can argue that this change is a bug fix as the user asked for the worktree path, but it should be explained in the commit message.\n\nIt is a shame that we end up duplicating code that is already in branch.c. As I said last time, I think that it would be better to have a single implementation that maps branches to worktrees in branch.c and call that function rather than duplicating it here.\n\nThanks\n\nPhillip\n\n> +\n> +\t\tif (wt->head_ref)\n> +\t\t\tadd_worktree_entry(map, wt, wt->head_ref);\n> +\n> +\t\tif (wt_status_check_rebase(wt, &state) &&\n> +\t\t    (state.rebase_in_progress ||\n> +\t\t     state.rebase_interactive_in_progress) &&\n> +\t\t    state.branch) {\n> +\t\t\tstrbuf_reset(&ref);\n> +\t\t\tstrbuf_addf(&ref, \"refs/heads/%s\", state.branch);\n> +\t\t\tadd_worktree_entry(map, wt, ref.buf);\n>   \t\t}\n> +\t\twt_status_state_free_buffers(&state);\n> +\n> +\t\tif (wt_status_check_bisect(wt, &state) &&\n> +\t\t    state.bisecting_from) {\n> +\t\t\tstrbuf_reset(&ref);\n> +\t\t\tstrbuf_addf(&ref, \"refs/heads/%s\", state.bisecting_from);\n> +\t\t\tadd_worktree_entry(map, wt, ref.buf);\n> +\t\t}\n> +\t\twt_status_state_free_buffers(&state);\n>   \t}\n> +\tstrbuf_release(&ref);\n>   }\n>   >   static void lazy_init_worktree_map(void)\n> diff --git a/t/t3203-branch-output.sh b/t/t3203-branch-output.sh\n> index 7c44186341..e04599392d 100755\n> --- a/t/t3203-branch-output.sh\n> +++ b/t/t3203-branch-output.sh\n> @@ -442,6 +442,41 @@ test_expect_success 'worktree colors correct' '\n>   \ttest_cmp expect actual\n>   '\n>   > +test_expect_success 'branch being rebased in a worktree is marked' '\n> +\ttest_when_finished \"git -C rebase_dir rebase --abort; rm -rf rebase_dir; git worktree prune; git branch -D rebasing\" &&\n> +\tgit worktree add -b rebasing rebase_dir branch-two &&\n> +\tgit -C rebase_dir commit --allow-empty -m rebase-me &&\n> +\ttest_must_fail git -C rebase_dir rebase -f -x false HEAD~1 &&\n> +\ttest_must_fail git -C rebase_dir symbolic-ref -q HEAD &&\n> +\tgit branch --list rebasing >actual &&\n> +\techo \"+ rebasing\" >expect &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'branch being rebased is marked in its own worktree' '\n> +\ttest_when_finished \"git -C rebase_dir rebase --abort; rm -rf rebase_dir; git worktree prune; git branch -D rebasing\" &&\n> +\tgit worktree add -b rebasing rebase_dir branch-two &&\n> +\tgit -C rebase_dir commit --allow-empty -m rebase-me &&\n> +\ttest_must_fail git -C rebase_dir rebase -f -x false HEAD~1 &&\n> +\tgit -C rebase_dir branch --list rebasing >actual &&\n> +\techo \"+ rebasing\" >expect &&\n> +\ttest_cmp expect actual &&\n> +\tgit -C rebase_dir branch >actual &&\n> +\ttest_grep \"^\\* (no branch, rebasing rebasing)\" actual\n> +'\n> +\n> +test_expect_success 'branch being bisected in a worktree is marked' '\n> +\ttest_when_finished \"git -C bisect_dir bisect reset; rm -rf bisect_dir; git worktree prune; git branch -D bisecting\" &&\n> +\tgit worktree add -b bisecting bisect_dir branch-two &&\n> +\tgit -C bisect_dir commit --allow-empty -m bisect-one &&\n> +\tgit -C bisect_dir commit --allow-empty -m bisect-two &&\n> +\tgit -C bisect_dir bisect start HEAD HEAD~2 &&\n> +\ttest_must_fail git -C bisect_dir symbolic-ref -q HEAD &&\n> +\tgit branch --list bisecting >actual &&\n> +\techo \"+ bisecting\" >expect &&\n> +\ttest_cmp expect actual\n> +'\n> +\n>   test_expect_success \"set up color tests\" '\n>   \techo \"<RED>main<RESET>\" >expect.color &&\n>   \techo \"main\" >expect.bare &&\n> diff --git a/t/t6302-for-each-ref-filter.sh b/t/t6302-for-each-ref-filter.sh\n> index 7f060d97bf..d656901f21 100755\n> --- a/t/t6302-for-each-ref-filter.sh\n> +++ b/t/t6302-for-each-ref-filter.sh\n> @@ -3,6 +3,7 @@\n>   test_description='test for-each-refs usage of ref-filter APIs'\n>   >   . ./test-lib.sh\n> +. \"$TEST_DIRECTORY\"/lib-rebase.sh\n>   . \"$TEST_DIRECTORY\"/lib-gpg.sh\n>   >   test_expect_success 'setup some history and refs' '\n> @@ -541,6 +542,68 @@ test_expect_success 'validate worktree atom' '\n>   \ttest_cmp expect actual\n>   '\n>   > +test_expect_success 'worktree atom names a worktree rebasing the branch' '\n> +\ttest_when_finished \"git -C rebasing rebase --abort; rm -rf rebasing; git worktree prune; git branch -D rebasing\" &&\n> +\tgit worktree add -b rebasing rebasing main &&\n> +\tgit -C rebasing commit --allow-empty -m rebase-me &&\n> +\ttest_must_fail git -C rebasing rebase -f -x false main &&\n> +\ttest_must_fail git -C rebasing symbolic-ref -q HEAD &&\n> +\techo \"$(pwd)/rebasing\" >expect &&\n> +\tgit for-each-ref --format=\"%(worktreepath)\" refs/heads/rebasing >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'worktree atom names a worktree bisecting the branch' '\n> +\ttest_when_finished \"git -C bisecting bisect reset; rm -rf bisecting; git worktree prune; git branch -D bisecting\" &&\n> +\tgit worktree add -b bisecting bisecting main &&\n> +\tgit -C bisecting commit --allow-empty -m bisect-one &&\n> +\tgit -C bisecting commit --allow-empty -m bisect-two &&\n> +\tgit -C bisecting bisect start HEAD main &&\n> +\ttest_must_fail git -C bisecting symbolic-ref -q HEAD &&\n> +\techo \"$(pwd)/bisecting\" >expect &&\n> +\tgit for-each-ref --format=\"%(worktreepath)\" refs/heads/bisecting >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'worktree atom with an interactive rebase stopped at edit' '\n> +\ttest_when_finished \"git -C editing rebase --abort; rm -rf editing; git worktree prune; git branch -D editing\" &&\n> +\tgit worktree add -b editing editing main &&\n> +\tgit -C editing commit --allow-empty -m edit-me &&\n> +\t(\n> +\t\tset_fake_editor &&\n> +\t\tFAKE_LINES=\"edit 1\" git -C editing rebase -i --keep-empty HEAD~1\n> +\t) &&\n> +\ttest_must_fail git -C editing symbolic-ref -q HEAD &&\n> +\techo \"$(pwd)/editing\" >expect &&\n> +\tgit for-each-ref --format=\"%(worktreepath)\" refs/heads/editing >actual &&\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'worktree atom ignores branches rebase --update-refs will move' '\n> +\ttest_when_finished \"git -C updating rebase --abort; rm -rf updating; git worktree prune; git branch -D updating mid\" &&\n> +\tgit worktree add -b updating updating main &&\n> +\tgit -C updating commit --allow-empty -m one &&\n> +\tgit -C updating branch mid &&\n> +\tgit -C updating commit --allow-empty -m two &&\n> +\ttest_must_fail git -C updating rebase -f --update-refs -x false main &&\n> +\tgit for-each-ref --format=\"%(refname:short) [%(worktreepath)]\" \\\n> +\t\trefs/heads/updating refs/heads/mid >actual &&\n> +\tcat >expect <<-EOF &&\n> +\tmid []\n> +\tupdating [$(pwd)/updating]\n> +\tEOF\n> +\ttest_cmp expect actual\n> +'\n> +\n> +test_expect_success 'worktree atom ignores a rebase of a detached HEAD' '\n> +\ttest_when_finished \"git -C detached-rebase rebase --abort; rm -rf detached-rebase; git worktree prune\" &&\n> +\tgit worktree add --detach detached-rebase main &&\n> +\tgit -C detached-rebase commit --allow-empty -m detached &&\n> +\ttest_must_fail git -C detached-rebase rebase -f -x false main &&\n> +\tgit for-each-ref --format=\"%(worktreepath)\" refs/heads/ >actual &&\n> +\ttest_grep ! detached-rebase actual\n> +'\n> +\n>   test_expect_success 'start after with empty value' '\n>   \tcat >expect <<-\\EOF &&\n>   \trefs/heads/main\n\n\n"},{"id":"557177","messageId":"bf554c5f-066c-4e58-a583-0f95aedecf95@gmail.com","threadId":"66626","inReplyTo":"20261030142137.54750-2-me@jaspermayone.com","subject":"Re: [PATCH 1/2] worktree remove: refuse a worktree in the middle of a rebase or bisect","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2026-11-02T14:07:33Z","receivedAt":"2026-11-02T14:07:33Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Jasper\n\nOn 30/10/2026 14:21, Jasper Mayone wrote:\n> \"git worktree remove\" refuses a worktree with modified or untracked\n\n\"refuses to move\" would be clearer\n\n> files unless --force is given, but it removes a worktree that is in\n> the middle of a rebase, a \"git am\" session, a bisect or a cherry-pick\n> or revert sequence when its files happen to be clean. The state of\n> that operation lives in the worktree's own $GIT_DIR, so it is lost\n> with the worktree. A rebase stopped at an \"edit\" or \"break\" line, an\n> \"exec\" that failed, or a cherry-pick sequence after a conflict was\n> resolved and committed is clean in this sense and still has work in\n> progress.\n\nCherry picking or reverting a single commit that contains no new changes also looks clean, but there is still a cherry-pick operation in progress as CHERRY_PICK_HEAD exists. One could argue that, because the user doesn't really lose any work, we don't lose much by deleting such a worktree but we're still deleting a worktree that contains an in-progress operation. Fixing that case simply requires us to see if CHERRY_PICK_HEAD, or REVERT_HEAD exist.\n\n> Treat such a worktree as unclean: refuse to remove it unless --force\n> is given, and name the operation in the message.\n> > A worktree whose directory is already gone is still removed without\n> --force, as before: the check is part of the check for a clean\n> worktree, which is only done when the directory exists.\n\nSensible\n\n> Reported-by: D. Ben Knoble <ben.knoble@gmail.com>\n> Helped-by: Phillip Wood <phillip.wood123@gmail.com>\n\nI'm not sure what I've done anything to help with this patch\n\n> diff --git a/Documentation/git-worktree.adoc b/Documentation/git-worktree.adoc\n> index e6e77252ab..da4c38f909 100644\n> --- a/Documentation/git-worktree.adoc\n> +++ b/Documentation/git-worktree.adoc\n> @@ -142,9 +142,10 @@ the new working tree location.\n>   `remove`::\n>   >   Remove a worktree. Only clean worktrees (no untracked files and no\n> -modification in tracked files) can be removed. Unclean worktrees or ones\n> -with submodules can be removed with `--force`. The main worktree cannot be\n> -removed.\n> +modification in tracked files) can be removed. A worktree in the middle\n> +of a rebase, a `git am` session, a bisect or a cherry-pick or revert\n\n\"bisect, cherry-pick or revert\"\n\n> +sequence is not clean either, \"either\" doesn't make sense, because you've not described the unclean case yet. This line needs folding as well.\n\n> because removing it loses that state. Unclean worktrees or ones with submodules\n> +can be removed with `--force`. The main worktree cannot be removed.\n\nA list of the conditions that require --force might make this section clearer.\n>   `repair [<path>...]`::\n>   > @@ -189,7 +190,9 @@ twice. If the destination is already assigned to some other worktree but is\n>   missing (for instance, if _<new-path>_ was deleted manually), then `--force`\n>   allows the move to proceed; use `--force` twice if the destination is locked.\n>   +\n> -`remove` refuses to remove an unclean worktree unless `--force` is used.\n> +`remove` refuses to remove an unclean worktree, or one in the middle of a\n> +rebase, `git am` session, bisect, or cherry-pick or revert sequence,\n\ntoo many \"or\"s as above\n\n> +unless `--force` is used.\n>   To remove a locked worktree, specify `--force` twice.\n>   >   `-b <new-branch>`::\n> diff --git a/builtin/worktree.c b/builtin/worktree.c\n> index 77ecd0f71f..ed08de1f43 100644\n> --- a/builtin/worktree.c\n> +++ b/builtin/worktree.c\n> @@ -26,6 +26,7 @@\n>   #include \"submodule.h\"\n>   #include \"utf8.h\"\n>   #include \"worktree.h\"\n> +#include \"wt-status.h\"\n>   #include \"quote.h\"\n>   >   #define BUILTIN_WORKTREE_ADD_USAGE \\\n> @@ -1408,6 +1409,32 @@ static void check_clean_worktree(struct worktree *wt,\n>   \t\t\t  original_path, ret);\n>   }\n>   > +/*\n> + * A rebase, \"git am\", bisect or cherry-pick/revert sequence keeps its\n> + * state in the worktree's own $GIT_DIR, and removing the worktree\n> + * throws that away even when \"git status\" says the files are clean.\n> + */\n> +static void check_no_operation_in_progress(struct worktree *wt,\n> +\t\t\t\t\t   const char *original_path)\n> +{\n> +\tstruct wt_status_state state = { 0 };\n> +\n> +\tif (wt_status_check_rebase(wt, &state)) {\n> +\t\tif (state.am_in_progress)\n> +\t\t\tdie(_(\"'%s' is in the middle of an am session, use --force to delete it\"),\n> +\t\t\t    original_path);\n> +\t\tdie(_(\"'%s' is in the middle of a rebase, use --force to delete it\"),\n> +\t\t    original_path);\n> +\t}\n> +\tif (wt_status_check_bisect(wt, &state))\n> +\t\tdie(_(\"'%s' is in the middle of a bisect, use --force to delete it\"),\n> +\t\t    original_path);\n> +\twt_status_state_free_buffers(&state);\n> +\tif (is_directory(worktree_git_path(wt, \"sequencer\")))\n> +\t\tdie(_(\"'%s' is in the middle of a cherry-pick or revert, use --force to delete it\"),\n> +\t\t    original_path);\n> +}\n\nThis looks correct, though I wish we had a standard function that returned an enum describing which operation was in progress, rather than having to open code this sort of thing.\n\n>   static int delete_git_work_tree(struct worktree *wt)\n>   {\n>   \tstruct strbuf sb = STRBUF_INIT;\n> @@ -1461,8 +1488,10 @@ static int remove_worktree(int ac, const char **av, const char *prefix,\n>   \tstrbuf_release(&errmsg);\n>   >   \tif (file_exists(wt->path)) {\n> -\t\tif (!force)\n> +\t\tif (!force) {\n> +\t\t\tcheck_no_operation_in_progress(wt, av[0]);\n\nThis triggers the new check when the worktree directory exists - good.\n\n>   \t\t\tcheck_clean_worktree(wt, av[0]);\n> +\t\t}\n>   >   \t\tret |= delete_git_work_tree(wt);\n>   \t}\n> diff --git a/t/t2403-worktree-move.sh b/t/t2403-worktree-move.sh\n> index 69768c1207..4eb56a8911 100755\n> --- a/t/t2403-worktree-move.sh\n> +++ b/t/t2403-worktree-move.sh\n> @@ -271,4 +271,63 @@ test_expect_success 'move worktree with relative path to absolute path' '\n>   \ttest_cmp expect .git/worktrees/absolute/gitdir\n>   '\n>   > +test_expect_success 'remove worktree in the middle of a rebase' '\n> +\ttest_when_finished \"rm -rf mid-rebase; git worktree prune\" &&\n> +\tgit worktree add --detach mid-rebase init &&\n> +\ttest_commit -C mid-rebase rebase-me &&\n> +\ttest_must_fail git -C mid-rebase rebase -f -x false init &&\n> +\ttest_must_fail git worktree remove mid-rebase 2>err &&\n> +\ttest_grep \"in the middle of a rebase\" err &&\n> +\ttest_path_is_dir mid-rebase &&\n> +\tgit worktree remove --force mid-rebase &&\n> +\ttest_path_is_missing mid-rebase\n> +'\n> +\n> +test_expect_success 'remove worktree in the middle of an am session' '\n> +\ttest_when_finished \"rm -rf mid-am patch; git worktree prune\" &&\n> +\tgit worktree add --detach mid-am init &&\n> +\ttest_commit -C mid-am am-me &&\n> +\tgit -C mid-am format-patch -1 --stdout >patch &&\n> +\tgit -C mid-am reset --hard init &&\n> +\techo conflict >mid-am/am-me.t &&\n> +\tgit -C mid-am add am-me.t &&\n> +\tgit -C mid-am commit -m conflict &&\n> +\ttest_must_fail git -C mid-am am ../patch &&\n> +\tgit -C mid-am reset --hard &&\n\nIf we're running a lot of consecutive commands in a worktree, it is clearer to use a subshell\n\n\t(\n\t\tcd mid-am &&\n\t\t...\n\t) &&\n\nI wonder how hard it would be to extend the \"setup\" test to create a branch that has conflicts when it is applied with am, cherry-pick, or rebase, so that we don't have to set up the conflicts in each test.\n\nThanks\n\nPhillip\n\n> +\ttest_must_fail git worktree remove mid-am 2>err &&\n> +\ttest_grep \"in the middle of an am session\" err &&\n> +\tgit worktree remove --force mid-am\n> +'\n> +\n> +test_expect_success 'remove worktree in the middle of a bisect' '\n> +\ttest_when_finished \"rm -rf mid-bisect; git worktree prune\" &&\n> +\tgit worktree add --detach mid-bisect init &&\n> +\ttest_commit -C mid-bisect bisect-one &&\n> +\ttest_commit -C mid-bisect bisect-two &&\n> +\tgit -C mid-bisect bisect start HEAD init &&\n> +\ttest_must_fail git worktree remove mid-bisect 2>err &&\n> +\ttest_grep \"in the middle of a bisect\" err &&\n> +\tgit worktree remove --force mid-bisect\n> +'\n> +\n> +test_expect_success 'remove worktree in the middle of a cherry-pick sequence' '\n> +\ttest_when_finished \"rm -rf mid-pick; git worktree prune\" &&\n> +\tgit worktree add --detach mid-pick init &&\n> +\tgit -C mid-pick switch -c pick-src &&\n> +\ttest_commit -C mid-pick pick-one &&\n> +\ttest_commit -C mid-pick pick-two &&\n> +\tgit -C mid-pick switch --detach init &&\n> +\techo conflict >mid-pick/pick-one.t &&\n> +\tgit -C mid-pick add pick-one.t &&\n> +\tgit -C mid-pick commit -m conflict &&\n> +\ttest_must_fail git -C mid-pick cherry-pick pick-src~1 pick-src &&\n> +\tgit -C mid-pick checkout --theirs pick-one.t &&\n> +\tgit -C mid-pick add pick-one.t &&\n> +\tgit -C mid-pick commit --no-edit &&\n> +\ttest_path_is_dir .git/worktrees/mid-pick/sequencer &&\n> +\ttest_must_fail git worktree remove mid-pick 2>err &&\n> +\ttest_grep \"in the middle of a cherry-pick or revert\" err &&\n> +\tgit worktree remove --force mid-pick\n> +'\n> +\n>   test_done\n\n\n"},{"id":"557211","messageId":"20261102175708.75996-1-me@jaspermayone.com","threadId":"66626","inReplyTo":"bf554c5f-066c-4e58-a583-0f95aedecf95@gmail.com","subject":"Re: [PATCH 1/2] worktree remove: refuse a worktree in the middle of a rebase or bisect","fromName":"Jasper Mayone","fromEmail":"me@jaspermayone.com","sentAt":"2026-11-02T17:57:07Z","receivedAt":"2026-11-02T17:57:07Z","isPatch":true,"sender":{"key":"me@jaspermayone.com","avatar":null},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> Cherry picking or reverting a single commit that contains no new\n> changes also looks clean, but there is still a cherry-pick operation\n> in progress as CHERRY_PICK_HEAD exists.\n[...]\n> Fixing that case simply requires us to see if CHERRY_PICK_HEAD, or\n> REVERT_HEAD exist.\n\nGood catch. v4 will check for CHERRY_PICK_HEAD and REVERT_HEAD in the\nworktree's $GIT_DIR as well as the sequencer directory, with a test for\na single empty cherry-pick.\n\n>> Helped-by: Phillip Wood <phillip.wood123@gmail.com>\n>\n> I'm not sure what I've done anything to help with this patch\n\nYou are right, your help was on the other patch. I will drop it here.\n\n> A list of the conditions that require --force might make this section\n> clearer.\n\nAgreed. I will turn the `remove` description into a list of what makes\na worktree unclean (modified or untracked files, submodules, and an\noperation in progress), and fix the \"or\" chains and \"either\" you\npointed out.\n\n> If we're running a lot of consecutive commands in a worktree, it is\n> clearer to use a subshell\n\nWill do.\n\n> I wonder how hard it would be to extend the \"setup\" test to create a\n> branch that has conflicts when it is applied with am, cherry-pick, or\n> rebase, so that we don't have to set up the conflicts in each test.\n\nI will try that for v4. A branch that changes the same line as a\ncommit on top of \"init\" should give a conflict for all three.\n\nThanks for the review.\n\nJasper\n\n"},{"id":"557212","messageId":"20261102175722.76770-1-me@jaspermayone.com","threadId":"66626","inReplyTo":"37d53d40-66b6-46bb-a38d-0d3534ca112a@gmail.com","subject":"Re: [PATCH 2/2] ref-filter: name the worktree of a branch being rebased or bisected","fromName":"Jasper Mayone","fromEmail":"me@jaspermayone.com","sentAt":"2026-11-02T17:57:22Z","receivedAt":"2026-11-02T17:57:22Z","isPatch":true,"sender":{"key":"me@jaspermayone.com","avatar":null},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> Those commands also reject branches that are being updated by a rebase\n> via \"update-ref\" commands. If we're interested in checkouts we don't\n> want to list those, but it means that \"%(worktree)\" will still give\n> different results.\n\nYes, that difference is on purpose. I will say so in the commit message\nand the documentation.\n\n> I don't really follow this logic. At the moment neither \"git\n> for-each-ref\", nor \"git worktree list\" show this - there is no way for\n> a script to find out which worktree is working on a given branch. We\n> can choose to add it to one or both commands.\n\nYou are right, that paragraph is wrong. No command shows it today, and\nthis patch adds it to %(worktreepath) only. I will rewrite it.\n\n> If we want it to behave like prepare_checked_out_branches() we should\n> use that function, rather than copying some of it to a separate file.\n\nAgreed. For v4 I have added a small function to branch.c,\n\n    const char *branch_checked_out_worktree(const char *refname);\n\nwhich reads the list that prepare_checked_out_branches() builds and\nskips the BRANCH_CHECKOUT_KIND_UPDATE_REF entries. ref-filter calls it\nand drops its own hashmap, so the patch now removes more code than it\nadds in ref-filter.c. t6302, t3203, t3200, t2400 and t2403 pass with it.\n\n> That sounds a bit confusing - \"* (no branch, rebasing topic)\" already\n> tells us what we need to know, why do we need \"+ topic\" as well?\n\nI agree. In v4 \"git branch\" will not mark the branch with \"+\" in the\nworktree that is rebasing or bisecting it, since the \"*\" line already\nnames it. The \"also in the worktree\" sentence in git-branch.adoc goes\naway with that.\n\n> It is not a new problem, but I think it would be helpful to add\n> something like\n>\n>     As the path may contain a newline, [...]\n>\n>     Note that if a branch checked out in more than one worktree,\n>     `%(worktreepath)` will only show one of those worktrees.\n\nThanks, I will add that as a separate documentation patch before this\none.\n\n>> +\t\tif (wt->is_bare)\n>> +\t\t\tcontinue;\n>\n> This is a change in behavior. We currently print the git_dir of bare\n> worktrees. I think you can argue that this change is a bug fix as the\n> user asked for the worktree path, but it should be explained in the\n> commit message.\n\nAgreed. With a bare repository and a linked worktree, today \"git\nbranch\" in the linked worktree shows \"+ main\" for the bare\nrepository's HEAD, and %(worktreepath) gives the path of the bare\nrepository. main is not checked out anywhere, and \"git worktree add\"\nalready lets you check it out. I will explain that in the commit\nmessage and add a test for it.\n\nThanks,\nJasper\n\n"},{"id":"557215","messageId":"20261102182930.3511-1-me@jaspermayone.com","threadId":"66626","inReplyTo":"20261030142137.54750-1-me@jaspermayone.com","subject":"[PATCH v4 0/4] worktree: treat a branch being rebased or bisected as checked out","fromName":"Jasper Mayone","fromEmail":"me@jaspermayone.com","sentAt":"2026-11-02T18:29:26Z","receivedAt":"2026-11-02T18:29:26Z","isPatch":true,"sender":{"key":"me@jaspermayone.com","avatar":null},"body":"While a worktree is rebasing or bisecting a branch, its HEAD is\ndetached. \"git worktree add\" and \"git branch -d\" already treat the\nbranch as checked out there, but \"git worktree remove\" deletes such a\nworktree when its files are clean, and %(worktreepath) and \"git\nbranch\" do not show which worktree has the branch.\n\nPatch 1 makes \"git worktree remove\" refuse a worktree in the middle of\nan operation unless --force is given. Patch 2 documents how to read\n%(worktreepath) safely. Patch 3 makes %(worktreepath) name the\nworktree that is rebasing or bisecting a branch, and patch 4 keeps\n\"git branch\" from marking that branch with \"+\" in that worktree.\n\nChanges since v3 [1]:\n\n * Patch 1: also refuse a worktree that has CHERRY_PICK_HEAD or\n   REVERT_HEAD, for a single cherry-pick or revert whose files are\n   clean, with tests. The git-worktree documentation now lists the\n   cases that need --force. The tests use a shared setup and\n   subshells. Dropped the Helped-by trailer, as Phillip asked.\n\n * Patch 2 is new. It adds the notes that Phillip suggested to the\n   documentation of %(worktreepath): the path can contain a newline,\n   and only one worktree is shown for a branch that is checked out in\n   more than one.\n\n * Patch 3: %(worktreepath) now uses a new branch.c function,\n   branch_checked_out_worktree(), that reads the list that\n   prepare_checked_out_branches() builds, in place of a copy of that\n   code in ref-filter.c. ref-filter.c loses its own worktree map. The\n   commit message explains why branches that \"rebase --update-refs\"\n   will update are not included, and the change for the HEAD of a\n   bare repository, which now has a test.\n\n * Patch 4 is new. \"git branch\" no longer marks a branch with \"+\" in\n   the worktree that is rebasing or bisecting it, because the\n   \"(no branch, ...)\" line already names it.\n\n[1] https://lore.kernel.org/git/20261030142137.54750-1-me@jaspermayone.com/\n\nJasper Mayone (4):\n  worktree remove: refuse a worktree in the middle of an operation\n  doc: for-each-ref: say how to read %(worktreepath) safely\n  ref-filter: name the worktree of a branch being rebased or bisected\n  branch: do not mark the branch the current worktree is rebasing\n\n Documentation/git-branch.adoc       |   6 +-\n Documentation/git-for-each-ref.adoc |  12 +++-\n Documentation/git-worktree.adoc     |  17 +++--\n branch.c                            |  13 ++++\n branch.h                            |   9 +++\n builtin/branch.c                    | 103 +++++++++++++++++++++-------\n builtin/worktree.c                  |  37 +++++++++-\n ref-filter.c                        |  74 +-------------------\n t/t2403-worktree-move.sh            |  94 +++++++++++++++++++++++++\n t/t3203-branch-output.sh            |  49 +++++++++++++\n t/t6302-for-each-ref-filter.sh      |  76 ++++++++++++++++++++\n 11 files changed, 386 insertions(+), 104 deletions(-)\n\nRange-diff against v3:\n1:  a79b69a0e4 ! 1:  bca692b844 worktree remove: refuse a worktree in the middle of a rebase or bisect\n    @@ Metadata\n     Author: Jasper Mayone <me@jaspermayone.com>\n     \n      ## Commit message ##\n    -    worktree remove: refuse a worktree in the middle of a rebase or bisect\n    +    worktree remove: refuse a worktree in the middle of an operation\n     \n    -    \"git worktree remove\" refuses a worktree with modified or untracked\n    -    files unless --force is given, but it removes a worktree that is in\n    -    the middle of a rebase, a \"git am\" session, a bisect or a cherry-pick\n    -    or revert sequence when its files happen to be clean. The state of\n    -    that operation lives in the worktree's own $GIT_DIR, so it is lost\n    -    with the worktree. A rebase stopped at an \"edit\" or \"break\" line, an\n    -    \"exec\" that failed, or a cherry-pick sequence after a conflict was\n    -    resolved and committed is clean in this sense and still has work in\n    -    progress.\n    +    \"git worktree remove\" refuses to remove a worktree with modified or\n    +    untracked files unless --force is given, but it removes a worktree\n    +    that is in the middle of a rebase, a \"git am\" session, a bisect, a\n    +    cherry-pick or a revert when its files happen to be clean. The state\n    +    of that operation lives in the worktree's own $GIT_DIR, so it is lost\n    +    with the worktree. Examples of such a clean worktree are a rebase\n    +    stopped at an \"edit\" or \"break\" line or after a failed \"exec\", a\n    +    cherry-pick sequence after a conflict was resolved and committed, and\n    +    a single cherry-pick or revert whose conflict was resolved to the\n    +    current contents, which leaves CHERRY_PICK_HEAD or REVERT_HEAD behind.\n     \n         Treat such a worktree as unclean: refuse to remove it unless --force\n         is given, and name the operation in the message.\n    @@ Commit message\n         worktree, which is only done when the directory exists.\n     \n         Reported-by: D. Ben Knoble <ben.knoble@gmail.com>\n    -    Helped-by: Phillip Wood <phillip.wood123@gmail.com>\n         Signed-off-by: Jasper Mayone <me@jaspermayone.com>\n     \n      ## Documentation/git-worktree.adoc ##\n     @@ Documentation/git-worktree.adoc: the new working tree location.\n    + \n      `remove`::\n      \n    - Remove a worktree. Only clean worktrees (no untracked files and no\n    +-Remove a worktree. Only clean worktrees (no untracked files and no\n     -modification in tracked files) can be removed. Unclean worktrees or ones\n     -with submodules can be removed with `--force`. The main worktree cannot be\n     -removed.\n    -+modification in tracked files) can be removed. A worktree in the middle\n    -+of a rebase, a `git am` session, a bisect or a cherry-pick or revert\n    -+sequence is not clean either, because removing it loses that state. Unclean worktrees or ones with submodules\n    -+can be removed with `--force`. The main worktree cannot be removed.\n    ++Remove a worktree. The main worktree cannot be removed. Without\n    ++`--force`, a worktree is not removed when:\n    +++\n    ++--\n    ++* it has untracked files or modifications in tracked files,\n    ++* it has submodules, or\n    ++* it is in the middle of a rebase, a `git am` session, a bisect, a\n    ++  cherry-pick or a revert, because removing it loses the state of that\n    ++  operation.\n    ++--\n      \n      `repair [<path>...]`::\n      \n    @@ Documentation/git-worktree.adoc: twice. If the destination is already assigned t\n      allows the move to proceed; use `--force` twice if the destination is locked.\n      +\n     -`remove` refuses to remove an unclean worktree unless `--force` is used.\n    -+`remove` refuses to remove an unclean worktree, or one in the middle of a\n    -+rebase, `git am` session, bisect, or cherry-pick or revert sequence,\n    -+unless `--force` is used.\n    ++`remove` refuses to remove a worktree in the cases listed under\n    ++`remove` above unless `--force` is used.\n      To remove a locked worktree, specify `--force` twice.\n      \n      `-b <new-branch>`::\n    @@ builtin/worktree.c: static void check_clean_worktree(struct worktree *wt,\n      }\n      \n     +/*\n    -+ * A rebase, \"git am\", bisect or cherry-pick/revert sequence keeps its\n    -+ * state in the worktree's own $GIT_DIR, and removing the worktree\n    -+ * throws that away even when \"git status\" says the files are clean.\n    ++ * A rebase, \"git am\", bisect, cherry-pick or revert keeps its state in\n    ++ * the worktree's own $GIT_DIR, and removing the worktree throws that\n    ++ * away even when \"git status\" says the files are clean.\n     + */\n     +static void check_no_operation_in_progress(struct worktree *wt,\n     +\t\t\t\t\t   const char *original_path)\n    @@ builtin/worktree.c: static void check_clean_worktree(struct worktree *wt,\n     +\t\tdie(_(\"'%s' is in the middle of a bisect, use --force to delete it\"),\n     +\t\t    original_path);\n     +\twt_status_state_free_buffers(&state);\n    ++\tif (refs_ref_exists(get_worktree_ref_store(wt), \"CHERRY_PICK_HEAD\"))\n    ++\t\tdie(_(\"'%s' is in the middle of a cherry-pick, use --force to delete it\"),\n    ++\t\t    original_path);\n    ++\tif (refs_ref_exists(get_worktree_ref_store(wt), \"REVERT_HEAD\"))\n    ++\t\tdie(_(\"'%s' is in the middle of a revert, use --force to delete it\"),\n    ++\t\t    original_path);\n     +\tif (is_directory(worktree_git_path(wt, \"sequencer\")))\n     +\t\tdie(_(\"'%s' is in the middle of a cherry-pick or revert, use --force to delete it\"),\n     +\t\t    original_path);\n    @@ t/t2403-worktree-move.sh: test_expect_success 'move worktree with relative path\n      \ttest_cmp expect .git/worktrees/absolute/gitdir\n      '\n      \n    ++test_expect_success 'setup commits that conflict for am, cherry-pick and revert' '\n    ++\tgit worktree add --detach conflict-setup init &&\n    ++\t(\n    ++\t\tcd conflict-setup &&\n    ++\t\ttest_commit base conflict.t base &&\n    ++\t\tgit switch --detach init &&\n    ++\t\ttest_commit side conflict.t side &&\n    ++\t\ttest_commit side-two\n    ++\t) &&\n    ++\tgit worktree remove conflict-setup\n    ++'\n    ++\n     +test_expect_success 'remove worktree in the middle of a rebase' '\n     +\ttest_when_finished \"rm -rf mid-rebase; git worktree prune\" &&\n    -+\tgit worktree add --detach mid-rebase init &&\n    -+\ttest_commit -C mid-rebase rebase-me &&\n    ++\tgit worktree add --detach mid-rebase side &&\n     +\ttest_must_fail git -C mid-rebase rebase -f -x false init &&\n     +\ttest_must_fail git worktree remove mid-rebase 2>err &&\n     +\ttest_grep \"in the middle of a rebase\" err &&\n    @@ t/t2403-worktree-move.sh: test_expect_success 'move worktree with relative path\n     +\n     +test_expect_success 'remove worktree in the middle of an am session' '\n     +\ttest_when_finished \"rm -rf mid-am patch; git worktree prune\" &&\n    -+\tgit worktree add --detach mid-am init &&\n    -+\ttest_commit -C mid-am am-me &&\n    -+\tgit -C mid-am format-patch -1 --stdout >patch &&\n    -+\tgit -C mid-am reset --hard init &&\n    -+\techo conflict >mid-am/am-me.t &&\n    -+\tgit -C mid-am add am-me.t &&\n    -+\tgit -C mid-am commit -m conflict &&\n    -+\ttest_must_fail git -C mid-am am ../patch &&\n    -+\tgit -C mid-am reset --hard &&\n    ++\tgit format-patch -1 --stdout side >patch &&\n    ++\tgit worktree add --detach mid-am base &&\n    ++\t(\n    ++\t\tcd mid-am &&\n    ++\t\ttest_must_fail git am ../patch &&\n    ++\t\tgit reset --hard\n    ++\t) &&\n     +\ttest_must_fail git worktree remove mid-am 2>err &&\n     +\ttest_grep \"in the middle of an am session\" err &&\n     +\tgit worktree remove --force mid-am\n    @@ t/t2403-worktree-move.sh: test_expect_success 'move worktree with relative path\n     +\n     +test_expect_success 'remove worktree in the middle of a bisect' '\n     +\ttest_when_finished \"rm -rf mid-bisect; git worktree prune\" &&\n    -+\tgit worktree add --detach mid-bisect init &&\n    -+\ttest_commit -C mid-bisect bisect-one &&\n    -+\ttest_commit -C mid-bisect bisect-two &&\n    -+\tgit -C mid-bisect bisect start HEAD init &&\n    ++\tgit worktree add --detach mid-bisect side-two &&\n    ++\tgit -C mid-bisect bisect start side-two init &&\n     +\ttest_must_fail git worktree remove mid-bisect 2>err &&\n     +\ttest_grep \"in the middle of a bisect\" err &&\n     +\tgit worktree remove --force mid-bisect\n     +'\n     +\n    -+test_expect_success 'remove worktree in the middle of a cherry-pick sequence' '\n    ++test_expect_success 'remove worktree in the middle of a cherry-pick' '\n     +\ttest_when_finished \"rm -rf mid-pick; git worktree prune\" &&\n    -+\tgit worktree add --detach mid-pick init &&\n    -+\tgit -C mid-pick switch -c pick-src &&\n    -+\ttest_commit -C mid-pick pick-one &&\n    -+\ttest_commit -C mid-pick pick-two &&\n    -+\tgit -C mid-pick switch --detach init &&\n    -+\techo conflict >mid-pick/pick-one.t &&\n    -+\tgit -C mid-pick add pick-one.t &&\n    -+\tgit -C mid-pick commit -m conflict &&\n    -+\ttest_must_fail git -C mid-pick cherry-pick pick-src~1 pick-src &&\n    -+\tgit -C mid-pick checkout --theirs pick-one.t &&\n    -+\tgit -C mid-pick add pick-one.t &&\n    -+\tgit -C mid-pick commit --no-edit &&\n    -+\ttest_path_is_dir .git/worktrees/mid-pick/sequencer &&\n    ++\tgit worktree add --detach mid-pick base &&\n    ++\t(\n    ++\t\tcd mid-pick &&\n    ++\t\ttest_must_fail git cherry-pick side &&\n    ++\t\tgit checkout --ours conflict.t &&\n    ++\t\tgit add conflict.t &&\n    ++\t\tgit rev-parse --verify CHERRY_PICK_HEAD\n    ++\t) &&\n    ++\ttest_path_is_missing .git/worktrees/mid-pick/sequencer &&\n     +\ttest_must_fail git worktree remove mid-pick 2>err &&\n    -+\ttest_grep \"in the middle of a cherry-pick or revert\" err &&\n    ++\ttest_grep \"in the middle of a cherry-pick,\" err &&\n     +\tgit worktree remove --force mid-pick\n     +'\n    ++\n    ++test_expect_success 'remove worktree in the middle of a revert' '\n    ++\ttest_when_finished \"rm -rf mid-revert; git worktree prune\" &&\n    ++\tgit worktree add --detach mid-revert base &&\n    ++\t(\n    ++\t\tcd mid-revert &&\n    ++\t\ttest_must_fail git revert side &&\n    ++\t\tgit checkout --ours conflict.t &&\n    ++\t\tgit add conflict.t &&\n    ++\t\tgit rev-parse --verify REVERT_HEAD\n    ++\t) &&\n    ++\ttest_path_is_missing .git/worktrees/mid-revert/sequencer &&\n    ++\ttest_must_fail git worktree remove mid-revert 2>err &&\n    ++\ttest_grep \"in the middle of a revert\" err &&\n    ++\tgit worktree remove --force mid-revert\n    ++'\n    ++\n    ++test_expect_success 'remove worktree in the middle of a cherry-pick sequence' '\n    ++\ttest_when_finished \"rm -rf mid-pick-seq; git worktree prune\" &&\n    ++\tgit worktree add --detach mid-pick-seq base &&\n    ++\t(\n    ++\t\tcd mid-pick-seq &&\n    ++\t\ttest_must_fail git cherry-pick side side-two &&\n    ++\t\tgit checkout --theirs conflict.t &&\n    ++\t\tgit add conflict.t &&\n    ++\t\tgit commit --no-edit\n    ++\t) &&\n    ++\ttest_path_is_dir .git/worktrees/mid-pick-seq/sequencer &&\n    ++\ttest_must_fail git worktree remove mid-pick-seq 2>err &&\n    ++\ttest_grep \"in the middle of a cherry-pick or revert\" err &&\n    ++\tgit worktree remove --force mid-pick-seq\n    ++'\n     +\n      test_done\n-:  ---------- > 2:  39eb92d441 doc: for-each-ref: say how to read %(worktreepath) safely\n2:  0cd13347e1 ! 3:  990a19fd11 ref-filter: name the worktree of a branch being rebased or bisected\n    @@ Commit message\n     \n         The rest of Git already treats that branch as checked out: \"git\n         worktree add\" and \"git branch -d\" refuse it, because finishing the\n    -    rebase or bisect will check it out again. \"git worktree list\" shows\n    -    such a worktree as \"(detached HEAD)\", so %(worktreepath) is the only\n    -    place a script could find out which worktree is working on the\n    -    branch.\n    +    rebase or bisect will check it out again. But no command shows which\n    +    worktree is working on the branch: \"git worktree list\" shows such a\n    +    worktree as \"(detached HEAD)\", and %(worktreepath) is empty.\n     \n    -    Also map the branch that a worktree is rebasing or bisecting to that\n    -    worktree, as prepare_checked_out_branches() in branch.c does.\n    -    Branches that \"rebase --update-refs\" will update are not mapped: that\n    -    worktree has not checked them out, it only moves them when the rebase\n    -    is done.\n    +    branch.c already knows the answer, as prepare_checked_out_branches()\n    +    records the branch that each worktree has checked out, is rebasing or\n    +    is bisecting. Add branch_checked_out_worktree() to look a branch up in\n    +    that list, and use it for %(worktreepath) in place of the map that\n    +    ref-filter.c builds from the HEAD of each worktree.\n     \n    -    As a result \"git branch\" marks such a branch with \"+\", also when it\n    -    runs in the worktree that is rebasing or bisecting it, where it shows\n    -    both \"* (no branch, rebasing topic)\" and \"+ topic\". Document that.\n    +    The new function skips the branches that \"rebase --update-refs\" will\n    +    update. branch_checked_out() includes them, so that they cannot be\n    +    checked out or deleted during the rebase, but the worktree that runs\n    +    the rebase has not checked them out. %(worktreepath) therefore stays\n    +    empty for them, unlike branch_checked_out().\n    +\n    +    This also changes the result for a bare repository: its HEAD names a\n    +    branch, and %(worktreepath) used to give the path of the bare\n    +    repository for that branch. \"git branch\" in a linked worktree marked\n    +    the branch with \"+\", although no worktree has it checked out and \"git\n    +    worktree add\" lets you check it out. prepare_checked_out_branches()\n    +    skips bare repositories, so %(worktreepath) is now empty for that\n    +    branch and \"git branch\" no longer marks it.\n     \n         Reported-by: Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com>\n         Helped-by: Phillip Wood <phillip.wood123@gmail.com>\n    @@ Documentation/git-branch.adoc: DESCRIPTION\n      branches are listed; the current branch will be highlighted in green and\n      marked with an asterisk.  Any branches checked out in linked worktrees will\n     -be highlighted in cyan and marked with a plus sign. Option `-r` causes the\n    +-remote-tracking branches to be listed,\n     +be highlighted in cyan and marked with a plus sign. A branch that a\n    -+worktree is rebasing or bisecting counts as checked out there, so it is\n    -+marked in the same way, also in the worktree that is rebasing or\n    -+bisecting it. Option `-r` causes the\n    - remote-tracking branches to be listed,\n    ++worktree is rebasing or bisecting counts as checked out in that\n    ++worktree, so the other worktrees mark it in the same way. Option `-r`\n    ++causes the remote-tracking branches to be listed,\n      and option `-a` shows both local and remote branches.\n      \n    + If a `<pattern>`\n     \n      ## Documentation/git-for-each-ref.adoc ##\n     @@ Documentation/git-for-each-ref.adoc: but if used together the last option is selected.\n    @@ Documentation/git-for-each-ref.adoc: but if used together the last option is sel\n     -\tout, if it is checked out in any linked worktree. Empty string\n     +\tout, if it is checked out in any linked worktree. A branch that\n     +\ta worktree is rebasing or bisecting counts as checked out there,\n    -+\talthough that worktree's `HEAD` is detached. Empty string\n    - \totherwise.\n    - \n    - `ahead-behind:<commit-ish>`::\n    ++\talthough that worktree's `HEAD` is detached. A branch that\n    ++\t`git rebase --update-refs` will update does not. Empty string\n    + \totherwise. If the ref is checked out in more than one worktree,\n    + \tonly one of them is shown. The path can contain a newline, so\n    + \tuse one of the quoting options such as `--shell`, or follow\n     \n    - ## ref-filter.c ##\n    -@@ ref-filter.c: static struct expand_data {\n    - \n    - struct ref_to_worktree_entry {\n    - \tstruct hashmap_entry ent;\n    --\tstruct worktree *wt; /* key is wt->head_ref */\n    -+\tstruct worktree *wt;\n    -+\tchar refname[FLEX_ARRAY]; /* the key */\n    - };\n    - \n    - static int ref_to_worktree_map_cmpfnc(const void *lookupdata UNUSED,\n    -@@ ref-filter.c: static int ref_to_worktree_map_cmpfnc(const void *lookupdata UNUSED,\n    - \te = container_of(eptr, const struct ref_to_worktree_entry, ent);\n    - \tk = container_of(kptr, const struct ref_to_worktree_entry, ent);\n    - \n    --\treturn strcmp(e->wt->head_ref,\n    --\t\tkeydata_aka_refname ? keydata_aka_refname : k->wt->head_ref);\n    -+\treturn strcmp(e->refname,\n    -+\t\tkeydata_aka_refname ? keydata_aka_refname : k->refname);\n    + ## branch.c ##\n    +@@ branch.c: const char *branch_checked_out(const char *refname)\n    + \treturn strmap_get(&current_checked_out_branches, refname);\n      }\n      \n    - static struct ref_to_worktree_map {\n    ++const char *branch_checked_out_worktree(const char *refname)\n    ++{\n    ++\tconst char *path = NULL;\n    ++\n    ++\tprepare_checked_out_branches();\n    ++\tfor (size_t i = 0; i < checked_out_branches_nr; i++) {\n    ++\t\tif (checked_out_branches[i].kind != BRANCH_CHECKOUT_KIND_UPDATE_REF &&\n    ++\t\t    !strcmp(refname, checked_out_branches[i].refname))\n    ++\t\t\tpath = checked_out_branches[i].path;\n    ++\t}\n    ++\treturn path;\n    ++}\n    ++\n    + const char *branch_bisecting(const char *refname)\n    + {\n    + \tprepare_checked_out_branches();\n    +\n    + ## branch.h ##\n    +@@ branch.h: void create_branches_recursively(struct repository *r, const char *name,\n    +  */\n    + const char *branch_checked_out(const char *refname);\n    + \n    ++/*\n    ++ * If a worktree has the branch 'refname' checked out, or is rebasing or\n    ++ * bisecting it, return the path of that worktree, or of the last one if\n    ++ * there are more. Otherwise return NULL. Unlike branch_checked_out(),\n    ++ * this ignores the branches that \"git rebase --update-refs\" will\n    ++ * update, because no worktree has them checked out.\n    ++ */\n    ++const char *branch_checked_out_worktree(const char *refname);\n    ++\n    + /*\n    +  * If the branch at 'refname' is currently used for bisecting in a\n    +  * worktree, then return the path to that worktree.\n    +\n    + ## ref-filter.c ##\n    +@@\n    + #define DISABLE_SIGN_COMPARE_WARNINGS\n    + \n    + #include \"git-compat-util.h\"\n    ++#include \"branch.h\"\n    + #include \"environment.h\"\n    + #include \"gettext.h\"\n    + #include \"config.h\"\n    +@@ ref-filter.c: static struct expand_data {\n    + \tstruct object_info info;\n    + } oi, oi_deref;\n    + \n    +-struct ref_to_worktree_entry {\n    +-\tstruct hashmap_entry ent;\n    +-\tstruct worktree *wt; /* key is wt->head_ref */\n    +-};\n    +-\n    +-static int ref_to_worktree_map_cmpfnc(const void *lookupdata UNUSED,\n    +-\t\t\t\t      const struct hashmap_entry *eptr,\n    +-\t\t\t\t      const struct hashmap_entry *kptr,\n    +-\t\t\t\t      const void *keydata_aka_refname)\n    +-{\n    +-\tconst struct ref_to_worktree_entry *e, *k;\n    +-\n    +-\te = container_of(eptr, const struct ref_to_worktree_entry, ent);\n    +-\tk = container_of(kptr, const struct ref_to_worktree_entry, ent);\n    +-\n    +-\treturn strcmp(e->wt->head_ref,\n    +-\t\tkeydata_aka_refname ? keydata_aka_refname : k->wt->head_ref);\n    +-}\n    +-\n    +-static struct ref_to_worktree_map {\n    +-\tstruct hashmap map;\n    +-\tstruct worktree **worktrees;\n    +-} ref_to_worktree_map;\n    +-\n    + /*\n    +  * The enum atom_type is used as the index of valid_atom array.\n    +  * In the atom parsing stage, it will be passed to used_atom.atom_type\n     @@ ref-filter.c: static int get_object(struct ref_array_item *ref, int deref,\n      \treturn ret;\n      }\n      \n    -+static void add_worktree_entry(struct hashmap *map, struct worktree *wt,\n    -+\t\t\t       const char *refname)\n    -+{\n    -+\tstruct ref_to_worktree_entry *entry;\n    -+\n    -+\tFLEX_ALLOC_STR(entry, refname, refname);\n    -+\tentry->wt = wt;\n    -+\thashmap_entry_init(&entry->ent, strhash(refname));\n    -+\thashmap_add(map, &entry->ent);\n    -+}\n    -+\n    -+/*\n    -+ * Map each branch to the worktree that has it checked out. A worktree\n    -+ * that is rebasing or bisecting a branch has it checked out, too,\n    -+ * even though its HEAD may be detached, as for\n    -+ * prepare_checked_out_branches() in branch.c.\n    -+ */\n    - static void populate_worktree_map(struct hashmap *map, struct worktree **worktrees)\n    - {\n    +-static void populate_worktree_map(struct hashmap *map, struct worktree **worktrees)\n    +-{\n     -\tint i;\n    -+\tstruct strbuf ref = STRBUF_INIT;\n    - \n    +-\n     -\tfor (i = 0; worktrees[i]; i++) {\n     -\t\tif (worktrees[i]->head_ref) {\n     -\t\t\tstruct ref_to_worktree_entry *entry;\n    @@ ref-filter.c: static int get_object(struct ref_array_item *ref, int deref,\n     -\t\t\tentry->wt = worktrees[i];\n     -\t\t\thashmap_entry_init(&entry->ent,\n     -\t\t\t\t\tstrhash(worktrees[i]->head_ref));\n    -+\tfor (size_t i = 0; worktrees[i]; i++) {\n    -+\t\tstruct worktree *wt = worktrees[i];\n    -+\t\tstruct wt_status_state state = { 0 };\n    - \n    +-\n     -\t\t\thashmap_add(map, &entry->ent);\n    -+\t\tif (wt->is_bare)\n    -+\t\t\tcontinue;\n    -+\n    -+\t\tif (wt->head_ref)\n    -+\t\t\tadd_worktree_entry(map, wt, wt->head_ref);\n    -+\n    -+\t\tif (wt_status_check_rebase(wt, &state) &&\n    -+\t\t    (state.rebase_in_progress ||\n    -+\t\t     state.rebase_interactive_in_progress) &&\n    -+\t\t    state.branch) {\n    -+\t\t\tstrbuf_reset(&ref);\n    -+\t\t\tstrbuf_addf(&ref, \"refs/heads/%s\", state.branch);\n    -+\t\t\tadd_worktree_entry(map, wt, ref.buf);\n    - \t\t}\n    -+\t\twt_status_state_free_buffers(&state);\n    -+\n    -+\t\tif (wt_status_check_bisect(wt, &state) &&\n    -+\t\t    state.bisecting_from) {\n    -+\t\t\tstrbuf_reset(&ref);\n    -+\t\t\tstrbuf_addf(&ref, \"refs/heads/%s\", state.bisecting_from);\n    -+\t\t\tadd_worktree_entry(map, wt, ref.buf);\n    -+\t\t}\n    -+\t\twt_status_state_free_buffers(&state);\n    - \t}\n    -+\tstrbuf_release(&ref);\n    +-\t\t}\n    +-\t}\n    +-}\n    +-\n    +-static void lazy_init_worktree_map(void)\n    +-{\n    +-\tif (ref_to_worktree_map.worktrees)\n    +-\t\treturn;\n    +-\n    +-\tref_to_worktree_map.worktrees = get_worktrees(the_repository);\n    +-\thashmap_init(&(ref_to_worktree_map.map), ref_to_worktree_map_cmpfnc, NULL, 0);\n    +-\tpopulate_worktree_map(&(ref_to_worktree_map.map), ref_to_worktree_map.worktrees);\n    +-}\n    +-\n    + static char *get_worktree_path(const struct ref_array_item *ref)\n    + {\n    +-\tstruct hashmap_entry entry, *e;\n    +-\tstruct ref_to_worktree_entry *lookup_result;\n    ++\tconst char *path = branch_checked_out_worktree(ref->refname);\n    + \n    +-\tlazy_init_worktree_map();\n    +-\n    +-\thashmap_entry_init(&entry, strhash(ref->refname));\n    +-\te = hashmap_get(&(ref_to_worktree_map.map), &entry, ref->refname);\n    +-\n    +-\tif (!e)\n    +-\t\treturn xstrdup(\"\");\n    +-\n    +-\tlookup_result = container_of(e, struct ref_to_worktree_entry, ent);\n    +-\n    +-\treturn xstrdup(lookup_result->wt->path);\n    ++\treturn xstrdup(path ? path : \"\");\n    + }\n    + \n    + /*\n    +@@ ref-filter.c: void ref_array_clear(struct ref_array *array)\n    + \tFREE_AND_NULL(used_atom);\n    + \tused_atom_cnt = 0;\n    + \n    +-\tif (ref_to_worktree_map.worktrees) {\n    +-\t\thashmap_clear_and_free(&(ref_to_worktree_map.map),\n    +-\t\t\t\t\tstruct ref_to_worktree_entry, ent);\n    +-\t\tfree_worktrees(ref_to_worktree_map.worktrees);\n    +-\t\tref_to_worktree_map.worktrees = NULL;\n    +-\t}\n    +-\n    + \tFREE_AND_NULL(array->counts);\n      }\n      \n    - static void lazy_init_worktree_map(void)\n     \n      ## t/t3203-branch-output.sh ##\n     @@ t/t3203-branch-output.sh: test_expect_success 'worktree colors correct' '\n    @@ t/t3203-branch-output.sh: test_expect_success 'worktree colors correct' '\n     +test_expect_success 'branch being rebased in a worktree is marked' '\n     +\ttest_when_finished \"git -C rebase_dir rebase --abort; rm -rf rebase_dir; git worktree prune; git branch -D rebasing\" &&\n     +\tgit worktree add -b rebasing rebase_dir branch-two &&\n    -+\tgit -C rebase_dir commit --allow-empty -m rebase-me &&\n    -+\ttest_must_fail git -C rebase_dir rebase -f -x false HEAD~1 &&\n    -+\ttest_must_fail git -C rebase_dir symbolic-ref -q HEAD &&\n    ++\t(\n    ++\t\tcd rebase_dir &&\n    ++\t\tgit commit --allow-empty -m rebase-me &&\n    ++\t\ttest_must_fail git rebase -f -x false HEAD~1 &&\n    ++\t\ttest_must_fail git symbolic-ref -q HEAD\n    ++\t) &&\n     +\tgit branch --list rebasing >actual &&\n     +\techo \"+ rebasing\" >expect &&\n     +\ttest_cmp expect actual\n     +'\n     +\n    -+test_expect_success 'branch being rebased is marked in its own worktree' '\n    -+\ttest_when_finished \"git -C rebase_dir rebase --abort; rm -rf rebase_dir; git worktree prune; git branch -D rebasing\" &&\n    -+\tgit worktree add -b rebasing rebase_dir branch-two &&\n    -+\tgit -C rebase_dir commit --allow-empty -m rebase-me &&\n    -+\ttest_must_fail git -C rebase_dir rebase -f -x false HEAD~1 &&\n    -+\tgit -C rebase_dir branch --list rebasing >actual &&\n    -+\techo \"+ rebasing\" >expect &&\n    -+\ttest_cmp expect actual &&\n    -+\tgit -C rebase_dir branch >actual &&\n    -+\ttest_grep \"^\\* (no branch, rebasing rebasing)\" actual\n    -+'\n    -+\n     +test_expect_success 'branch being bisected in a worktree is marked' '\n     +\ttest_when_finished \"git -C bisect_dir bisect reset; rm -rf bisect_dir; git worktree prune; git branch -D bisecting\" &&\n     +\tgit worktree add -b bisecting bisect_dir branch-two &&\n    -+\tgit -C bisect_dir commit --allow-empty -m bisect-one &&\n    -+\tgit -C bisect_dir commit --allow-empty -m bisect-two &&\n    -+\tgit -C bisect_dir bisect start HEAD HEAD~2 &&\n    -+\ttest_must_fail git -C bisect_dir symbolic-ref -q HEAD &&\n    ++\t(\n    ++\t\tcd bisect_dir &&\n    ++\t\tgit commit --allow-empty -m bisect-one &&\n    ++\t\tgit commit --allow-empty -m bisect-two &&\n    ++\t\tgit bisect start HEAD HEAD~2 &&\n    ++\t\ttest_must_fail git symbolic-ref -q HEAD\n    ++\t) &&\n     +\tgit branch --list bisecting >actual &&\n     +\techo \"+ bisecting\" >expect &&\n     +\ttest_cmp expect actual\n    @@ t/t6302-for-each-ref-filter.sh: test_expect_success 'validate worktree atom' '\n     +\tgit for-each-ref --format=\"%(worktreepath)\" refs/heads/ >actual &&\n     +\ttest_grep ! detached-rebase actual\n     +'\n    ++\n    ++test_expect_success 'worktree atom ignores the HEAD of a bare repository' '\n    ++\ttest_when_finished \"rm -rf bare.git bare-wt\" &&\n    ++\tgit clone --bare . bare.git &&\n    ++\tgit -C bare.git symbolic-ref HEAD refs/heads/main &&\n    ++\tgit -C bare.git worktree add --detach ../bare-wt main &&\n    ++\tgit -C bare.git for-each-ref --format=\"%(worktreepath)\" refs/heads/main >actual &&\n    ++\techo >expect &&\n    ++\ttest_cmp expect actual &&\n    ++\tgit -C bare-wt branch --list main >actual &&\n    ++\techo \"  main\" >expect &&\n    ++\ttest_cmp expect actual\n    ++'\n     +\n      test_expect_success 'start after with empty value' '\n      \tcat >expect <<-\\EOF &&\n-:  ---------- > 4:  56e29c448f branch: do not mark the branch the current worktree is rebasing\n-- \n2.56.0\n\n\n"},{"id":"557216","messageId":"20261102182930.3511-2-me@jaspermayone.com","threadId":"66626","inReplyTo":"20261102182930.3511-1-me@jaspermayone.com","subject":"[PATCH v4 1/4] worktree remove: refuse a worktree in the middle of an operation","fromName":"Jasper Mayone","fromEmail":"me@jaspermayone.com","sentAt":"2026-11-02T18:29:27Z","receivedAt":"2026-11-02T18:29:27Z","isPatch":true,"sender":{"key":"me@jaspermayone.com","avatar":null},"body":"\"git worktree remove\" refuses to remove a worktree with modified or\nuntracked files unless --force is given, but it removes a worktree\nthat is in the middle of a rebase, a \"git am\" session, a bisect, a\ncherry-pick or a revert when its files happen to be clean. The state\nof that operation lives in the worktree's own $GIT_DIR, so it is lost\nwith the worktree. Examples of such a clean worktree are a rebase\nstopped at an \"edit\" or \"break\" line or after a failed \"exec\", a\ncherry-pick sequence after a conflict was resolved and committed, and\na single cherry-pick or revert whose conflict was resolved to the\ncurrent contents, which leaves CHERRY_PICK_HEAD or REVERT_HEAD behind.\n\nTreat such a worktree as unclean: refuse to remove it unless --force\nis given, and name the operation in the message.\n\nA worktree whose directory is already gone is still removed without\n--force, as before: the check is part of the check for a clean\nworktree, which is only done when the directory exists.\n\nReported-by: D. Ben Knoble <ben.knoble@gmail.com>\nSigned-off-by: Jasper Mayone <me@jaspermayone.com>\n---\n Documentation/git-worktree.adoc | 17 ++++--\n builtin/worktree.c              | 37 ++++++++++++-\n t/t2403-worktree-move.sh        | 94 +++++++++++++++++++++++++++++++++\n 3 files changed, 142 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/git-worktree.adoc b/Documentation/git-worktree.adoc\nindex e6e77252ab..4a7dc47dd8 100644\n--- a/Documentation/git-worktree.adoc\n+++ b/Documentation/git-worktree.adoc\n@@ -141,10 +141,16 @@ the new working tree location.\n \n `remove`::\n \n-Remove a worktree. Only clean worktrees (no untracked files and no\n-modification in tracked files) can be removed. Unclean worktrees or ones\n-with submodules can be removed with `--force`. The main worktree cannot be\n-removed.\n+Remove a worktree. The main worktree cannot be removed. Without\n+`--force`, a worktree is not removed when:\n++\n+--\n+* it has untracked files or modifications in tracked files,\n+* it has submodules, or\n+* it is in the middle of a rebase, a `git am` session, a bisect, a\n+  cherry-pick or a revert, because removing it loses the state of that\n+  operation.\n+--\n \n `repair [<path>...]`::\n \n@@ -189,7 +195,8 @@ twice. If the destination is already assigned to some other worktree but is\n missing (for instance, if _<new-path>_ was deleted manually), then `--force`\n allows the move to proceed; use `--force` twice if the destination is locked.\n +\n-`remove` refuses to remove an unclean worktree unless `--force` is used.\n+`remove` refuses to remove a worktree in the cases listed under\n+`remove` above unless `--force` is used.\n To remove a locked worktree, specify `--force` twice.\n \n `-b <new-branch>`::\ndiff --git a/builtin/worktree.c b/builtin/worktree.c\nindex 77ecd0f71f..7f53215cda 100644\n--- a/builtin/worktree.c\n+++ b/builtin/worktree.c\n@@ -26,6 +26,7 @@\n #include \"submodule.h\"\n #include \"utf8.h\"\n #include \"worktree.h\"\n+#include \"wt-status.h\"\n #include \"quote.h\"\n \n #define BUILTIN_WORKTREE_ADD_USAGE \\\n@@ -1408,6 +1409,38 @@ static void check_clean_worktree(struct worktree *wt,\n \t\t\t  original_path, ret);\n }\n \n+/*\n+ * A rebase, \"git am\", bisect, cherry-pick or revert keeps its state in\n+ * the worktree's own $GIT_DIR, and removing the worktree throws that\n+ * away even when \"git status\" says the files are clean.\n+ */\n+static void check_no_operation_in_progress(struct worktree *wt,\n+\t\t\t\t\t   const char *original_path)\n+{\n+\tstruct wt_status_state state = { 0 };\n+\n+\tif (wt_status_check_rebase(wt, &state)) {\n+\t\tif (state.am_in_progress)\n+\t\t\tdie(_(\"'%s' is in the middle of an am session, use --force to delete it\"),\n+\t\t\t    original_path);\n+\t\tdie(_(\"'%s' is in the middle of a rebase, use --force to delete it\"),\n+\t\t    original_path);\n+\t}\n+\tif (wt_status_check_bisect(wt, &state))\n+\t\tdie(_(\"'%s' is in the middle of a bisect, use --force to delete it\"),\n+\t\t    original_path);\n+\twt_status_state_free_buffers(&state);\n+\tif (refs_ref_exists(get_worktree_ref_store(wt), \"CHERRY_PICK_HEAD\"))\n+\t\tdie(_(\"'%s' is in the middle of a cherry-pick, use --force to delete it\"),\n+\t\t    original_path);\n+\tif (refs_ref_exists(get_worktree_ref_store(wt), \"REVERT_HEAD\"))\n+\t\tdie(_(\"'%s' is in the middle of a revert, use --force to delete it\"),\n+\t\t    original_path);\n+\tif (is_directory(worktree_git_path(wt, \"sequencer\")))\n+\t\tdie(_(\"'%s' is in the middle of a cherry-pick or revert, use --force to delete it\"),\n+\t\t    original_path);\n+}\n+\n static int delete_git_work_tree(struct worktree *wt)\n {\n \tstruct strbuf sb = STRBUF_INIT;\n@@ -1461,8 +1494,10 @@ static int remove_worktree(int ac, const char **av, const char *prefix,\n \tstrbuf_release(&errmsg);\n \n \tif (file_exists(wt->path)) {\n-\t\tif (!force)\n+\t\tif (!force) {\n+\t\t\tcheck_no_operation_in_progress(wt, av[0]);\n \t\t\tcheck_clean_worktree(wt, av[0]);\n+\t\t}\n \n \t\tret |= delete_git_work_tree(wt);\n \t}\ndiff --git a/t/t2403-worktree-move.sh b/t/t2403-worktree-move.sh\nindex 69768c1207..2e45cd77f9 100755\n--- a/t/t2403-worktree-move.sh\n+++ b/t/t2403-worktree-move.sh\n@@ -271,4 +271,98 @@ test_expect_success 'move worktree with relative path to absolute path' '\n \ttest_cmp expect .git/worktrees/absolute/gitdir\n '\n \n+test_expect_success 'setup commits that conflict for am, cherry-pick and revert' '\n+\tgit worktree add --detach conflict-setup init &&\n+\t(\n+\t\tcd conflict-setup &&\n+\t\ttest_commit base conflict.t base &&\n+\t\tgit switch --detach init &&\n+\t\ttest_commit side conflict.t side &&\n+\t\ttest_commit side-two\n+\t) &&\n+\tgit worktree remove conflict-setup\n+'\n+\n+test_expect_success 'remove worktree in the middle of a rebase' '\n+\ttest_when_finished \"rm -rf mid-rebase; git worktree prune\" &&\n+\tgit worktree add --detach mid-rebase side &&\n+\ttest_must_fail git -C mid-rebase rebase -f -x false init &&\n+\ttest_must_fail git worktree remove mid-rebase 2>err &&\n+\ttest_grep \"in the middle of a rebase\" err &&\n+\ttest_path_is_dir mid-rebase &&\n+\tgit worktree remove --force mid-rebase &&\n+\ttest_path_is_missing mid-rebase\n+'\n+\n+test_expect_success 'remove worktree in the middle of an am session' '\n+\ttest_when_finished \"rm -rf mid-am patch; git worktree prune\" &&\n+\tgit format-patch -1 --stdout side >patch &&\n+\tgit worktree add --detach mid-am base &&\n+\t(\n+\t\tcd mid-am &&\n+\t\ttest_must_fail git am ../patch &&\n+\t\tgit reset --hard\n+\t) &&\n+\ttest_must_fail git worktree remove mid-am 2>err &&\n+\ttest_grep \"in the middle of an am session\" err &&\n+\tgit worktree remove --force mid-am\n+'\n+\n+test_expect_success 'remove worktree in the middle of a bisect' '\n+\ttest_when_finished \"rm -rf mid-bisect; git worktree prune\" &&\n+\tgit worktree add --detach mid-bisect side-two &&\n+\tgit -C mid-bisect bisect start side-two init &&\n+\ttest_must_fail git worktree remove mid-bisect 2>err &&\n+\ttest_grep \"in the middle of a bisect\" err &&\n+\tgit worktree remove --force mid-bisect\n+'\n+\n+test_expect_success 'remove worktree in the middle of a cherry-pick' '\n+\ttest_when_finished \"rm -rf mid-pick; git worktree prune\" &&\n+\tgit worktree add --detach mid-pick base &&\n+\t(\n+\t\tcd mid-pick &&\n+\t\ttest_must_fail git cherry-pick side &&\n+\t\tgit checkout --ours conflict.t &&\n+\t\tgit add conflict.t &&\n+\t\tgit rev-parse --verify CHERRY_PICK_HEAD\n+\t) &&\n+\ttest_path_is_missing .git/worktrees/mid-pick/sequencer &&\n+\ttest_must_fail git worktree remove mid-pick 2>err &&\n+\ttest_grep \"in the middle of a cherry-pick,\" err &&\n+\tgit worktree remove --force mid-pick\n+'\n+\n+test_expect_success 'remove worktree in the middle of a revert' '\n+\ttest_when_finished \"rm -rf mid-revert; git worktree prune\" &&\n+\tgit worktree add --detach mid-revert base &&\n+\t(\n+\t\tcd mid-revert &&\n+\t\ttest_must_fail git revert side &&\n+\t\tgit checkout --ours conflict.t &&\n+\t\tgit add conflict.t &&\n+\t\tgit rev-parse --verify REVERT_HEAD\n+\t) &&\n+\ttest_path_is_missing .git/worktrees/mid-revert/sequencer &&\n+\ttest_must_fail git worktree remove mid-revert 2>err &&\n+\ttest_grep \"in the middle of a revert\" err &&\n+\tgit worktree remove --force mid-revert\n+'\n+\n+test_expect_success 'remove worktree in the middle of a cherry-pick sequence' '\n+\ttest_when_finished \"rm -rf mid-pick-seq; git worktree prune\" &&\n+\tgit worktree add --detach mid-pick-seq base &&\n+\t(\n+\t\tcd mid-pick-seq &&\n+\t\ttest_must_fail git cherry-pick side side-two &&\n+\t\tgit checkout --theirs conflict.t &&\n+\t\tgit add conflict.t &&\n+\t\tgit commit --no-edit\n+\t) &&\n+\ttest_path_is_dir .git/worktrees/mid-pick-seq/sequencer &&\n+\ttest_must_fail git worktree remove mid-pick-seq 2>err &&\n+\ttest_grep \"in the middle of a cherry-pick or revert\" err &&\n+\tgit worktree remove --force mid-pick-seq\n+'\n+\n test_done\n-- \n2.56.0\n\n\n"},{"id":"557217","messageId":"20261102182930.3511-3-me@jaspermayone.com","threadId":"66626","inReplyTo":"20261102182930.3511-1-me@jaspermayone.com","subject":"[PATCH v4 2/4] doc: for-each-ref: say how to read %(worktreepath) safely","fromName":"Jasper Mayone","fromEmail":"me@jaspermayone.com","sentAt":"2026-11-02T18:29:28Z","receivedAt":"2026-11-02T18:29:28Z","isPatch":true,"sender":{"key":"me@jaspermayone.com","avatar":null},"body":"The path that %(worktreepath) gives can contain a newline, which\nbreaks a script that reads one record per line. Say how to avoid\nthat: use a quoting option, or end the atom with %00 and look for the\nNUL before the LF that ends the record.\n\nAlso say that only one worktree is shown when a branch is checked out\nin more than one worktree.\n\nSuggested-by: Phillip Wood <phillip.wood123@gmail.com>\nSigned-off-by: Jasper Mayone <me@jaspermayone.com>\n---\n Documentation/git-for-each-ref.adoc | 7 ++++++-\n 1 file changed, 6 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/git-for-each-ref.adoc b/Documentation/git-for-each-ref.adoc\nindex a7b31e8aaf..9247737aa7 100644\n--- a/Documentation/git-for-each-ref.adoc\n+++ b/Documentation/git-for-each-ref.adoc\n@@ -183,7 +183,12 @@ but if used together the last option is selected.\n `worktreepath`::\n \tThe absolute path to the worktree in which the ref is checked\n \tout, if it is checked out in any linked worktree. Empty string\n-\totherwise.\n+\totherwise. If the ref is checked out in more than one worktree,\n+\tonly one of them is shown. The path can contain a newline, so\n+\tuse one of the quoting options such as `--shell`, or follow\n+\t`%(worktreepath)` with `%00` and parse the output by looking for\n+\tthe `NUL` after the path and then for the `LF` that ends each\n+\trecord.\n \n `ahead-behind:<commit-ish>`::\n \tTwo integers, separated by a space, demonstrating the number of\n-- \n2.56.0\n\n\n"},{"id":"557218","messageId":"20261102182930.3511-4-me@jaspermayone.com","threadId":"66626","inReplyTo":"20261102182930.3511-1-me@jaspermayone.com","subject":"[PATCH v4 3/4] ref-filter: name the worktree of a branch being rebased or bisected","fromName":"Jasper Mayone","fromEmail":"me@jaspermayone.com","sentAt":"2026-11-02T18:29:29Z","receivedAt":"2026-11-02T18:29:29Z","isPatch":true,"sender":{"key":"me@jaspermayone.com","avatar":null},"body":"%(worktreepath) gives the worktree that has a branch checked out, and\n\"git branch\" uses it to mark such branches with \"+\". It only looks at\nthe HEAD of each worktree, so while a worktree is rebasing or\nbisecting a branch, its HEAD is detached and the branch shows no\nworktree at all.\n\nThe rest of Git already treats that branch as checked out: \"git\nworktree add\" and \"git branch -d\" refuse it, because finishing the\nrebase or bisect will check it out again. But no command shows which\nworktree is working on the branch: \"git worktree list\" shows such a\nworktree as \"(detached HEAD)\", and %(worktreepath) is empty.\n\nbranch.c already knows the answer, as prepare_checked_out_branches()\nrecords the branch that each worktree has checked out, is rebasing or\nis bisecting. Add branch_checked_out_worktree() to look a branch up in\nthat list, and use it for %(worktreepath) in place of the map that\nref-filter.c builds from the HEAD of each worktree.\n\nThe new function skips the branches that \"rebase --update-refs\" will\nupdate. branch_checked_out() includes them, so that they cannot be\nchecked out or deleted during the rebase, but the worktree that runs\nthe rebase has not checked them out. %(worktreepath) therefore stays\nempty for them, unlike branch_checked_out().\n\nThis also changes the result for a bare repository: its HEAD names a\nbranch, and %(worktreepath) used to give the path of the bare\nrepository for that branch. \"git branch\" in a linked worktree marked\nthe branch with \"+\", although no worktree has it checked out and \"git\nworktree add\" lets you check it out. prepare_checked_out_branches()\nskips bare repositories, so %(worktreepath) is now empty for that\nbranch and \"git branch\" no longer marks it.\n\nReported-by: Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com>\nHelped-by: Phillip Wood <phillip.wood123@gmail.com>\nSigned-off-by: Jasper Mayone <me@jaspermayone.com>\n---\n Documentation/git-branch.adoc       |  6 ++-\n Documentation/git-for-each-ref.adoc |  5 +-\n branch.c                            | 13 +++++\n branch.h                            |  9 ++++\n ref-filter.c                        | 74 ++--------------------------\n t/t3203-branch-output.sh            | 29 +++++++++++\n t/t6302-for-each-ref-filter.sh      | 76 +++++++++++++++++++++++++++++\n 7 files changed, 138 insertions(+), 74 deletions(-)\n\ndiff --git a/Documentation/git-branch.adoc b/Documentation/git-branch.adoc\nindex bfdf459329..510b0afc74 100644\n--- a/Documentation/git-branch.adoc\n+++ b/Documentation/git-branch.adoc\n@@ -33,8 +33,10 @@ DESCRIPTION\n If `--list` is given, or if there are no non-option arguments, existing\n branches are listed; the current branch will be highlighted in green and\n marked with an asterisk.  Any branches checked out in linked worktrees will\n-be highlighted in cyan and marked with a plus sign. Option `-r` causes the\n-remote-tracking branches to be listed,\n+be highlighted in cyan and marked with a plus sign. A branch that a\n+worktree is rebasing or bisecting counts as checked out in that\n+worktree, so the other worktrees mark it in the same way. Option `-r`\n+causes the remote-tracking branches to be listed,\n and option `-a` shows both local and remote branches.\n \n If a `<pattern>`\ndiff --git a/Documentation/git-for-each-ref.adoc b/Documentation/git-for-each-ref.adoc\nindex 9247737aa7..4456b4809a 100644\n--- a/Documentation/git-for-each-ref.adoc\n+++ b/Documentation/git-for-each-ref.adoc\n@@ -182,7 +182,10 @@ but if used together the last option is selected.\n \n `worktreepath`::\n \tThe absolute path to the worktree in which the ref is checked\n-\tout, if it is checked out in any linked worktree. Empty string\n+\tout, if it is checked out in any linked worktree. A branch that\n+\ta worktree is rebasing or bisecting counts as checked out there,\n+\talthough that worktree's `HEAD` is detached. A branch that\n+\t`git rebase --update-refs` will update does not. Empty string\n \totherwise. If the ref is checked out in more than one worktree,\n \tonly one of them is shown. The path can contain a newline, so\n \tuse one of the quoting options such as `--shell`, or follow\ndiff --git a/branch.c b/branch.c\nindex 5bb1c28915..8dab72d857 100644\n--- a/branch.c\n+++ b/branch.c\n@@ -485,6 +485,19 @@ const char *branch_checked_out(const char *refname)\n \treturn strmap_get(&current_checked_out_branches, refname);\n }\n \n+const char *branch_checked_out_worktree(const char *refname)\n+{\n+\tconst char *path = NULL;\n+\n+\tprepare_checked_out_branches();\n+\tfor (size_t i = 0; i < checked_out_branches_nr; i++) {\n+\t\tif (checked_out_branches[i].kind != BRANCH_CHECKOUT_KIND_UPDATE_REF &&\n+\t\t    !strcmp(refname, checked_out_branches[i].refname))\n+\t\t\tpath = checked_out_branches[i].path;\n+\t}\n+\treturn path;\n+}\n+\n const char *branch_bisecting(const char *refname)\n {\n \tprepare_checked_out_branches();\ndiff --git a/branch.h b/branch.h\nindex 95b2431f24..60cb92a3e4 100644\n--- a/branch.h\n+++ b/branch.h\n@@ -106,6 +106,15 @@ void create_branches_recursively(struct repository *r, const char *name,\n  */\n const char *branch_checked_out(const char *refname);\n \n+/*\n+ * If a worktree has the branch 'refname' checked out, or is rebasing or\n+ * bisecting it, return the path of that worktree, or of the last one if\n+ * there are more. Otherwise return NULL. Unlike branch_checked_out(),\n+ * this ignores the branches that \"git rebase --update-refs\" will\n+ * update, because no worktree has them checked out.\n+ */\n+const char *branch_checked_out_worktree(const char *refname);\n+\n /*\n  * If the branch at 'refname' is currently used for bisecting in a\n  * worktree, then return the path to that worktree.\ndiff --git a/ref-filter.c b/ref-filter.c\nindex bdf54f6f59..b636e56199 100644\n--- a/ref-filter.c\n+++ b/ref-filter.c\n@@ -2,6 +2,7 @@\n #define DISABLE_SIGN_COMPARE_WARNINGS\n \n #include \"git-compat-util.h\"\n+#include \"branch.h\"\n #include \"environment.h\"\n #include \"gettext.h\"\n #include \"config.h\"\n@@ -95,30 +96,6 @@ static struct expand_data {\n \tstruct object_info info;\n } oi, oi_deref;\n \n-struct ref_to_worktree_entry {\n-\tstruct hashmap_entry ent;\n-\tstruct worktree *wt; /* key is wt->head_ref */\n-};\n-\n-static int ref_to_worktree_map_cmpfnc(const void *lookupdata UNUSED,\n-\t\t\t\t      const struct hashmap_entry *eptr,\n-\t\t\t\t      const struct hashmap_entry *kptr,\n-\t\t\t\t      const void *keydata_aka_refname)\n-{\n-\tconst struct ref_to_worktree_entry *e, *k;\n-\n-\te = container_of(eptr, const struct ref_to_worktree_entry, ent);\n-\tk = container_of(kptr, const struct ref_to_worktree_entry, ent);\n-\n-\treturn strcmp(e->wt->head_ref,\n-\t\tkeydata_aka_refname ? keydata_aka_refname : k->wt->head_ref);\n-}\n-\n-static struct ref_to_worktree_map {\n-\tstruct hashmap map;\n-\tstruct worktree **worktrees;\n-} ref_to_worktree_map;\n-\n /*\n  * The enum atom_type is used as the index of valid_atom array.\n  * In the atom parsing stage, it will be passed to used_atom.atom_type\n@@ -2380,49 +2357,11 @@ static int get_object(struct ref_array_item *ref, int deref,\n \treturn ret;\n }\n \n-static void populate_worktree_map(struct hashmap *map, struct worktree **worktrees)\n-{\n-\tint i;\n-\n-\tfor (i = 0; worktrees[i]; i++) {\n-\t\tif (worktrees[i]->head_ref) {\n-\t\t\tstruct ref_to_worktree_entry *entry;\n-\t\t\tentry = xmalloc(sizeof(*entry));\n-\t\t\tentry->wt = worktrees[i];\n-\t\t\thashmap_entry_init(&entry->ent,\n-\t\t\t\t\tstrhash(worktrees[i]->head_ref));\n-\n-\t\t\thashmap_add(map, &entry->ent);\n-\t\t}\n-\t}\n-}\n-\n-static void lazy_init_worktree_map(void)\n-{\n-\tif (ref_to_worktree_map.worktrees)\n-\t\treturn;\n-\n-\tref_to_worktree_map.worktrees = get_worktrees(the_repository);\n-\thashmap_init(&(ref_to_worktree_map.map), ref_to_worktree_map_cmpfnc, NULL, 0);\n-\tpopulate_worktree_map(&(ref_to_worktree_map.map), ref_to_worktree_map.worktrees);\n-}\n-\n static char *get_worktree_path(const struct ref_array_item *ref)\n {\n-\tstruct hashmap_entry entry, *e;\n-\tstruct ref_to_worktree_entry *lookup_result;\n+\tconst char *path = branch_checked_out_worktree(ref->refname);\n \n-\tlazy_init_worktree_map();\n-\n-\thashmap_entry_init(&entry, strhash(ref->refname));\n-\te = hashmap_get(&(ref_to_worktree_map.map), &entry, ref->refname);\n-\n-\tif (!e)\n-\t\treturn xstrdup(\"\");\n-\n-\tlookup_result = container_of(e, struct ref_to_worktree_entry, ent);\n-\n-\treturn xstrdup(lookup_result->wt->path);\n+\treturn xstrdup(path ? path : \"\");\n }\n \n /*\n@@ -3195,13 +3134,6 @@ void ref_array_clear(struct ref_array *array)\n \tFREE_AND_NULL(used_atom);\n \tused_atom_cnt = 0;\n \n-\tif (ref_to_worktree_map.worktrees) {\n-\t\thashmap_clear_and_free(&(ref_to_worktree_map.map),\n-\t\t\t\t\tstruct ref_to_worktree_entry, ent);\n-\t\tfree_worktrees(ref_to_worktree_map.worktrees);\n-\t\tref_to_worktree_map.worktrees = NULL;\n-\t}\n-\n \tFREE_AND_NULL(array->counts);\n }\n \ndiff --git a/t/t3203-branch-output.sh b/t/t3203-branch-output.sh\nindex 7c44186341..debfac5f26 100755\n--- a/t/t3203-branch-output.sh\n+++ b/t/t3203-branch-output.sh\n@@ -442,6 +442,35 @@ test_expect_success 'worktree colors correct' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'branch being rebased in a worktree is marked' '\n+\ttest_when_finished \"git -C rebase_dir rebase --abort; rm -rf rebase_dir; git worktree prune; git branch -D rebasing\" &&\n+\tgit worktree add -b rebasing rebase_dir branch-two &&\n+\t(\n+\t\tcd rebase_dir &&\n+\t\tgit commit --allow-empty -m rebase-me &&\n+\t\ttest_must_fail git rebase -f -x false HEAD~1 &&\n+\t\ttest_must_fail git symbolic-ref -q HEAD\n+\t) &&\n+\tgit branch --list rebasing >actual &&\n+\techo \"+ rebasing\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'branch being bisected in a worktree is marked' '\n+\ttest_when_finished \"git -C bisect_dir bisect reset; rm -rf bisect_dir; git worktree prune; git branch -D bisecting\" &&\n+\tgit worktree add -b bisecting bisect_dir branch-two &&\n+\t(\n+\t\tcd bisect_dir &&\n+\t\tgit commit --allow-empty -m bisect-one &&\n+\t\tgit commit --allow-empty -m bisect-two &&\n+\t\tgit bisect start HEAD HEAD~2 &&\n+\t\ttest_must_fail git symbolic-ref -q HEAD\n+\t) &&\n+\tgit branch --list bisecting >actual &&\n+\techo \"+ bisecting\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success \"set up color tests\" '\n \techo \"<RED>main<RESET>\" >expect.color &&\n \techo \"main\" >expect.bare &&\ndiff --git a/t/t6302-for-each-ref-filter.sh b/t/t6302-for-each-ref-filter.sh\nindex 7f060d97bf..317ed6f6cd 100755\n--- a/t/t6302-for-each-ref-filter.sh\n+++ b/t/t6302-for-each-ref-filter.sh\n@@ -3,6 +3,7 @@\n test_description='test for-each-refs usage of ref-filter APIs'\n \n . ./test-lib.sh\n+. \"$TEST_DIRECTORY\"/lib-rebase.sh\n . \"$TEST_DIRECTORY\"/lib-gpg.sh\n \n test_expect_success 'setup some history and refs' '\n@@ -541,6 +542,81 @@ test_expect_success 'validate worktree atom' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'worktree atom names a worktree rebasing the branch' '\n+\ttest_when_finished \"git -C rebasing rebase --abort; rm -rf rebasing; git worktree prune; git branch -D rebasing\" &&\n+\tgit worktree add -b rebasing rebasing main &&\n+\tgit -C rebasing commit --allow-empty -m rebase-me &&\n+\ttest_must_fail git -C rebasing rebase -f -x false main &&\n+\ttest_must_fail git -C rebasing symbolic-ref -q HEAD &&\n+\techo \"$(pwd)/rebasing\" >expect &&\n+\tgit for-each-ref --format=\"%(worktreepath)\" refs/heads/rebasing >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'worktree atom names a worktree bisecting the branch' '\n+\ttest_when_finished \"git -C bisecting bisect reset; rm -rf bisecting; git worktree prune; git branch -D bisecting\" &&\n+\tgit worktree add -b bisecting bisecting main &&\n+\tgit -C bisecting commit --allow-empty -m bisect-one &&\n+\tgit -C bisecting commit --allow-empty -m bisect-two &&\n+\tgit -C bisecting bisect start HEAD main &&\n+\ttest_must_fail git -C bisecting symbolic-ref -q HEAD &&\n+\techo \"$(pwd)/bisecting\" >expect &&\n+\tgit for-each-ref --format=\"%(worktreepath)\" refs/heads/bisecting >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'worktree atom with an interactive rebase stopped at edit' '\n+\ttest_when_finished \"git -C editing rebase --abort; rm -rf editing; git worktree prune; git branch -D editing\" &&\n+\tgit worktree add -b editing editing main &&\n+\tgit -C editing commit --allow-empty -m edit-me &&\n+\t(\n+\t\tset_fake_editor &&\n+\t\tFAKE_LINES=\"edit 1\" git -C editing rebase -i --keep-empty HEAD~1\n+\t) &&\n+\ttest_must_fail git -C editing symbolic-ref -q HEAD &&\n+\techo \"$(pwd)/editing\" >expect &&\n+\tgit for-each-ref --format=\"%(worktreepath)\" refs/heads/editing >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'worktree atom ignores branches rebase --update-refs will move' '\n+\ttest_when_finished \"git -C updating rebase --abort; rm -rf updating; git worktree prune; git branch -D updating mid\" &&\n+\tgit worktree add -b updating updating main &&\n+\tgit -C updating commit --allow-empty -m one &&\n+\tgit -C updating branch mid &&\n+\tgit -C updating commit --allow-empty -m two &&\n+\ttest_must_fail git -C updating rebase -f --update-refs -x false main &&\n+\tgit for-each-ref --format=\"%(refname:short) [%(worktreepath)]\" \\\n+\t\trefs/heads/updating refs/heads/mid >actual &&\n+\tcat >expect <<-EOF &&\n+\tmid []\n+\tupdating [$(pwd)/updating]\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'worktree atom ignores a rebase of a detached HEAD' '\n+\ttest_when_finished \"git -C detached-rebase rebase --abort; rm -rf detached-rebase; git worktree prune\" &&\n+\tgit worktree add --detach detached-rebase main &&\n+\tgit -C detached-rebase commit --allow-empty -m detached &&\n+\ttest_must_fail git -C detached-rebase rebase -f -x false main &&\n+\tgit for-each-ref --format=\"%(worktreepath)\" refs/heads/ >actual &&\n+\ttest_grep ! detached-rebase actual\n+'\n+\n+test_expect_success 'worktree atom ignores the HEAD of a bare repository' '\n+\ttest_when_finished \"rm -rf bare.git bare-wt\" &&\n+\tgit clone --bare . bare.git &&\n+\tgit -C bare.git symbolic-ref HEAD refs/heads/main &&\n+\tgit -C bare.git worktree add --detach ../bare-wt main &&\n+\tgit -C bare.git for-each-ref --format=\"%(worktreepath)\" refs/heads/main >actual &&\n+\techo >expect &&\n+\ttest_cmp expect actual &&\n+\tgit -C bare-wt branch --list main >actual &&\n+\techo \"  main\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'start after with empty value' '\n \tcat >expect <<-\\EOF &&\n \trefs/heads/main\n-- \n2.56.0\n\n\n"},{"id":"557219","messageId":"20261102182930.3511-5-me@jaspermayone.com","threadId":"66626","inReplyTo":"20261102182930.3511-1-me@jaspermayone.com","subject":"[PATCH v4 4/4] branch: do not mark the branch the current worktree is rebasing","fromName":"Jasper Mayone","fromEmail":"me@jaspermayone.com","sentAt":"2026-11-02T18:29:30Z","receivedAt":"2026-11-02T18:29:30Z","isPatch":true,"sender":{"key":"me@jaspermayone.com","avatar":null},"body":"Since the previous commit, \"git branch\" marks a branch with \"+\" when a\nworktree is rebasing or bisecting it. In that worktree itself, the\nlist already starts with \"* (no branch, rebasing topic)\", so the\n\"+ topic\" line below it says the same thing twice.\n\nThe default format cannot tell that apart: %(worktreepath) gives the\nsame path either way, and comparing it with %(if:equals=...) is not\nsafe because a path, or a branch name, can contain the \")\" that ends\nthe atom. So build a second format that has no \"+\" marker and no\n\"(path)\" for -vv, and use it for each branch that\nbranch_checked_out_worktree() maps to the current worktree. A branch\nthat is checked out normally in the current worktree also gets that\nformat, but %(HEAD) still marks it with \"*\" and -vv already hides\nits path, so its output does not change.\n\nTo pick a format for each branch, print_ref_list() now formats the\nbranches itself in place of print_formatted_ref_array(), keeping\n--omit-empty. A format given with --format is used for every branch,\nas before.\n\nSuggested-by: Phillip Wood <phillip.wood123@gmail.com>\nSigned-off-by: Jasper Mayone <me@jaspermayone.com>\n---\n builtin/branch.c         | 103 ++++++++++++++++++++++++++++++---------\n t/t3203-branch-output.sh |  22 ++++++++-\n 2 files changed, 101 insertions(+), 24 deletions(-)\n\ndiff --git a/builtin/branch.c b/builtin/branch.c\nindex a613148fc7..b55a400217 100644\n--- a/builtin/branch.c\n+++ b/builtin/branch.c\n@@ -418,16 +418,28 @@ static const char *quote_literal_for_format(const char *s)\n \treturn buf.buf;\n }\n \n-static char *build_format(struct ref_filter *filter, int maxwidth, const char *remote_prefix)\n+/*\n+ * With \"show_worktree\" unset, the format does not mark a branch that is\n+ * checked out in a worktree, nor show the path of that worktree. It is\n+ * used for a branch that the current worktree is rebasing or bisecting,\n+ * because the \"(no branch, ...)\" line already names it.\n+ */\n+static char *build_format(struct ref_filter *filter, int maxwidth,\n+\t\t\t  const char *remote_prefix, int show_worktree)\n {\n \tstruct strbuf fmt = STRBUF_INIT;\n \tstruct strbuf local = STRBUF_INIT;\n \tstruct strbuf remote = STRBUF_INIT;\n \n-\tstrbuf_addf(&local, \"%%(if)%%(HEAD)%%(then)* %s%%(else)%%(if)%%(worktreepath)%%(then)+ %s%%(else)  %s%%(end)%%(end)\",\n-\t\t\tbranch_get_color(BRANCH_COLOR_CURRENT),\n-\t\t\tbranch_get_color(BRANCH_COLOR_WORKTREE),\n-\t\t\tbranch_get_color(BRANCH_COLOR_LOCAL));\n+\tif (show_worktree)\n+\t\tstrbuf_addf(&local, \"%%(if)%%(HEAD)%%(then)* %s%%(else)%%(if)%%(worktreepath)%%(then)+ %s%%(else)  %s%%(end)%%(end)\",\n+\t\t\t    branch_get_color(BRANCH_COLOR_CURRENT),\n+\t\t\t    branch_get_color(BRANCH_COLOR_WORKTREE),\n+\t\t\t    branch_get_color(BRANCH_COLOR_LOCAL));\n+\telse\n+\t\tstrbuf_addf(&local, \"%%(if)%%(HEAD)%%(then)* %s%%(else)  %s%%(end)\",\n+\t\t\t    branch_get_color(BRANCH_COLOR_CURRENT),\n+\t\t\t    branch_get_color(BRANCH_COLOR_LOCAL));\n \tstrbuf_addf(&remote, \"  %s\",\n \t\t    branch_get_color(BRANCH_COLOR_REMOTE));\n \n@@ -447,8 +459,9 @@ static char *build_format(struct ref_filter *filter, int maxwidth, const char *r\n \n \t\tif (filter->verbose > 1)\n \t\t{\n-\t\t\tstrbuf_addf(&local, \"%%(if:notequals=*)%%(HEAD)%%(then)%%(if)%%(worktreepath)%%(then)(%s%%(worktreepath)%s) %%(end)%%(end)\",\n-\t\t\t\t    branch_get_color(BRANCH_COLOR_WORKTREE), branch_get_color(BRANCH_COLOR_RESET));\n+\t\t\tif (show_worktree)\n+\t\t\t\tstrbuf_addf(&local, \"%%(if:notequals=*)%%(HEAD)%%(then)%%(if)%%(worktreepath)%%(then)(%s%%(worktreepath)%s) %%(end)%%(end)\",\n+\t\t\t\t\t    branch_get_color(BRANCH_COLOR_WORKTREE), branch_get_color(BRANCH_COLOR_RESET));\n \t\t\tstrbuf_addf(&local, \"%%(if)%%(upstream)%%(then)[%s%%(upstream:short)%s%%(if)%%(upstream:track)\"\n \t\t\t\t    \"%%(then): %%(upstream:track,nobracket)%%(end)] %%(end)%%(contents:subject)\",\n \t\t\t\t    branch_get_color(BRANCH_COLOR_UPSTREAM), branch_get_color(BRANCH_COLOR_RESET));\n@@ -477,6 +490,32 @@ static char *build_format(struct ref_filter *filter, int maxwidth, const char *r\n \treturn strbuf_detach(&fmt, NULL);\n }\n \n+static char *current_worktree_path(void)\n+{\n+\tstruct worktree **worktrees = get_worktrees(the_repository);\n+\tchar *path = NULL;\n+\n+\tfor (size_t i = 0; worktrees[i]; i++) {\n+\t\tif (worktrees[i]->is_current) {\n+\t\t\tpath = xstrdup(worktrees[i]->path);\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\tfree_worktrees(worktrees);\n+\treturn path;\n+}\n+\n+/*\n+ * Is \"refname\" checked out in the worktree at \"here\", or is that\n+ * worktree rebasing or bisecting it?\n+ */\n+static int checked_out_here(const char *refname, const char *here)\n+{\n+\tconst char *path = branch_checked_out_worktree(refname);\n+\n+\treturn path && !strcmp(path, here);\n+}\n+\n static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sorting,\n \t\t\t   struct ref_format *format, struct string_list *output)\n {\n@@ -485,6 +524,9 @@ static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sortin\n \tint maxwidth = 0;\n \tconst char *remote_prefix = \"\";\n \tchar *to_free = NULL;\n+\tstruct ref_format here_format = REF_FORMAT_INIT;\n+\tchar *here_to_free = NULL, *here = NULL;\n+\tstruct strbuf out = STRBUF_INIT, err = STRBUF_INIT;\n \n \t/*\n \t * If we are listing more than just remote branches,\n@@ -501,39 +543,54 @@ static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sortin\n \tif (filter->verbose)\n \t\tmaxwidth = calc_maxwidth(&array, strlen(remote_prefix));\n \n-\tif (!format->format)\n-\t\tformat->format = to_free = build_format(filter, maxwidth, remote_prefix);\n+\tif (!format->format) {\n+\t\tformat->format = to_free = build_format(filter, maxwidth,\n+\t\t\t\t\t\t\tremote_prefix, 1);\n+\t\there = current_worktree_path();\n+\t\there_format = *format;\n+\t\there_format.format = here_to_free = build_format(filter, maxwidth,\n+\t\t\t\t\t\t\t\t remote_prefix, 0);\n+\t}\n \tformat->use_color = branch_use_color;\n+\there_format.use_color = branch_use_color;\n \n-\tif (verify_ref_format(format))\n+\tif (verify_ref_format(format) ||\n+\t    (here && verify_ref_format(&here_format)))\n \t\tdie(_(\"unable to parse format string\"));\n \n \tfilter_ahead_behind(the_repository, &array);\n \tref_array_sort(sorting, &array);\n \n-\tif (column_active(colopts)) {\n-\t\tstruct strbuf out = STRBUF_INIT, err = STRBUF_INIT;\n-\n+\tif (column_active(colopts))\n \t\tassert(!filter->verbose && \"--column and --verbose are incompatible\");\n \n-\t\tfor (i = 0; i < array.nr; i++) {\n-\t\t\tstrbuf_reset(&err);\n-\t\t\tstrbuf_reset(&out);\n-\t\t\tif (format_ref_array_item(array.items[i], format, &out, &err))\n-\t\t\t\tdie(\"%s\", err.buf);\n+\tfor (i = 0; i < array.nr; i++) {\n+\t\tstruct ref_array_item *item = array.items[i];\n+\t\tstruct ref_format *f = format;\n \n+\t\tif (here && checked_out_here(item->refname, here))\n+\t\t\tf = &here_format;\n+\n+\t\tstrbuf_reset(&err);\n+\t\tstrbuf_reset(&out);\n+\t\tif (format_ref_array_item(item, f, &out, &err))\n+\t\t\tdie(\"%s\", err.buf);\n+\n+\t\tif (column_active(colopts))\n \t\t\t/* format to a string_list to let print_columns() do its job */\n \t\t\tstring_list_append(output, out.buf);\n+\t\telse if (out.len || !format->array_opts.omit_empty) {\n+\t\t\tfwrite(out.buf, 1, out.len, stdout);\n+\t\t\tputchar('\\n');\n \t\t}\n-\n-\t\tstrbuf_release(&err);\n-\t\tstrbuf_release(&out);\n-\t} else {\n-\t\tprint_formatted_ref_array(&array, format);\n \t}\n \n+\tstrbuf_release(&err);\n+\tstrbuf_release(&out);\n \tref_array_clear(&array);\n \tfree(to_free);\n+\tfree(here_to_free);\n+\tfree(here);\n }\n \n static void print_current_branch_name(void)\ndiff --git a/t/t3203-branch-output.sh b/t/t3203-branch-output.sh\nindex debfac5f26..5541f1429b 100755\n--- a/t/t3203-branch-output.sh\n+++ b/t/t3203-branch-output.sh\n@@ -456,6 +456,23 @@ test_expect_success 'branch being rebased in a worktree is marked' '\n \ttest_cmp expect actual\n '\n \n+test_expect_success 'branch being rebased is not marked in its own worktree' '\n+\ttest_when_finished \"git -C rebase_dir rebase --abort; rm -rf rebase_dir; git worktree prune; git branch -D rebasing\" &&\n+\tgit worktree add -b rebasing rebase_dir branch-two &&\n+\t(\n+\t\tcd rebase_dir &&\n+\t\tgit commit --allow-empty -m rebase-me &&\n+\t\ttest_must_fail git rebase -f -x false HEAD~1 &&\n+\t\tgit branch --list rebasing >actual &&\n+\t\techo \"  rebasing\" >expect &&\n+\t\ttest_cmp expect actual &&\n+\t\tgit branch -vv --list rebasing >actual &&\n+\t\ttest_grep ! \"$(pwd)\" actual &&\n+\t\tgit branch >actual &&\n+\t\ttest_grep \"^\\* (no branch, rebasing rebasing)\" actual\n+\t)\n+'\n+\n test_expect_success 'branch being bisected in a worktree is marked' '\n \ttest_when_finished \"git -C bisect_dir bisect reset; rm -rf bisect_dir; git worktree prune; git branch -D bisecting\" &&\n \tgit worktree add -b bisecting bisect_dir branch-two &&\n@@ -464,7 +481,10 @@ test_expect_success 'branch being bisected in a worktree is marked' '\n \t\tgit commit --allow-empty -m bisect-one &&\n \t\tgit commit --allow-empty -m bisect-two &&\n \t\tgit bisect start HEAD HEAD~2 &&\n-\t\ttest_must_fail git symbolic-ref -q HEAD\n+\t\ttest_must_fail git symbolic-ref -q HEAD &&\n+\t\tgit branch --list bisecting >actual &&\n+\t\techo \"  bisecting\" >expect &&\n+\t\ttest_cmp expect actual\n \t) &&\n \tgit branch --list bisecting >actual &&\n \techo \"+ bisecting\" >expect &&\n-- \n2.56.0\n\n\n"}]}