* [PATCH v4 1/4] worktree remove: refuse a worktree in the middle of an operation
2026-11-02 18:29 ` [PATCH v4 0/4] " Jasper Mayone
@ 2026-11-02 18:29 ` Jasper Mayone
2026-11-10 16:47 ` Phillip Wood
2026-11-02 18:29 ` [PATCH v4 2/4] doc: for-each-ref: say how to read %(worktreepath) safely Jasper Mayone
` (4 subsequent siblings)
5 siblings, 1 reply; 50+ messages in thread
From: Jasper Mayone @ 2026-11-02 18:29 UTC (permalink / raw)
To: git
Cc: gitster, phillip.wood123, sunshine, kristofferhaugsbakk,
ben.knoble, haraldnordgren, Jasper Mayone
"git worktree remove" refuses to remove a worktree with modified or
untracked files unless --force is given, but it removes a worktree
that is in the middle of a rebase, a "git am" session, a bisect, a
cherry-pick or a revert when its files happen to be clean. The state
of that operation lives in the worktree's own $GIT_DIR, so it is lost
with the worktree. Examples of such a clean worktree are a rebase
stopped at an "edit" or "break" line or after a failed "exec", a
cherry-pick sequence after a conflict was resolved and committed, and
a single cherry-pick or revert whose conflict was resolved to the
current contents, which leaves CHERRY_PICK_HEAD or REVERT_HEAD behind.
Treat such a worktree as unclean: refuse to remove it unless --force
is given, and name the operation in the message.
A worktree whose directory is already gone is still removed without
--force, as before: the check is part of the check for a clean
worktree, which is only done when the directory exists.
Reported-by: D. Ben Knoble <ben.knoble@gmail.com>
Signed-off-by: Jasper Mayone <me@jaspermayone.com>
---
Documentation/git-worktree.adoc | 17 ++++--
builtin/worktree.c | 37 ++++++++++++-
t/t2403-worktree-move.sh | 94 +++++++++++++++++++++++++++++++++
3 files changed, 142 insertions(+), 6 deletions(-)
diff --git a/Documentation/git-worktree.adoc b/Documentation/git-worktree.adoc
index e6e77252ab..4a7dc47dd8 100644
--- a/Documentation/git-worktree.adoc
+++ b/Documentation/git-worktree.adoc
@@ -141,10 +141,16 @@ the new working tree location.
`remove`::
-Remove a worktree. Only clean worktrees (no untracked files and no
-modification in tracked files) can be removed. Unclean worktrees or ones
-with submodules can be removed with `--force`. The main worktree cannot be
-removed.
+Remove a worktree. The main worktree cannot be removed. Without
+`--force`, a worktree is not removed when:
++
+--
+* it has untracked files or modifications in tracked files,
+* it has submodules, or
+* it is in the middle of a rebase, a `git am` session, a bisect, a
+ cherry-pick or a revert, because removing it loses the state of that
+ operation.
+--
`repair [<path>...]`::
@@ -189,7 +195,8 @@ twice. If the destination is already assigned to some other worktree but is
missing (for instance, if _<new-path>_ was deleted manually), then `--force`
allows the move to proceed; use `--force` twice if the destination is locked.
+
-`remove` refuses to remove an unclean worktree unless `--force` is used.
+`remove` refuses to remove a worktree in the cases listed under
+`remove` above unless `--force` is used.
To remove a locked worktree, specify `--force` twice.
`-b <new-branch>`::
diff --git a/builtin/worktree.c b/builtin/worktree.c
index 77ecd0f71f..7f53215cda 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -26,6 +26,7 @@
#include "submodule.h"
#include "utf8.h"
#include "worktree.h"
+#include "wt-status.h"
#include "quote.h"
#define BUILTIN_WORKTREE_ADD_USAGE \
@@ -1408,6 +1409,38 @@ static void check_clean_worktree(struct worktree *wt,
original_path, ret);
}
+/*
+ * A rebase, "git am", bisect, cherry-pick or revert keeps its state in
+ * the worktree's own $GIT_DIR, and removing the worktree throws that
+ * away even when "git status" says the files are clean.
+ */
+static void check_no_operation_in_progress(struct worktree *wt,
+ const char *original_path)
+{
+ struct wt_status_state state = { 0 };
+
+ if (wt_status_check_rebase(wt, &state)) {
+ if (state.am_in_progress)
+ die(_("'%s' is in the middle of an am session, use --force to delete it"),
+ original_path);
+ die(_("'%s' is in the middle of a rebase, use --force to delete it"),
+ original_path);
+ }
+ if (wt_status_check_bisect(wt, &state))
+ die(_("'%s' is in the middle of a bisect, use --force to delete it"),
+ original_path);
+ wt_status_state_free_buffers(&state);
+ if (refs_ref_exists(get_worktree_ref_store(wt), "CHERRY_PICK_HEAD"))
+ die(_("'%s' is in the middle of a cherry-pick, use --force to delete it"),
+ original_path);
+ if (refs_ref_exists(get_worktree_ref_store(wt), "REVERT_HEAD"))
+ die(_("'%s' is in the middle of a revert, use --force to delete it"),
+ original_path);
+ if (is_directory(worktree_git_path(wt, "sequencer")))
+ die(_("'%s' is in the middle of a cherry-pick or revert, use --force to delete it"),
+ original_path);
+}
+
static int delete_git_work_tree(struct worktree *wt)
{
struct strbuf sb = STRBUF_INIT;
@@ -1461,8 +1494,10 @@ static int remove_worktree(int ac, const char **av, const char *prefix,
strbuf_release(&errmsg);
if (file_exists(wt->path)) {
- if (!force)
+ if (!force) {
+ check_no_operation_in_progress(wt, av[0]);
check_clean_worktree(wt, av[0]);
+ }
ret |= delete_git_work_tree(wt);
}
diff --git a/t/t2403-worktree-move.sh b/t/t2403-worktree-move.sh
index 69768c1207..2e45cd77f9 100755
--- a/t/t2403-worktree-move.sh
+++ b/t/t2403-worktree-move.sh
@@ -271,4 +271,98 @@ test_expect_success 'move worktree with relative path to absolute path' '
test_cmp expect .git/worktrees/absolute/gitdir
'
+test_expect_success 'setup commits that conflict for am, cherry-pick and revert' '
+ git worktree add --detach conflict-setup init &&
+ (
+ cd conflict-setup &&
+ test_commit base conflict.t base &&
+ git switch --detach init &&
+ test_commit side conflict.t side &&
+ test_commit side-two
+ ) &&
+ git worktree remove conflict-setup
+'
+
+test_expect_success 'remove worktree in the middle of a rebase' '
+ test_when_finished "rm -rf mid-rebase; git worktree prune" &&
+ git worktree add --detach mid-rebase side &&
+ test_must_fail git -C mid-rebase rebase -f -x false init &&
+ test_must_fail git worktree remove mid-rebase 2>err &&
+ test_grep "in the middle of a rebase" err &&
+ test_path_is_dir mid-rebase &&
+ git worktree remove --force mid-rebase &&
+ test_path_is_missing mid-rebase
+'
+
+test_expect_success 'remove worktree in the middle of an am session' '
+ test_when_finished "rm -rf mid-am patch; git worktree prune" &&
+ git format-patch -1 --stdout side >patch &&
+ git worktree add --detach mid-am base &&
+ (
+ cd mid-am &&
+ test_must_fail git am ../patch &&
+ git reset --hard
+ ) &&
+ test_must_fail git worktree remove mid-am 2>err &&
+ test_grep "in the middle of an am session" err &&
+ git worktree remove --force mid-am
+'
+
+test_expect_success 'remove worktree in the middle of a bisect' '
+ test_when_finished "rm -rf mid-bisect; git worktree prune" &&
+ git worktree add --detach mid-bisect side-two &&
+ git -C mid-bisect bisect start side-two init &&
+ test_must_fail git worktree remove mid-bisect 2>err &&
+ test_grep "in the middle of a bisect" err &&
+ git worktree remove --force mid-bisect
+'
+
+test_expect_success 'remove worktree in the middle of a cherry-pick' '
+ test_when_finished "rm -rf mid-pick; git worktree prune" &&
+ git worktree add --detach mid-pick base &&
+ (
+ cd mid-pick &&
+ test_must_fail git cherry-pick side &&
+ git checkout --ours conflict.t &&
+ git add conflict.t &&
+ git rev-parse --verify CHERRY_PICK_HEAD
+ ) &&
+ test_path_is_missing .git/worktrees/mid-pick/sequencer &&
+ test_must_fail git worktree remove mid-pick 2>err &&
+ test_grep "in the middle of a cherry-pick," err &&
+ git worktree remove --force mid-pick
+'
+
+test_expect_success 'remove worktree in the middle of a revert' '
+ test_when_finished "rm -rf mid-revert; git worktree prune" &&
+ git worktree add --detach mid-revert base &&
+ (
+ cd mid-revert &&
+ test_must_fail git revert side &&
+ git checkout --ours conflict.t &&
+ git add conflict.t &&
+ git rev-parse --verify REVERT_HEAD
+ ) &&
+ test_path_is_missing .git/worktrees/mid-revert/sequencer &&
+ test_must_fail git worktree remove mid-revert 2>err &&
+ test_grep "in the middle of a revert" err &&
+ git worktree remove --force mid-revert
+'
+
+test_expect_success 'remove worktree in the middle of a cherry-pick sequence' '
+ test_when_finished "rm -rf mid-pick-seq; git worktree prune" &&
+ git worktree add --detach mid-pick-seq base &&
+ (
+ cd mid-pick-seq &&
+ test_must_fail git cherry-pick side side-two &&
+ git checkout --theirs conflict.t &&
+ git add conflict.t &&
+ git commit --no-edit
+ ) &&
+ test_path_is_dir .git/worktrees/mid-pick-seq/sequencer &&
+ test_must_fail git worktree remove mid-pick-seq 2>err &&
+ test_grep "in the middle of a cherry-pick or revert" err &&
+ git worktree remove --force mid-pick-seq
+'
+
test_done
--
2.56.0
^ permalink raw reply related [flat|nested] 50+ messages in thread* Re: [PATCH v4 1/4] worktree remove: refuse a worktree in the middle of an operation
2026-11-02 18:29 ` [PATCH v4 1/4] worktree remove: refuse a worktree in the middle of an operation Jasper Mayone
@ 2026-11-10 16:47 ` Phillip Wood
2026-11-10 17:09 ` Ben Knoble
0 siblings, 1 reply; 50+ messages in thread
From: Phillip Wood @ 2026-11-10 16:47 UTC (permalink / raw)
To: Jasper Mayone, git
Cc: gitster, sunshine, kristofferhaugsbakk, ben.knoble,
haraldnordgren
Hi Jasper
On 02/11/2026 18:29, Jasper Mayone wrote:
>
> diff --git a/Documentation/git-worktree.adoc b/Documentation/git-worktree.adoc
> index e6e77252ab..4a7dc47dd8 100644
> --- a/Documentation/git-worktree.adoc
> +++ b/Documentation/git-worktree.adoc
> @@ -141,10 +141,16 @@ the new working tree location.
>
> `remove`::
>
> -Remove a worktree. Only clean worktrees (no untracked files and no
> -modification in tracked files) can be removed. Unclean worktrees or ones
> -with submodules can be removed with `--force`. The main worktree cannot be
> -removed.
> +Remove a worktree. The main worktree cannot be removed. Without
> +`--force`, a worktree is not removed when:
> ++
> +--
> +* it has untracked files or modifications in tracked files,
> +* it has submodules, or
This is a list so each item should be a self contained sentence, we
should drop the comma above and ", or" here.
> +* it is in the middle of a rebase, a `git am` session, a bisect, a
> + cherry-pick or a revert, because removing it loses the state of that
> + operation.
Looking at the html rendering of this, it is slightly odd that "git am"
is in a different font to the rest of the command names. As we're
creating a list we could have a separate item for each command, I'm not
sure whether that would be clearer or not.
The rest of this patch looks good
Thanks
Phillip
> +--
>
> `repair [<path>...]`::
>
> @@ -189,7 +195,8 @@ twice. If the destination is already assigned to some other worktree but is
> missing (for instance, if _<new-path>_ was deleted manually), then `--force`
> allows the move to proceed; use `--force` twice if the destination is locked.
> +
> -`remove` refuses to remove an unclean worktree unless `--force` is used.
> +`remove` refuses to remove a worktree in the cases listed under
> +`remove` above unless `--force` is used.
> To remove a locked worktree, specify `--force` twice.
>
> `-b <new-branch>`::
> diff --git a/builtin/worktree.c b/builtin/worktree.c
> index 77ecd0f71f..7f53215cda 100644
> --- a/builtin/worktree.c
> +++ b/builtin/worktree.c
> @@ -26,6 +26,7 @@
> #include "submodule.h"
> #include "utf8.h"
> #include "worktree.h"
> +#include "wt-status.h"
> #include "quote.h"
>
> #define BUILTIN_WORKTREE_ADD_USAGE \
> @@ -1408,6 +1409,38 @@ static void check_clean_worktree(struct worktree *wt,
> original_path, ret);
> }
>
> +/*
> + * A rebase, "git am", bisect, cherry-pick or revert keeps its state in
> + * the worktree's own $GIT_DIR, and removing the worktree throws that
> + * away even when "git status" says the files are clean.
> + */
> +static void check_no_operation_in_progress(struct worktree *wt,
> + const char *original_path)
> +{
> + struct wt_status_state state = { 0 };
> +
> + if (wt_status_check_rebase(wt, &state)) {
> + if (state.am_in_progress)
> + die(_("'%s' is in the middle of an am session, use --force to delete it"),
> + original_path);
> + die(_("'%s' is in the middle of a rebase, use --force to delete it"),
> + original_path);
> + }
> + if (wt_status_check_bisect(wt, &state))
> + die(_("'%s' is in the middle of a bisect, use --force to delete it"),
> + original_path);
> + wt_status_state_free_buffers(&state);
> + if (refs_ref_exists(get_worktree_ref_store(wt), "CHERRY_PICK_HEAD"))
> + die(_("'%s' is in the middle of a cherry-pick, use --force to delete it"),
> + original_path);
> + if (refs_ref_exists(get_worktree_ref_store(wt), "REVERT_HEAD"))
> + die(_("'%s' is in the middle of a revert, use --force to delete it"),
> + original_path);
> + if (is_directory(worktree_git_path(wt, "sequencer")))
> + die(_("'%s' is in the middle of a cherry-pick or revert, use --force to delete it"),
> + original_path);
> +}
> +
> static int delete_git_work_tree(struct worktree *wt)
> {
> struct strbuf sb = STRBUF_INIT;
> @@ -1461,8 +1494,10 @@ static int remove_worktree(int ac, const char **av, const char *prefix,
> strbuf_release(&errmsg);
>
> if (file_exists(wt->path)) {
> - if (!force)
> + if (!force) {
> + check_no_operation_in_progress(wt, av[0]);
> check_clean_worktree(wt, av[0]);
> + }
>
> ret |= delete_git_work_tree(wt);
> }
> diff --git a/t/t2403-worktree-move.sh b/t/t2403-worktree-move.sh
> index 69768c1207..2e45cd77f9 100755
> --- a/t/t2403-worktree-move.sh
> +++ b/t/t2403-worktree-move.sh
> @@ -271,4 +271,98 @@ test_expect_success 'move worktree with relative path to absolute path' '
> test_cmp expect .git/worktrees/absolute/gitdir
> '
>
> +test_expect_success 'setup commits that conflict for am, cherry-pick and revert' '
> + git worktree add --detach conflict-setup init &&
> + (
> + cd conflict-setup &&
> + test_commit base conflict.t base &&
> + git switch --detach init &&
> + test_commit side conflict.t side &&
> + test_commit side-two
> + ) &&
> + git worktree remove conflict-setup
> +'
> +
> +test_expect_success 'remove worktree in the middle of a rebase' '
> + test_when_finished "rm -rf mid-rebase; git worktree prune" &&
> + git worktree add --detach mid-rebase side &&
> + test_must_fail git -C mid-rebase rebase -f -x false init &&
> + test_must_fail git worktree remove mid-rebase 2>err &&
> + test_grep "in the middle of a rebase" err &&
> + test_path_is_dir mid-rebase &&
> + git worktree remove --force mid-rebase &&
> + test_path_is_missing mid-rebase
> +'
> +
> +test_expect_success 'remove worktree in the middle of an am session' '
> + test_when_finished "rm -rf mid-am patch; git worktree prune" &&
> + git format-patch -1 --stdout side >patch &&
> + git worktree add --detach mid-am base &&
> + (
> + cd mid-am &&
> + test_must_fail git am ../patch &&
> + git reset --hard
> + ) &&
> + test_must_fail git worktree remove mid-am 2>err &&
> + test_grep "in the middle of an am session" err &&
> + git worktree remove --force mid-am
> +'
> +
> +test_expect_success 'remove worktree in the middle of a bisect' '
> + test_when_finished "rm -rf mid-bisect; git worktree prune" &&
> + git worktree add --detach mid-bisect side-two &&
> + git -C mid-bisect bisect start side-two init &&
> + test_must_fail git worktree remove mid-bisect 2>err &&
> + test_grep "in the middle of a bisect" err &&
> + git worktree remove --force mid-bisect
> +'
> +
> +test_expect_success 'remove worktree in the middle of a cherry-pick' '
> + test_when_finished "rm -rf mid-pick; git worktree prune" &&
> + git worktree add --detach mid-pick base &&
> + (
> + cd mid-pick &&
> + test_must_fail git cherry-pick side &&
> + git checkout --ours conflict.t &&
> + git add conflict.t &&
> + git rev-parse --verify CHERRY_PICK_HEAD
> + ) &&
> + test_path_is_missing .git/worktrees/mid-pick/sequencer &&
> + test_must_fail git worktree remove mid-pick 2>err &&
> + test_grep "in the middle of a cherry-pick," err &&
> + git worktree remove --force mid-pick
> +'
> +
> +test_expect_success 'remove worktree in the middle of a revert' '
> + test_when_finished "rm -rf mid-revert; git worktree prune" &&
> + git worktree add --detach mid-revert base &&
> + (
> + cd mid-revert &&
> + test_must_fail git revert side &&
> + git checkout --ours conflict.t &&
> + git add conflict.t &&
> + git rev-parse --verify REVERT_HEAD
> + ) &&
> + test_path_is_missing .git/worktrees/mid-revert/sequencer &&
> + test_must_fail git worktree remove mid-revert 2>err &&
> + test_grep "in the middle of a revert" err &&
> + git worktree remove --force mid-revert
> +'
> +
> +test_expect_success 'remove worktree in the middle of a cherry-pick sequence' '
> + test_when_finished "rm -rf mid-pick-seq; git worktree prune" &&
> + git worktree add --detach mid-pick-seq base &&
> + (
> + cd mid-pick-seq &&
> + test_must_fail git cherry-pick side side-two &&
> + git checkout --theirs conflict.t &&
> + git add conflict.t &&
> + git commit --no-edit
> + ) &&
> + test_path_is_dir .git/worktrees/mid-pick-seq/sequencer &&
> + test_must_fail git worktree remove mid-pick-seq 2>err &&
> + test_grep "in the middle of a cherry-pick or revert" err &&
> + git worktree remove --force mid-pick-seq
> +'
> +
> test_done
^ permalink raw reply [flat|nested] 50+ messages in thread* Re: [PATCH v4 1/4] worktree remove: refuse a worktree in the middle of an operation
2026-11-10 16:47 ` Phillip Wood
@ 2026-11-10 17:09 ` Ben Knoble
0 siblings, 0 replies; 50+ messages in thread
From: Ben Knoble @ 2026-11-10 17:09 UTC (permalink / raw)
To: phillip.wood
Cc: Jasper Mayone, git, gitster, sunshine, kristofferhaugsbakk,
haraldnordgren
>
> Le 10 nov. 2026 à 11:47, Phillip Wood <phillip.wood123@gmail.com> a écrit :
>
> Hi Jasper
>
>> On 02/11/2026 18:29, Jasper Mayone wrote:
>> diff --git a/Documentation/git-worktree.adoc b/Documentation/git-worktree.adoc
>> index e6e77252ab..4a7dc47dd8 100644
>> --- a/Documentation/git-worktree.adoc
>> +++ b/Documentation/git-worktree.adoc
>> @@ -141,10 +141,16 @@ the new working tree location.
>> `remove`::
>> -Remove a worktree. Only clean worktrees (no untracked files and no
>> -modification in tracked files) can be removed. Unclean worktrees or ones
>> -with submodules can be removed with `--force`. The main worktree cannot be
>> -removed.
>> +Remove a worktree. The main worktree cannot be removed. Without
>> +`--force`, a worktree is not removed when:
>> ++
>> +--
>> +* it has untracked files or modifications in tracked files,
>> +* it has submodules, or
>
> This is a list so each item should be a self contained sentence, we should drop the comma above and ", or" here.
>
>> +* it is in the middle of a rebase, a `git am` session, a bisect, a
>> + cherry-pick or a revert, because removing it loses the state of that
>> + operation.
Well, each item is a clause/phrase… I actually think the “or” fits better than
rewriting the list introduction and items to be complete sentences.
I think that construction is ok as-is.
^ permalink raw reply [flat|nested] 50+ messages in thread
* [PATCH v4 2/4] doc: for-each-ref: say how to read %(worktreepath) safely
2026-11-02 18:29 ` [PATCH v4 0/4] " Jasper Mayone
2026-11-02 18:29 ` [PATCH v4 1/4] worktree remove: refuse a worktree in the middle of an operation Jasper Mayone
@ 2026-11-02 18:29 ` Jasper Mayone
2026-11-09 12:36 ` D. Ben Knoble
2026-11-10 16:47 ` Phillip Wood
2026-11-02 18:29 ` [PATCH v4 3/4] ref-filter: name the worktree of a branch being rebased or bisected Jasper Mayone
` (3 subsequent siblings)
5 siblings, 2 replies; 50+ messages in thread
From: Jasper Mayone @ 2026-11-02 18:29 UTC (permalink / raw)
To: git
Cc: gitster, phillip.wood123, sunshine, kristofferhaugsbakk,
ben.knoble, haraldnordgren, Jasper Mayone
The path that %(worktreepath) gives can contain a newline, which
breaks a script that reads one record per line. Say how to avoid
that: use a quoting option, or end the atom with %00 and look for the
NUL before the LF that ends the record.
Also say that only one worktree is shown when a branch is checked out
in more than one worktree.
Suggested-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Jasper Mayone <me@jaspermayone.com>
---
Documentation/git-for-each-ref.adoc | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/Documentation/git-for-each-ref.adoc b/Documentation/git-for-each-ref.adoc
index a7b31e8aaf..9247737aa7 100644
--- a/Documentation/git-for-each-ref.adoc
+++ b/Documentation/git-for-each-ref.adoc
@@ -183,7 +183,12 @@ but if used together the last option is selected.
`worktreepath`::
The absolute path to the worktree in which the ref is checked
out, if it is checked out in any linked worktree. Empty string
- otherwise.
+ otherwise. If the ref is checked out in more than one worktree,
+ only one of them is shown. The path can contain a newline, so
+ use one of the quoting options such as `--shell`, or follow
+ `%(worktreepath)` with `%00` and parse the output by looking for
+ the `NUL` after the path and then for the `LF` that ends each
+ record.
`ahead-behind:<commit-ish>`::
Two integers, separated by a space, demonstrating the number of
--
2.56.0
^ permalink raw reply related [flat|nested] 50+ messages in thread* Re: [PATCH v4 2/4] doc: for-each-ref: say how to read %(worktreepath) safely
2026-11-02 18:29 ` [PATCH v4 2/4] doc: for-each-ref: say how to read %(worktreepath) safely Jasper Mayone
@ 2026-11-09 12:36 ` D. Ben Knoble
2026-11-09 15:10 ` Phillip Wood
2026-11-10 16:47 ` Phillip Wood
1 sibling, 1 reply; 50+ messages in thread
From: D. Ben Knoble @ 2026-11-09 12:36 UTC (permalink / raw)
To: Jasper Mayone
Cc: git, gitster, phillip.wood123, sunshine, kristofferhaugsbakk,
haraldnordgren
On Mon, Nov 2, 2026 at 1:29 PM Jasper Mayone <me@jaspermayone.com> wrote:
>
> The path that %(worktreepath) gives can contain a newline, which
> breaks a script that reads one record per line. Say how to avoid
> that: use a quoting option, or end the atom with %00 and look for the
> NUL before the LF that ends the record.
>
> Also say that only one worktree is shown when a branch is checked out
> in more than one worktree.
I was under the impression a branch could not be checked out in
multiple worktrees due to possible races from each worktree performing
updates; or at least, that the porcelain would refuse to create such
worktrees.
Has that limitation been lifted recently?
> Suggested-by: Phillip Wood <phillip.wood123@gmail.com>
> Signed-off-by: Jasper Mayone <me@jaspermayone.com>
> ---
> Documentation/git-for-each-ref.adoc | 7 ++++++-
> 1 file changed, 6 insertions(+), 1 deletion(-)
>
> diff --git a/Documentation/git-for-each-ref.adoc b/Documentation/git-for-each-ref.adoc
> index a7b31e8aaf..9247737aa7 100644
> --- a/Documentation/git-for-each-ref.adoc
> +++ b/Documentation/git-for-each-ref.adoc
> @@ -183,7 +183,12 @@ but if used together the last option is selected.
> `worktreepath`::
> The absolute path to the worktree in which the ref is checked
> out, if it is checked out in any linked worktree. Empty string
> - otherwise.
> + otherwise. If the ref is checked out in more than one worktree,
> + only one of them is shown. The path can contain a newline, so
> + use one of the quoting options such as `--shell`, or follow
> + `%(worktreepath)` with `%00` and parse the output by looking for
> + the `NUL` after the path and then for the `LF` that ends each
> + record.
>
> `ahead-behind:<commit-ish>`::
> Two integers, separated by a space, demonstrating the number of
> --
> 2.56.0
>
--
D. Ben Knoble
^ permalink raw reply [flat|nested] 50+ messages in thread
* Re: [PATCH v4 2/4] doc: for-each-ref: say how to read %(worktreepath) safely
2026-11-09 12:36 ` D. Ben Knoble
@ 2026-11-09 15:10 ` Phillip Wood
2026-11-09 17:27 ` Ben Knoble
0 siblings, 1 reply; 50+ messages in thread
From: Phillip Wood @ 2026-11-09 15:10 UTC (permalink / raw)
To: D. Ben Knoble, Jasper Mayone
Cc: git, gitster, sunshine, kristofferhaugsbakk, haraldnordgren
Hi Ben
On 09/11/2026 12:36, D. Ben Knoble wrote:
> On Mon, Nov 2, 2026 at 1:29 PM Jasper Mayone <me@jaspermayone.com> wrote:
>>
>> The path that %(worktreepath) gives can contain a newline, which
>> breaks a script that reads one record per line. Say how to avoid
>> that: use a quoting option, or end the atom with %00 and look for the
>> NUL before the LF that ends the record.
>>
>> Also say that only one worktree is shown when a branch is checked out
>> in more than one worktree.
>
> I was under the impression a branch could not be checked out in
> multiple worktrees due to possible races from each worktree performing
> updates; or at least, that the porcelain would refuse to create such
> worktrees.
>
> Has that limitation been lifted recently?
No, it is not encouraged, but it is possible to checkout the same branch
in more than one worktree with "git worktree add --force", or "git
checkout --ignore-other-worktrees". Those options have existed for some
time. It's also possible to bypass the checks by using "git symbolic-ref".
Thanks
Phillip
>> Suggested-by: Phillip Wood <phillip.wood123@gmail.com>
>> Signed-off-by: Jasper Mayone <me@jaspermayone.com>
>> ---
>> Documentation/git-for-each-ref.adoc | 7 ++++++-
>> 1 file changed, 6 insertions(+), 1 deletion(-)
>>
>> diff --git a/Documentation/git-for-each-ref.adoc b/Documentation/git-for-each-ref.adoc
>> index a7b31e8aaf..9247737aa7 100644
>> --- a/Documentation/git-for-each-ref.adoc
>> +++ b/Documentation/git-for-each-ref.adoc
>> @@ -183,7 +183,12 @@ but if used together the last option is selected.
>> `worktreepath`::
>> The absolute path to the worktree in which the ref is checked
>> out, if it is checked out in any linked worktree. Empty string
>> - otherwise.
>> + otherwise. If the ref is checked out in more than one worktree,
>> + only one of them is shown. The path can contain a newline, so
>> + use one of the quoting options such as `--shell`, or follow
>> + `%(worktreepath)` with `%00` and parse the output by looking for
>> + the `NUL` after the path and then for the `LF` that ends each
>> + record.
>>
>> `ahead-behind:<commit-ish>`::
>> Two integers, separated by a space, demonstrating the number of
>> --
>> 2.56.0
>>
>
>
^ permalink raw reply [flat|nested] 50+ messages in thread
* Re: [PATCH v4 2/4] doc: for-each-ref: say how to read %(worktreepath) safely
2026-11-09 15:10 ` Phillip Wood
@ 2026-11-09 17:27 ` Ben Knoble
2026-11-09 20:17 ` Junio C Hamano
0 siblings, 1 reply; 50+ messages in thread
From: Ben Knoble @ 2026-11-09 17:27 UTC (permalink / raw)
To: phillip.wood
Cc: Jasper Mayone, git, gitster, sunshine, kristofferhaugsbakk,
haraldnordgren
> Le 9 nov. 2026 à 10:10, Phillip Wood <phillip.wood123@gmail.com> a écrit :
>
> Hi Ben
>
>> On 09/11/2026 12:36, D. Ben Knoble wrote:
>>> On Mon, Nov 2, 2026 at 1:29 PM Jasper Mayone <me@jaspermayone.com> wrote:
>>>
>>> The path that %(worktreepath) gives can contain a newline, which
>>> breaks a script that reads one record per line. Say how to avoid
>>> that: use a quoting option, or end the atom with %00 and look for the
>>> NUL before the LF that ends the record.
>>>
>>> Also say that only one worktree is shown when a branch is checked out
>>> in more than one worktree.
>> I was under the impression a branch could not be checked out in
>> multiple worktrees due to possible races from each worktree performing
>> updates; or at least, that the porcelain would refuse to create such
>> worktrees.
>> Has that limitation been lifted recently?
>
> No, it is not encouraged, but it is possible to checkout the same branch in more than one worktree with "git worktree add --force", or "git checkout --ignore-other-worktrees". Those options have existed for some time. It's also possible to bypass the checks by using "git symbolic-ref".
>
> Thanks
>
> Phillip
>
Faulty memory, then, thanks.
^ permalink raw reply [flat|nested] 50+ messages in thread
* Re: [PATCH v4 2/4] doc: for-each-ref: say how to read %(worktreepath) safely
2026-11-09 17:27 ` Ben Knoble
@ 2026-11-09 20:17 ` Junio C Hamano
0 siblings, 0 replies; 50+ messages in thread
From: Junio C Hamano @ 2026-11-09 20:17 UTC (permalink / raw)
To: Ben Knoble
Cc: phillip.wood, Jasper Mayone, git, sunshine, kristofferhaugsbakk,
haraldnordgren
Ben Knoble <ben.knoble@gmail.com> writes:
>>> I was under the impression a branch could not be checked out in
>>> multiple worktrees due to possible races from each worktree performing
>>> updates; or at least, that the porcelain would refuse to create such
>>> worktrees.
Phillip's answer covers everything, but I am not sure if "possible
races" is a good explanation to describe the reason this is
strongly discouraged.
You checkout 'main' in worktree A and worktree B, so their working
tree files and their index files all match what is recorded at the
commit pointed at by 'main'.
You then make changes and commit your changes in worktree A. The
'main' branch now points at a newly created commit, and the working
tree files and the index in worktree A are in sync with the new
commit (note: unlike the initial state, I am not saying "all match"
in this case, as this commit may have been a partial one. The
important thing is that from the end-user's perspective who has been
working in worktree A, things are consistent without any unexpected
change).
In the meantime, if worktree B weren't doing anything, its index and
the working tree files have not changed since the beginning. Yet
because 'main' (hence HEAD) moved, "git status" in worktree B
reports as if it reverted the change made in the updated 'main'.
That would be a grave confusion, and that is what we want to avoid
by forbidding a single branch from being checked out in multiple
worktrees. But given that worktree B was doing absolutely nothing
while worktree A was doing its own thing, I would not call this a
"race". It is something else.
Thanks.
^ permalink raw reply [flat|nested] 50+ messages in thread
* Re: [PATCH v4 2/4] doc: for-each-ref: say how to read %(worktreepath) safely
2026-11-02 18:29 ` [PATCH v4 2/4] doc: for-each-ref: say how to read %(worktreepath) safely Jasper Mayone
2026-11-09 12:36 ` D. Ben Knoble
@ 2026-11-10 16:47 ` Phillip Wood
1 sibling, 0 replies; 50+ messages in thread
From: Phillip Wood @ 2026-11-10 16:47 UTC (permalink / raw)
To: Jasper Mayone, git
Cc: gitster, sunshine, kristofferhaugsbakk, ben.knoble,
haraldnordgren
Hi Jasper
Thanks for adding this, it looks good
Phillip
On 02/11/2026 18:29, Jasper Mayone wrote:
> The path that %(worktreepath) gives can contain a newline, which
> breaks a script that reads one record per line. Say how to avoid
> that: use a quoting option, or end the atom with %00 and look for the
> NUL before the LF that ends the record.
>
> Also say that only one worktree is shown when a branch is checked out
> in more than one worktree.
>
> Suggested-by: Phillip Wood <phillip.wood123@gmail.com>
> Signed-off-by: Jasper Mayone <me@jaspermayone.com>
> ---
> Documentation/git-for-each-ref.adoc | 7 ++++++-
> 1 file changed, 6 insertions(+), 1 deletion(-)
>
> diff --git a/Documentation/git-for-each-ref.adoc b/Documentation/git-for-each-ref.adoc
> index a7b31e8aaf..9247737aa7 100644
> --- a/Documentation/git-for-each-ref.adoc
> +++ b/Documentation/git-for-each-ref.adoc
> @@ -183,7 +183,12 @@ but if used together the last option is selected.
> `worktreepath`::
> The absolute path to the worktree in which the ref is checked
> out, if it is checked out in any linked worktree. Empty string
> - otherwise.
> + otherwise. If the ref is checked out in more than one worktree,
> + only one of them is shown. The path can contain a newline, so
> + use one of the quoting options such as `--shell`, or follow
> + `%(worktreepath)` with `%00` and parse the output by looking for
> + the `NUL` after the path and then for the `LF` that ends each
> + record.
>
> `ahead-behind:<commit-ish>`::
> Two integers, separated by a space, demonstrating the number of
^ permalink raw reply [flat|nested] 50+ messages in thread
* [PATCH v4 3/4] ref-filter: name the worktree of a branch being rebased or bisected
2026-11-02 18:29 ` [PATCH v4 0/4] " Jasper Mayone
2026-11-02 18:29 ` [PATCH v4 1/4] worktree remove: refuse a worktree in the middle of an operation Jasper Mayone
2026-11-02 18:29 ` [PATCH v4 2/4] doc: for-each-ref: say how to read %(worktreepath) safely Jasper Mayone
@ 2026-11-02 18:29 ` Jasper Mayone
2026-11-10 16:47 ` Phillip Wood
2026-11-02 18:29 ` [PATCH v4 4/4] branch: do not mark the branch the current worktree is rebasing Jasper Mayone
` (2 subsequent siblings)
5 siblings, 1 reply; 50+ messages in thread
From: Jasper Mayone @ 2026-11-02 18:29 UTC (permalink / raw)
To: git
Cc: gitster, phillip.wood123, sunshine, kristofferhaugsbakk,
ben.knoble, haraldnordgren, Jasper Mayone
%(worktreepath) gives the worktree that has a branch checked out, and
"git branch" uses it to mark such branches with "+". It only looks at
the HEAD of each worktree, so while a worktree is rebasing or
bisecting a branch, its HEAD is detached and the branch shows no
worktree at all.
The rest of Git already treats that branch as checked out: "git
worktree add" and "git branch -d" refuse it, because finishing the
rebase or bisect will check it out again. But no command shows which
worktree is working on the branch: "git worktree list" shows such a
worktree as "(detached HEAD)", and %(worktreepath) is empty.
branch.c already knows the answer, as prepare_checked_out_branches()
records the branch that each worktree has checked out, is rebasing or
is bisecting. Add branch_checked_out_worktree() to look a branch up in
that list, and use it for %(worktreepath) in place of the map that
ref-filter.c builds from the HEAD of each worktree.
The new function skips the branches that "rebase --update-refs" will
update. branch_checked_out() includes them, so that they cannot be
checked out or deleted during the rebase, but the worktree that runs
the rebase has not checked them out. %(worktreepath) therefore stays
empty for them, unlike branch_checked_out().
This also changes the result for a bare repository: its HEAD names a
branch, and %(worktreepath) used to give the path of the bare
repository for that branch. "git branch" in a linked worktree marked
the branch with "+", although no worktree has it checked out and "git
worktree add" lets you check it out. prepare_checked_out_branches()
skips bare repositories, so %(worktreepath) is now empty for that
branch and "git branch" no longer marks it.
Reported-by: Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com>
Helped-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Jasper Mayone <me@jaspermayone.com>
---
Documentation/git-branch.adoc | 6 ++-
Documentation/git-for-each-ref.adoc | 5 +-
branch.c | 13 +++++
branch.h | 9 ++++
ref-filter.c | 74 ++--------------------------
t/t3203-branch-output.sh | 29 +++++++++++
t/t6302-for-each-ref-filter.sh | 76 +++++++++++++++++++++++++++++
7 files changed, 138 insertions(+), 74 deletions(-)
diff --git a/Documentation/git-branch.adoc b/Documentation/git-branch.adoc
index bfdf459329..510b0afc74 100644
--- a/Documentation/git-branch.adoc
+++ b/Documentation/git-branch.adoc
@@ -33,8 +33,10 @@ DESCRIPTION
If `--list` is given, or if there are no non-option arguments, existing
branches are listed; the current branch will be highlighted in green and
marked with an asterisk. Any branches checked out in linked worktrees will
-be highlighted in cyan and marked with a plus sign. Option `-r` causes the
-remote-tracking branches to be listed,
+be highlighted in cyan and marked with a plus sign. A branch that a
+worktree is rebasing or bisecting counts as checked out in that
+worktree, so the other worktrees mark it in the same way. Option `-r`
+causes the remote-tracking branches to be listed,
and option `-a` shows both local and remote branches.
If a `<pattern>`
diff --git a/Documentation/git-for-each-ref.adoc b/Documentation/git-for-each-ref.adoc
index 9247737aa7..4456b4809a 100644
--- a/Documentation/git-for-each-ref.adoc
+++ b/Documentation/git-for-each-ref.adoc
@@ -182,7 +182,10 @@ but if used together the last option is selected.
`worktreepath`::
The absolute path to the worktree in which the ref is checked
- out, if it is checked out in any linked worktree. Empty string
+ out, if it is checked out in any linked worktree. A branch that
+ a worktree is rebasing or bisecting counts as checked out there,
+ although that worktree's `HEAD` is detached. A branch that
+ `git rebase --update-refs` will update does not. Empty string
otherwise. If the ref is checked out in more than one worktree,
only one of them is shown. The path can contain a newline, so
use one of the quoting options such as `--shell`, or follow
diff --git a/branch.c b/branch.c
index 5bb1c28915..8dab72d857 100644
--- a/branch.c
+++ b/branch.c
@@ -485,6 +485,19 @@ const char *branch_checked_out(const char *refname)
return strmap_get(¤t_checked_out_branches, refname);
}
+const char *branch_checked_out_worktree(const char *refname)
+{
+ const char *path = NULL;
+
+ prepare_checked_out_branches();
+ for (size_t i = 0; i < checked_out_branches_nr; i++) {
+ if (checked_out_branches[i].kind != BRANCH_CHECKOUT_KIND_UPDATE_REF &&
+ !strcmp(refname, checked_out_branches[i].refname))
+ path = checked_out_branches[i].path;
+ }
+ return path;
+}
+
const char *branch_bisecting(const char *refname)
{
prepare_checked_out_branches();
diff --git a/branch.h b/branch.h
index 95b2431f24..60cb92a3e4 100644
--- a/branch.h
+++ b/branch.h
@@ -106,6 +106,15 @@ void create_branches_recursively(struct repository *r, const char *name,
*/
const char *branch_checked_out(const char *refname);
+/*
+ * If a worktree has the branch 'refname' checked out, or is rebasing or
+ * bisecting it, return the path of that worktree, or of the last one if
+ * there are more. Otherwise return NULL. Unlike branch_checked_out(),
+ * this ignores the branches that "git rebase --update-refs" will
+ * update, because no worktree has them checked out.
+ */
+const char *branch_checked_out_worktree(const char *refname);
+
/*
* If the branch at 'refname' is currently used for bisecting in a
* worktree, then return the path to that worktree.
diff --git a/ref-filter.c b/ref-filter.c
index bdf54f6f59..b636e56199 100644
--- a/ref-filter.c
+++ b/ref-filter.c
@@ -2,6 +2,7 @@
#define DISABLE_SIGN_COMPARE_WARNINGS
#include "git-compat-util.h"
+#include "branch.h"
#include "environment.h"
#include "gettext.h"
#include "config.h"
@@ -95,30 +96,6 @@ static struct expand_data {
struct object_info info;
} oi, oi_deref;
-struct ref_to_worktree_entry {
- struct hashmap_entry ent;
- struct worktree *wt; /* key is wt->head_ref */
-};
-
-static int ref_to_worktree_map_cmpfnc(const void *lookupdata UNUSED,
- const struct hashmap_entry *eptr,
- const struct hashmap_entry *kptr,
- const void *keydata_aka_refname)
-{
- const struct ref_to_worktree_entry *e, *k;
-
- e = container_of(eptr, const struct ref_to_worktree_entry, ent);
- k = container_of(kptr, const struct ref_to_worktree_entry, ent);
-
- return strcmp(e->wt->head_ref,
- keydata_aka_refname ? keydata_aka_refname : k->wt->head_ref);
-}
-
-static struct ref_to_worktree_map {
- struct hashmap map;
- struct worktree **worktrees;
-} ref_to_worktree_map;
-
/*
* The enum atom_type is used as the index of valid_atom array.
* In the atom parsing stage, it will be passed to used_atom.atom_type
@@ -2380,49 +2357,11 @@ static int get_object(struct ref_array_item *ref, int deref,
return ret;
}
-static void populate_worktree_map(struct hashmap *map, struct worktree **worktrees)
-{
- int i;
-
- for (i = 0; worktrees[i]; i++) {
- if (worktrees[i]->head_ref) {
- struct ref_to_worktree_entry *entry;
- entry = xmalloc(sizeof(*entry));
- entry->wt = worktrees[i];
- hashmap_entry_init(&entry->ent,
- strhash(worktrees[i]->head_ref));
-
- hashmap_add(map, &entry->ent);
- }
- }
-}
-
-static void lazy_init_worktree_map(void)
-{
- if (ref_to_worktree_map.worktrees)
- return;
-
- ref_to_worktree_map.worktrees = get_worktrees(the_repository);
- hashmap_init(&(ref_to_worktree_map.map), ref_to_worktree_map_cmpfnc, NULL, 0);
- populate_worktree_map(&(ref_to_worktree_map.map), ref_to_worktree_map.worktrees);
-}
-
static char *get_worktree_path(const struct ref_array_item *ref)
{
- struct hashmap_entry entry, *e;
- struct ref_to_worktree_entry *lookup_result;
+ const char *path = branch_checked_out_worktree(ref->refname);
- lazy_init_worktree_map();
-
- hashmap_entry_init(&entry, strhash(ref->refname));
- e = hashmap_get(&(ref_to_worktree_map.map), &entry, ref->refname);
-
- if (!e)
- return xstrdup("");
-
- lookup_result = container_of(e, struct ref_to_worktree_entry, ent);
-
- return xstrdup(lookup_result->wt->path);
+ return xstrdup(path ? path : "");
}
/*
@@ -3195,13 +3134,6 @@ void ref_array_clear(struct ref_array *array)
FREE_AND_NULL(used_atom);
used_atom_cnt = 0;
- if (ref_to_worktree_map.worktrees) {
- hashmap_clear_and_free(&(ref_to_worktree_map.map),
- struct ref_to_worktree_entry, ent);
- free_worktrees(ref_to_worktree_map.worktrees);
- ref_to_worktree_map.worktrees = NULL;
- }
-
FREE_AND_NULL(array->counts);
}
diff --git a/t/t3203-branch-output.sh b/t/t3203-branch-output.sh
index 7c44186341..debfac5f26 100755
--- a/t/t3203-branch-output.sh
+++ b/t/t3203-branch-output.sh
@@ -442,6 +442,35 @@ test_expect_success 'worktree colors correct' '
test_cmp expect actual
'
+test_expect_success 'branch being rebased in a worktree is marked' '
+ test_when_finished "git -C rebase_dir rebase --abort; rm -rf rebase_dir; git worktree prune; git branch -D rebasing" &&
+ git worktree add -b rebasing rebase_dir branch-two &&
+ (
+ cd rebase_dir &&
+ git commit --allow-empty -m rebase-me &&
+ test_must_fail git rebase -f -x false HEAD~1 &&
+ test_must_fail git symbolic-ref -q HEAD
+ ) &&
+ git branch --list rebasing >actual &&
+ echo "+ rebasing" >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success 'branch being bisected in a worktree is marked' '
+ test_when_finished "git -C bisect_dir bisect reset; rm -rf bisect_dir; git worktree prune; git branch -D bisecting" &&
+ git worktree add -b bisecting bisect_dir branch-two &&
+ (
+ cd bisect_dir &&
+ git commit --allow-empty -m bisect-one &&
+ git commit --allow-empty -m bisect-two &&
+ git bisect start HEAD HEAD~2 &&
+ test_must_fail git symbolic-ref -q HEAD
+ ) &&
+ git branch --list bisecting >actual &&
+ echo "+ bisecting" >expect &&
+ test_cmp expect actual
+'
+
test_expect_success "set up color tests" '
echo "<RED>main<RESET>" >expect.color &&
echo "main" >expect.bare &&
diff --git a/t/t6302-for-each-ref-filter.sh b/t/t6302-for-each-ref-filter.sh
index 7f060d97bf..317ed6f6cd 100755
--- a/t/t6302-for-each-ref-filter.sh
+++ b/t/t6302-for-each-ref-filter.sh
@@ -3,6 +3,7 @@
test_description='test for-each-refs usage of ref-filter APIs'
. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-rebase.sh
. "$TEST_DIRECTORY"/lib-gpg.sh
test_expect_success 'setup some history and refs' '
@@ -541,6 +542,81 @@ test_expect_success 'validate worktree atom' '
test_cmp expect actual
'
+test_expect_success 'worktree atom names a worktree rebasing the branch' '
+ test_when_finished "git -C rebasing rebase --abort; rm -rf rebasing; git worktree prune; git branch -D rebasing" &&
+ git worktree add -b rebasing rebasing main &&
+ git -C rebasing commit --allow-empty -m rebase-me &&
+ test_must_fail git -C rebasing rebase -f -x false main &&
+ test_must_fail git -C rebasing symbolic-ref -q HEAD &&
+ echo "$(pwd)/rebasing" >expect &&
+ git for-each-ref --format="%(worktreepath)" refs/heads/rebasing >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'worktree atom names a worktree bisecting the branch' '
+ test_when_finished "git -C bisecting bisect reset; rm -rf bisecting; git worktree prune; git branch -D bisecting" &&
+ git worktree add -b bisecting bisecting main &&
+ git -C bisecting commit --allow-empty -m bisect-one &&
+ git -C bisecting commit --allow-empty -m bisect-two &&
+ git -C bisecting bisect start HEAD main &&
+ test_must_fail git -C bisecting symbolic-ref -q HEAD &&
+ echo "$(pwd)/bisecting" >expect &&
+ git for-each-ref --format="%(worktreepath)" refs/heads/bisecting >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'worktree atom with an interactive rebase stopped at edit' '
+ test_when_finished "git -C editing rebase --abort; rm -rf editing; git worktree prune; git branch -D editing" &&
+ git worktree add -b editing editing main &&
+ git -C editing commit --allow-empty -m edit-me &&
+ (
+ set_fake_editor &&
+ FAKE_LINES="edit 1" git -C editing rebase -i --keep-empty HEAD~1
+ ) &&
+ test_must_fail git -C editing symbolic-ref -q HEAD &&
+ echo "$(pwd)/editing" >expect &&
+ git for-each-ref --format="%(worktreepath)" refs/heads/editing >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'worktree atom ignores branches rebase --update-refs will move' '
+ test_when_finished "git -C updating rebase --abort; rm -rf updating; git worktree prune; git branch -D updating mid" &&
+ git worktree add -b updating updating main &&
+ git -C updating commit --allow-empty -m one &&
+ git -C updating branch mid &&
+ git -C updating commit --allow-empty -m two &&
+ test_must_fail git -C updating rebase -f --update-refs -x false main &&
+ git for-each-ref --format="%(refname:short) [%(worktreepath)]" \
+ refs/heads/updating refs/heads/mid >actual &&
+ cat >expect <<-EOF &&
+ mid []
+ updating [$(pwd)/updating]
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'worktree atom ignores a rebase of a detached HEAD' '
+ test_when_finished "git -C detached-rebase rebase --abort; rm -rf detached-rebase; git worktree prune" &&
+ git worktree add --detach detached-rebase main &&
+ git -C detached-rebase commit --allow-empty -m detached &&
+ test_must_fail git -C detached-rebase rebase -f -x false main &&
+ git for-each-ref --format="%(worktreepath)" refs/heads/ >actual &&
+ test_grep ! detached-rebase actual
+'
+
+test_expect_success 'worktree atom ignores the HEAD of a bare repository' '
+ test_when_finished "rm -rf bare.git bare-wt" &&
+ git clone --bare . bare.git &&
+ git -C bare.git symbolic-ref HEAD refs/heads/main &&
+ git -C bare.git worktree add --detach ../bare-wt main &&
+ git -C bare.git for-each-ref --format="%(worktreepath)" refs/heads/main >actual &&
+ echo >expect &&
+ test_cmp expect actual &&
+ git -C bare-wt branch --list main >actual &&
+ echo " main" >expect &&
+ test_cmp expect actual
+'
+
test_expect_success 'start after with empty value' '
cat >expect <<-\EOF &&
refs/heads/main
--
2.56.0
^ permalink raw reply related [flat|nested] 50+ messages in thread* Re: [PATCH v4 3/4] ref-filter: name the worktree of a branch being rebased or bisected
2026-11-02 18:29 ` [PATCH v4 3/4] ref-filter: name the worktree of a branch being rebased or bisected Jasper Mayone
@ 2026-11-10 16:47 ` Phillip Wood
0 siblings, 0 replies; 50+ messages in thread
From: Phillip Wood @ 2026-11-10 16:47 UTC (permalink / raw)
To: Jasper Mayone, git
Cc: gitster, sunshine, kristofferhaugsbakk, ben.knoble,
haraldnordgren
Hi Jasper
On 02/11/2026 18:29, Jasper Mayone wrote:
> %(worktreepath) gives the worktree that has a branch checked out, and
> "git branch" uses it to mark such branches with "+". It only looks at
> the HEAD of each worktree, so while a worktree is rebasing or
> bisecting a branch, its HEAD is detached and the branch shows no
> worktree at all.
>
> The rest of Git already treats that branch as checked out: "git
> worktree add" and "git branch -d" refuse it, because finishing the
> rebase or bisect will check it out again. But no command shows which
> worktree is working on the branch: "git worktree list" shows such a
> worktree as "(detached HEAD)", and %(worktreepath) is empty.
>
> branch.c already knows the answer, as prepare_checked_out_branches()
> records the branch that each worktree has checked out, is rebasing or
> is bisecting. Add branch_checked_out_worktree() to look a branch up in
> that list, and use it for %(worktreepath) in place of the map that
> ref-filter.c builds from the HEAD of each worktree.
>
> The new function skips the branches that "rebase --update-refs" will
> update. branch_checked_out() includes them, so that they cannot be
> checked out or deleted during the rebase, but the worktree that runs
> the rebase has not checked them out. %(worktreepath) therefore stays
> empty for them, unlike branch_checked_out().
>
> This also changes the result for a bare repository: its HEAD names a
> branch, and %(worktreepath) used to give the path of the bare
> repository for that branch. "git branch" in a linked worktree marked
> the branch with "+", although no worktree has it checked out and "git
> worktree add" lets you check it out. prepare_checked_out_branches()
> skips bare repositories, so %(worktreepath) is now empty for that
> branch and "git branch" no longer marks it.
We now document the change in behavior - good.
> diff --git a/branch.c b/branch.c
> index 5bb1c28915..8dab72d857 100644
> --- a/branch.c
> +++ b/branch.c
> @@ -485,6 +485,19 @@ const char *branch_checked_out(const char *refname)
> return strmap_get(¤t_checked_out_branches, refname);
> }
>
> +const char *branch_checked_out_worktree(const char *refname)
> +{
> + const char *path = NULL;
> +
> + prepare_checked_out_branches();
> + for (size_t i = 0; i < checked_out_branches_nr; i++) {
> + if (checked_out_branches[i].kind != BRANCH_CHECKOUT_KIND_UPDATE_REF &&
> + !strcmp(refname, checked_out_branches[i].refname))
> + path = checked_out_branches[i].path;
> + }
Iterating over all the worktrees for every ref is going to be slower
than it needs to be. As I said in [1] we should use a hash map to map
branch names to the worktree where they are checked out. That is also
what the existing ref-filter code does.
Then we can add a function to get the worktree path where a branch is
checked out and call that from the ref-filter code.
Thanks
Phillip
> + return path;
> +}
> +
> const char *branch_bisecting(const char *refname)
> {
> prepare_checked_out_branches();
> diff --git a/branch.h b/branch.h
> index 95b2431f24..60cb92a3e4 100644
> --- a/branch.h
> +++ b/branch.h
> @@ -106,6 +106,15 @@ void create_branches_recursively(struct repository *r, const char *name,
> */
> const char *branch_checked_out(const char *refname);
>
> +/*
> + * If a worktree has the branch 'refname' checked out, or is rebasing or
> + * bisecting it, return the path of that worktree, or of the last one if
> + * there are more. Otherwise return NULL. Unlike branch_checked_out(),
> + * this ignores the branches that "git rebase --update-refs" will
> + * update, because no worktree has them checked out.
> + */
> +const char *branch_checked_out_worktree(const char *refname);
> +
> /*
> * If the branch at 'refname' is currently used for bisecting in a
> * worktree, then return the path to that worktree.
> diff --git a/ref-filter.c b/ref-filter.c
> index bdf54f6f59..b636e56199 100644
> --- a/ref-filter.c
> +++ b/ref-filter.c
> @@ -2,6 +2,7 @@
> #define DISABLE_SIGN_COMPARE_WARNINGS
>
> #include "git-compat-util.h"
> +#include "branch.h"
> #include "environment.h"
> #include "gettext.h"
> #include "config.h"
> @@ -95,30 +96,6 @@ static struct expand_data {
> struct object_info info;
> } oi, oi_deref;
>
> -struct ref_to_worktree_entry {
> - struct hashmap_entry ent;
> - struct worktree *wt; /* key is wt->head_ref */
> -};
> -
> -static int ref_to_worktree_map_cmpfnc(const void *lookupdata UNUSED,
> - const struct hashmap_entry *eptr,
> - const struct hashmap_entry *kptr,
> - const void *keydata_aka_refname)
> -{
> - const struct ref_to_worktree_entry *e, *k;
> -
> - e = container_of(eptr, const struct ref_to_worktree_entry, ent);
> - k = container_of(kptr, const struct ref_to_worktree_entry, ent);
> -
> - return strcmp(e->wt->head_ref,
> - keydata_aka_refname ? keydata_aka_refname : k->wt->head_ref);
> -}
> -
> -static struct ref_to_worktree_map {
> - struct hashmap map;
> - struct worktree **worktrees;
> -} ref_to_worktree_map;
> -
> /*
> * The enum atom_type is used as the index of valid_atom array.
> * In the atom parsing stage, it will be passed to used_atom.atom_type
> @@ -2380,49 +2357,11 @@ static int get_object(struct ref_array_item *ref, int deref,
> return ret;
> }
>
> -static void populate_worktree_map(struct hashmap *map, struct worktree **worktrees)
> -{
> - int i;
> -
> - for (i = 0; worktrees[i]; i++) {
> - if (worktrees[i]->head_ref) {
> - struct ref_to_worktree_entry *entry;
> - entry = xmalloc(sizeof(*entry));
> - entry->wt = worktrees[i];
> - hashmap_entry_init(&entry->ent,
> - strhash(worktrees[i]->head_ref));
> -
> - hashmap_add(map, &entry->ent);
> - }
> - }
> -}
> -
> -static void lazy_init_worktree_map(void)
> -{
> - if (ref_to_worktree_map.worktrees)
> - return;
> -
> - ref_to_worktree_map.worktrees = get_worktrees(the_repository);
> - hashmap_init(&(ref_to_worktree_map.map), ref_to_worktree_map_cmpfnc, NULL, 0);
> - populate_worktree_map(&(ref_to_worktree_map.map), ref_to_worktree_map.worktrees);
> -}
> -
> static char *get_worktree_path(const struct ref_array_item *ref)
> {
> - struct hashmap_entry entry, *e;
> - struct ref_to_worktree_entry *lookup_result;
> + const char *path = branch_checked_out_worktree(ref->refname);
>
> - lazy_init_worktree_map();
> -
> - hashmap_entry_init(&entry, strhash(ref->refname));
> - e = hashmap_get(&(ref_to_worktree_map.map), &entry, ref->refname);
> -
> - if (!e)
> - return xstrdup("");
> -
> - lookup_result = container_of(e, struct ref_to_worktree_entry, ent);
> -
> - return xstrdup(lookup_result->wt->path);
> + return xstrdup(path ? path : "");
> }
>
> /*
> @@ -3195,13 +3134,6 @@ void ref_array_clear(struct ref_array *array)
> FREE_AND_NULL(used_atom);
> used_atom_cnt = 0;
>
> - if (ref_to_worktree_map.worktrees) {
> - hashmap_clear_and_free(&(ref_to_worktree_map.map),
> - struct ref_to_worktree_entry, ent);
> - free_worktrees(ref_to_worktree_map.worktrees);
> - ref_to_worktree_map.worktrees = NULL;
> - }
> -
> FREE_AND_NULL(array->counts);
> }
>
> diff --git a/t/t3203-branch-output.sh b/t/t3203-branch-output.sh
> index 7c44186341..debfac5f26 100755
> --- a/t/t3203-branch-output.sh
> +++ b/t/t3203-branch-output.sh
> @@ -442,6 +442,35 @@ test_expect_success 'worktree colors correct' '
> test_cmp expect actual
> '
>
> +test_expect_success 'branch being rebased in a worktree is marked' '
> + test_when_finished "git -C rebase_dir rebase --abort; rm -rf rebase_dir; git worktree prune; git branch -D rebasing" &&
> + git worktree add -b rebasing rebase_dir branch-two &&
> + (
> + cd rebase_dir &&
> + git commit --allow-empty -m rebase-me &&
> + test_must_fail git rebase -f -x false HEAD~1 &&
> + test_must_fail git symbolic-ref -q HEAD
> + ) &&
> + git branch --list rebasing >actual &&
> + echo "+ rebasing" >expect &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success 'branch being bisected in a worktree is marked' '
> + test_when_finished "git -C bisect_dir bisect reset; rm -rf bisect_dir; git worktree prune; git branch -D bisecting" &&
> + git worktree add -b bisecting bisect_dir branch-two &&
> + (
> + cd bisect_dir &&
> + git commit --allow-empty -m bisect-one &&
> + git commit --allow-empty -m bisect-two &&
> + git bisect start HEAD HEAD~2 &&
> + test_must_fail git symbolic-ref -q HEAD
> + ) &&
> + git branch --list bisecting >actual &&
> + echo "+ bisecting" >expect &&
> + test_cmp expect actual
> +'
> +
> test_expect_success "set up color tests" '
> echo "<RED>main<RESET>" >expect.color &&
> echo "main" >expect.bare &&
> diff --git a/t/t6302-for-each-ref-filter.sh b/t/t6302-for-each-ref-filter.sh
> index 7f060d97bf..317ed6f6cd 100755
> --- a/t/t6302-for-each-ref-filter.sh
> +++ b/t/t6302-for-each-ref-filter.sh
> @@ -3,6 +3,7 @@
> test_description='test for-each-refs usage of ref-filter APIs'
>
> . ./test-lib.sh
> +. "$TEST_DIRECTORY"/lib-rebase.sh
> . "$TEST_DIRECTORY"/lib-gpg.sh
>
> test_expect_success 'setup some history and refs' '
> @@ -541,6 +542,81 @@ test_expect_success 'validate worktree atom' '
> test_cmp expect actual
> '
>
> +test_expect_success 'worktree atom names a worktree rebasing the branch' '
> + test_when_finished "git -C rebasing rebase --abort; rm -rf rebasing; git worktree prune; git branch -D rebasing" &&
> + git worktree add -b rebasing rebasing main &&
> + git -C rebasing commit --allow-empty -m rebase-me &&
> + test_must_fail git -C rebasing rebase -f -x false main &&
> + test_must_fail git -C rebasing symbolic-ref -q HEAD &&
> + echo "$(pwd)/rebasing" >expect &&
> + git for-each-ref --format="%(worktreepath)" refs/heads/rebasing >actual &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success 'worktree atom names a worktree bisecting the branch' '
> + test_when_finished "git -C bisecting bisect reset; rm -rf bisecting; git worktree prune; git branch -D bisecting" &&
> + git worktree add -b bisecting bisecting main &&
> + git -C bisecting commit --allow-empty -m bisect-one &&
> + git -C bisecting commit --allow-empty -m bisect-two &&
> + git -C bisecting bisect start HEAD main &&
> + test_must_fail git -C bisecting symbolic-ref -q HEAD &&
> + echo "$(pwd)/bisecting" >expect &&
> + git for-each-ref --format="%(worktreepath)" refs/heads/bisecting >actual &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success 'worktree atom with an interactive rebase stopped at edit' '
> + test_when_finished "git -C editing rebase --abort; rm -rf editing; git worktree prune; git branch -D editing" &&
> + git worktree add -b editing editing main &&
> + git -C editing commit --allow-empty -m edit-me &&
> + (
> + set_fake_editor &&
> + FAKE_LINES="edit 1" git -C editing rebase -i --keep-empty HEAD~1
> + ) &&
> + test_must_fail git -C editing symbolic-ref -q HEAD &&
> + echo "$(pwd)/editing" >expect &&
> + git for-each-ref --format="%(worktreepath)" refs/heads/editing >actual &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success 'worktree atom ignores branches rebase --update-refs will move' '
> + test_when_finished "git -C updating rebase --abort; rm -rf updating; git worktree prune; git branch -D updating mid" &&
> + git worktree add -b updating updating main &&
> + git -C updating commit --allow-empty -m one &&
> + git -C updating branch mid &&
> + git -C updating commit --allow-empty -m two &&
> + test_must_fail git -C updating rebase -f --update-refs -x false main &&
> + git for-each-ref --format="%(refname:short) [%(worktreepath)]" \
> + refs/heads/updating refs/heads/mid >actual &&
> + cat >expect <<-EOF &&
> + mid []
> + updating [$(pwd)/updating]
> + EOF
> + test_cmp expect actual
> +'
> +
> +test_expect_success 'worktree atom ignores a rebase of a detached HEAD' '
> + test_when_finished "git -C detached-rebase rebase --abort; rm -rf detached-rebase; git worktree prune" &&
> + git worktree add --detach detached-rebase main &&
> + git -C detached-rebase commit --allow-empty -m detached &&
> + test_must_fail git -C detached-rebase rebase -f -x false main &&
> + git for-each-ref --format="%(worktreepath)" refs/heads/ >actual &&
> + test_grep ! detached-rebase actual
> +'
> +
> +test_expect_success 'worktree atom ignores the HEAD of a bare repository' '
> + test_when_finished "rm -rf bare.git bare-wt" &&
> + git clone --bare . bare.git &&
> + git -C bare.git symbolic-ref HEAD refs/heads/main &&
> + git -C bare.git worktree add --detach ../bare-wt main &&
> + git -C bare.git for-each-ref --format="%(worktreepath)" refs/heads/main >actual &&
> + echo >expect &&
> + test_cmp expect actual &&
> + git -C bare-wt branch --list main >actual &&
> + echo " main" >expect &&
> + test_cmp expect actual
> +'
> +
> test_expect_success 'start after with empty value' '
> cat >expect <<-\EOF &&
> refs/heads/main
^ permalink raw reply [flat|nested] 50+ messages in thread
* [PATCH v4 4/4] branch: do not mark the branch the current worktree is rebasing
2026-11-02 18:29 ` [PATCH v4 0/4] " Jasper Mayone
` (2 preceding siblings ...)
2026-11-02 18:29 ` [PATCH v4 3/4] ref-filter: name the worktree of a branch being rebased or bisected Jasper Mayone
@ 2026-11-02 18:29 ` Jasper Mayone
2026-11-10 16:45 ` Phillip Wood
2026-11-10 16:49 ` [PATCH v4 0/4] worktree: treat a branch being rebased or bisected as checked out Phillip Wood
2026-11-11 15:28 ` [PATCH v5 0/5] " Jasper Mayone
5 siblings, 1 reply; 50+ messages in thread
From: Jasper Mayone @ 2026-11-02 18:29 UTC (permalink / raw)
To: git
Cc: gitster, phillip.wood123, sunshine, kristofferhaugsbakk,
ben.knoble, haraldnordgren, Jasper Mayone
Since the previous commit, "git branch" marks a branch with "+" when a
worktree is rebasing or bisecting it. In that worktree itself, the
list already starts with "* (no branch, rebasing topic)", so the
"+ topic" line below it says the same thing twice.
The default format cannot tell that apart: %(worktreepath) gives the
same path either way, and comparing it with %(if:equals=...) is not
safe because a path, or a branch name, can contain the ")" that ends
the atom. So build a second format that has no "+" marker and no
"(path)" for -vv, and use it for each branch that
branch_checked_out_worktree() maps to the current worktree. A branch
that is checked out normally in the current worktree also gets that
format, but %(HEAD) still marks it with "*" and -vv already hides
its path, so its output does not change.
To pick a format for each branch, print_ref_list() now formats the
branches itself in place of print_formatted_ref_array(), keeping
--omit-empty. A format given with --format is used for every branch,
as before.
Suggested-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Jasper Mayone <me@jaspermayone.com>
---
builtin/branch.c | 103 ++++++++++++++++++++++++++++++---------
t/t3203-branch-output.sh | 22 ++++++++-
2 files changed, 101 insertions(+), 24 deletions(-)
diff --git a/builtin/branch.c b/builtin/branch.c
index a613148fc7..b55a400217 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -418,16 +418,28 @@ static const char *quote_literal_for_format(const char *s)
return buf.buf;
}
-static char *build_format(struct ref_filter *filter, int maxwidth, const char *remote_prefix)
+/*
+ * With "show_worktree" unset, the format does not mark a branch that is
+ * checked out in a worktree, nor show the path of that worktree. It is
+ * used for a branch that the current worktree is rebasing or bisecting,
+ * because the "(no branch, ...)" line already names it.
+ */
+static char *build_format(struct ref_filter *filter, int maxwidth,
+ const char *remote_prefix, int show_worktree)
{
struct strbuf fmt = STRBUF_INIT;
struct strbuf local = STRBUF_INIT;
struct strbuf remote = STRBUF_INIT;
- strbuf_addf(&local, "%%(if)%%(HEAD)%%(then)* %s%%(else)%%(if)%%(worktreepath)%%(then)+ %s%%(else) %s%%(end)%%(end)",
- branch_get_color(BRANCH_COLOR_CURRENT),
- branch_get_color(BRANCH_COLOR_WORKTREE),
- branch_get_color(BRANCH_COLOR_LOCAL));
+ if (show_worktree)
+ strbuf_addf(&local, "%%(if)%%(HEAD)%%(then)* %s%%(else)%%(if)%%(worktreepath)%%(then)+ %s%%(else) %s%%(end)%%(end)",
+ branch_get_color(BRANCH_COLOR_CURRENT),
+ branch_get_color(BRANCH_COLOR_WORKTREE),
+ branch_get_color(BRANCH_COLOR_LOCAL));
+ else
+ strbuf_addf(&local, "%%(if)%%(HEAD)%%(then)* %s%%(else) %s%%(end)",
+ branch_get_color(BRANCH_COLOR_CURRENT),
+ branch_get_color(BRANCH_COLOR_LOCAL));
strbuf_addf(&remote, " %s",
branch_get_color(BRANCH_COLOR_REMOTE));
@@ -447,8 +459,9 @@ static char *build_format(struct ref_filter *filter, int maxwidth, const char *r
if (filter->verbose > 1)
{
- strbuf_addf(&local, "%%(if:notequals=*)%%(HEAD)%%(then)%%(if)%%(worktreepath)%%(then)(%s%%(worktreepath)%s) %%(end)%%(end)",
- branch_get_color(BRANCH_COLOR_WORKTREE), branch_get_color(BRANCH_COLOR_RESET));
+ if (show_worktree)
+ strbuf_addf(&local, "%%(if:notequals=*)%%(HEAD)%%(then)%%(if)%%(worktreepath)%%(then)(%s%%(worktreepath)%s) %%(end)%%(end)",
+ branch_get_color(BRANCH_COLOR_WORKTREE), branch_get_color(BRANCH_COLOR_RESET));
strbuf_addf(&local, "%%(if)%%(upstream)%%(then)[%s%%(upstream:short)%s%%(if)%%(upstream:track)"
"%%(then): %%(upstream:track,nobracket)%%(end)] %%(end)%%(contents:subject)",
branch_get_color(BRANCH_COLOR_UPSTREAM), branch_get_color(BRANCH_COLOR_RESET));
@@ -477,6 +490,32 @@ static char *build_format(struct ref_filter *filter, int maxwidth, const char *r
return strbuf_detach(&fmt, NULL);
}
+static char *current_worktree_path(void)
+{
+ struct worktree **worktrees = get_worktrees(the_repository);
+ char *path = NULL;
+
+ for (size_t i = 0; worktrees[i]; i++) {
+ if (worktrees[i]->is_current) {
+ path = xstrdup(worktrees[i]->path);
+ break;
+ }
+ }
+ free_worktrees(worktrees);
+ return path;
+}
+
+/*
+ * Is "refname" checked out in the worktree at "here", or is that
+ * worktree rebasing or bisecting it?
+ */
+static int checked_out_here(const char *refname, const char *here)
+{
+ const char *path = branch_checked_out_worktree(refname);
+
+ return path && !strcmp(path, here);
+}
+
static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sorting,
struct ref_format *format, struct string_list *output)
{
@@ -485,6 +524,9 @@ static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sortin
int maxwidth = 0;
const char *remote_prefix = "";
char *to_free = NULL;
+ struct ref_format here_format = REF_FORMAT_INIT;
+ char *here_to_free = NULL, *here = NULL;
+ struct strbuf out = STRBUF_INIT, err = STRBUF_INIT;
/*
* If we are listing more than just remote branches,
@@ -501,39 +543,54 @@ static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sortin
if (filter->verbose)
maxwidth = calc_maxwidth(&array, strlen(remote_prefix));
- if (!format->format)
- format->format = to_free = build_format(filter, maxwidth, remote_prefix);
+ if (!format->format) {
+ format->format = to_free = build_format(filter, maxwidth,
+ remote_prefix, 1);
+ here = current_worktree_path();
+ here_format = *format;
+ here_format.format = here_to_free = build_format(filter, maxwidth,
+ remote_prefix, 0);
+ }
format->use_color = branch_use_color;
+ here_format.use_color = branch_use_color;
- if (verify_ref_format(format))
+ if (verify_ref_format(format) ||
+ (here && verify_ref_format(&here_format)))
die(_("unable to parse format string"));
filter_ahead_behind(the_repository, &array);
ref_array_sort(sorting, &array);
- if (column_active(colopts)) {
- struct strbuf out = STRBUF_INIT, err = STRBUF_INIT;
-
+ if (column_active(colopts))
assert(!filter->verbose && "--column and --verbose are incompatible");
- for (i = 0; i < array.nr; i++) {
- strbuf_reset(&err);
- strbuf_reset(&out);
- if (format_ref_array_item(array.items[i], format, &out, &err))
- die("%s", err.buf);
+ for (i = 0; i < array.nr; i++) {
+ struct ref_array_item *item = array.items[i];
+ struct ref_format *f = format;
+ if (here && checked_out_here(item->refname, here))
+ f = &here_format;
+
+ strbuf_reset(&err);
+ strbuf_reset(&out);
+ if (format_ref_array_item(item, f, &out, &err))
+ die("%s", err.buf);
+
+ if (column_active(colopts))
/* format to a string_list to let print_columns() do its job */
string_list_append(output, out.buf);
+ else if (out.len || !format->array_opts.omit_empty) {
+ fwrite(out.buf, 1, out.len, stdout);
+ putchar('\n');
}
-
- strbuf_release(&err);
- strbuf_release(&out);
- } else {
- print_formatted_ref_array(&array, format);
}
+ strbuf_release(&err);
+ strbuf_release(&out);
ref_array_clear(&array);
free(to_free);
+ free(here_to_free);
+ free(here);
}
static void print_current_branch_name(void)
diff --git a/t/t3203-branch-output.sh b/t/t3203-branch-output.sh
index debfac5f26..5541f1429b 100755
--- a/t/t3203-branch-output.sh
+++ b/t/t3203-branch-output.sh
@@ -456,6 +456,23 @@ test_expect_success 'branch being rebased in a worktree is marked' '
test_cmp expect actual
'
+test_expect_success 'branch being rebased is not marked in its own worktree' '
+ test_when_finished "git -C rebase_dir rebase --abort; rm -rf rebase_dir; git worktree prune; git branch -D rebasing" &&
+ git worktree add -b rebasing rebase_dir branch-two &&
+ (
+ cd rebase_dir &&
+ git commit --allow-empty -m rebase-me &&
+ test_must_fail git rebase -f -x false HEAD~1 &&
+ git branch --list rebasing >actual &&
+ echo " rebasing" >expect &&
+ test_cmp expect actual &&
+ git branch -vv --list rebasing >actual &&
+ test_grep ! "$(pwd)" actual &&
+ git branch >actual &&
+ test_grep "^\* (no branch, rebasing rebasing)" actual
+ )
+'
+
test_expect_success 'branch being bisected in a worktree is marked' '
test_when_finished "git -C bisect_dir bisect reset; rm -rf bisect_dir; git worktree prune; git branch -D bisecting" &&
git worktree add -b bisecting bisect_dir branch-two &&
@@ -464,7 +481,10 @@ test_expect_success 'branch being bisected in a worktree is marked' '
git commit --allow-empty -m bisect-one &&
git commit --allow-empty -m bisect-two &&
git bisect start HEAD HEAD~2 &&
- test_must_fail git symbolic-ref -q HEAD
+ test_must_fail git symbolic-ref -q HEAD &&
+ git branch --list bisecting >actual &&
+ echo " bisecting" >expect &&
+ test_cmp expect actual
) &&
git branch --list bisecting >actual &&
echo "+ bisecting" >expect &&
--
2.56.0
^ permalink raw reply related [flat|nested] 50+ messages in thread* Re: [PATCH v4 4/4] branch: do not mark the branch the current worktree is rebasing
2026-11-02 18:29 ` [PATCH v4 4/4] branch: do not mark the branch the current worktree is rebasing Jasper Mayone
@ 2026-11-10 16:45 ` Phillip Wood
0 siblings, 0 replies; 50+ messages in thread
From: Phillip Wood @ 2026-11-10 16:45 UTC (permalink / raw)
To: Jasper Mayone, git
Cc: gitster, sunshine, kristofferhaugsbakk, ben.knoble,
haraldnordgren
Hi Jasper
On 02/11/2026 18:29, Jasper Mayone wrote:
>
> diff --git a/builtin/branch.c b/builtin/branch.c
> index a613148fc7..b55a400217 100644
> --- a/builtin/branch.c
> +++ b/builtin/branch.c
> @@ -418,16 +418,28 @@ static const char *quote_literal_for_format(const char *s)
> return buf.buf;
> }
>
> -static char *build_format(struct ref_filter *filter, int maxwidth, const char *remote_prefix)
> +/*
> + * With "show_worktree" unset, the format does not mark a branch that is
> + * checked out in a worktree, nor show the path of that worktree. It is
What is "It" here
> + * used for a branch that the current worktree is rebasing or bisecting,
> + * because the "(no branch, ...)" line already names it.
I'm not sure what is being named - do you mean "it already shows the
branch name"?
In any case there are a lot of code changes here which is surprising for
what I had hoped would be a simple tweak. If we added a
'%(currentworktree)' format specifier, which should be relatively easy
as we can use the hash map of branches to checked out worktrees to look
up the worktree and check "wt->is_current" then
%(if)%(worktreepath)%(then)%(if)%(currentworktree)%(then)%(else)+
%(else) %(end)%(end)
labels checked out worktrees with "+ " if they are not the current worktree.
Thanks
Phillip
> + */
> +static char *build_format(struct ref_filter *filter, int maxwidth,
> + const char *remote_prefix, int show_worktree)
> {
> struct strbuf fmt = STRBUF_INIT;
> struct strbuf local = STRBUF_INIT;
> struct strbuf remote = STRBUF_INIT;
>
> - strbuf_addf(&local, "%%(if)%%(HEAD)%%(then)* %s%%(else)%%(if)%%(worktreepath)%%(then)+ %s%%(else) %s%%(end)%%(end)",
> - branch_get_color(BRANCH_COLOR_CURRENT),
> - branch_get_color(BRANCH_COLOR_WORKTREE),
> - branch_get_color(BRANCH_COLOR_LOCAL));
> + if (show_worktree)
> + strbuf_addf(&local, "%%(if)%%(HEAD)%%(then)* %s%%(else)%%(if)%%(worktreepath)%%(then)+ %s%%(else) %s%%(end)%%(end)",
> + branch_get_color(BRANCH_COLOR_CURRENT),
> + branch_get_color(BRANCH_COLOR_WORKTREE),
> + branch_get_color(BRANCH_COLOR_LOCAL));
> + else
> + strbuf_addf(&local, "%%(if)%%(HEAD)%%(then)* %s%%(else) %s%%(end)",
> + branch_get_color(BRANCH_COLOR_CURRENT),
> + branch_get_color(BRANCH_COLOR_LOCAL));
> strbuf_addf(&remote, " %s",
> branch_get_color(BRANCH_COLOR_REMOTE));
>
> @@ -447,8 +459,9 @@ static char *build_format(struct ref_filter *filter, int maxwidth, const char *r
>
> if (filter->verbose > 1)
> {
> - strbuf_addf(&local, "%%(if:notequals=*)%%(HEAD)%%(then)%%(if)%%(worktreepath)%%(then)(%s%%(worktreepath)%s) %%(end)%%(end)",
> - branch_get_color(BRANCH_COLOR_WORKTREE), branch_get_color(BRANCH_COLOR_RESET));
> + if (show_worktree)
> + strbuf_addf(&local, "%%(if:notequals=*)%%(HEAD)%%(then)%%(if)%%(worktreepath)%%(then)(%s%%(worktreepath)%s) %%(end)%%(end)",
> + branch_get_color(BRANCH_COLOR_WORKTREE), branch_get_color(BRANCH_COLOR_RESET));
> strbuf_addf(&local, "%%(if)%%(upstream)%%(then)[%s%%(upstream:short)%s%%(if)%%(upstream:track)"
> "%%(then): %%(upstream:track,nobracket)%%(end)] %%(end)%%(contents:subject)",
> branch_get_color(BRANCH_COLOR_UPSTREAM), branch_get_color(BRANCH_COLOR_RESET));
> @@ -477,6 +490,32 @@ static char *build_format(struct ref_filter *filter, int maxwidth, const char *r
> return strbuf_detach(&fmt, NULL);
> }
>
> +static char *current_worktree_path(void)
> +{
> + struct worktree **worktrees = get_worktrees(the_repository);
> + char *path = NULL;
> +
> + for (size_t i = 0; worktrees[i]; i++) {
> + if (worktrees[i]->is_current) {
> + path = xstrdup(worktrees[i]->path);
> + break;
> + }
> + }
> + free_worktrees(worktrees);
> + return path;
> +}
> +
> +/*
> + * Is "refname" checked out in the worktree at "here", or is that
> + * worktree rebasing or bisecting it?
> + */
> +static int checked_out_here(const char *refname, const char *here)
> +{
> + const char *path = branch_checked_out_worktree(refname);
> +
> + return path && !strcmp(path, here);
> +}
> +
> static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sorting,
> struct ref_format *format, struct string_list *output)
> {
> @@ -485,6 +524,9 @@ static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sortin
> int maxwidth = 0;
> const char *remote_prefix = "";
> char *to_free = NULL;
> + struct ref_format here_format = REF_FORMAT_INIT;
> + char *here_to_free = NULL, *here = NULL;
> + struct strbuf out = STRBUF_INIT, err = STRBUF_INIT;
>
> /*
> * If we are listing more than just remote branches,
> @@ -501,39 +543,54 @@ static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sortin
> if (filter->verbose)
> maxwidth = calc_maxwidth(&array, strlen(remote_prefix));
>
> - if (!format->format)
> - format->format = to_free = build_format(filter, maxwidth, remote_prefix);
> + if (!format->format) {
> + format->format = to_free = build_format(filter, maxwidth,
> + remote_prefix, 1);
> + here = current_worktree_path();
> + here_format = *format;
> + here_format.format = here_to_free = build_format(filter, maxwidth,
> + remote_prefix, 0);
> + }
> format->use_color = branch_use_color;
> + here_format.use_color = branch_use_color;
>
> - if (verify_ref_format(format))
> + if (verify_ref_format(format) ||
> + (here && verify_ref_format(&here_format)))
> die(_("unable to parse format string"));
>
> filter_ahead_behind(the_repository, &array);
> ref_array_sort(sorting, &array);
>
> - if (column_active(colopts)) {
> - struct strbuf out = STRBUF_INIT, err = STRBUF_INIT;
> -
> + if (column_active(colopts))
> assert(!filter->verbose && "--column and --verbose are incompatible");
>
> - for (i = 0; i < array.nr; i++) {
> - strbuf_reset(&err);
> - strbuf_reset(&out);
> - if (format_ref_array_item(array.items[i], format, &out, &err))
> - die("%s", err.buf);
> + for (i = 0; i < array.nr; i++) {
> + struct ref_array_item *item = array.items[i];
> + struct ref_format *f = format;
>
> + if (here && checked_out_here(item->refname, here))
> + f = &here_format;
> +
> + strbuf_reset(&err);
> + strbuf_reset(&out);
> + if (format_ref_array_item(item, f, &out, &err))
> + die("%s", err.buf);
> +
> + if (column_active(colopts))
> /* format to a string_list to let print_columns() do its job */
> string_list_append(output, out.buf);
> + else if (out.len || !format->array_opts.omit_empty) {
> + fwrite(out.buf, 1, out.len, stdout);
> + putchar('\n');
> }
> -
> - strbuf_release(&err);
> - strbuf_release(&out);
> - } else {
> - print_formatted_ref_array(&array, format);
> }
>
> + strbuf_release(&err);
> + strbuf_release(&out);
> ref_array_clear(&array);
> free(to_free);
> + free(here_to_free);
> + free(here);
> }
>
> static void print_current_branch_name(void)
> diff --git a/t/t3203-branch-output.sh b/t/t3203-branch-output.sh
> index debfac5f26..5541f1429b 100755
> --- a/t/t3203-branch-output.sh
> +++ b/t/t3203-branch-output.sh
> @@ -456,6 +456,23 @@ test_expect_success 'branch being rebased in a worktree is marked' '
> test_cmp expect actual
> '
>
> +test_expect_success 'branch being rebased is not marked in its own worktree' '
> + test_when_finished "git -C rebase_dir rebase --abort; rm -rf rebase_dir; git worktree prune; git branch -D rebasing" &&
> + git worktree add -b rebasing rebase_dir branch-two &&
> + (
> + cd rebase_dir &&
> + git commit --allow-empty -m rebase-me &&
> + test_must_fail git rebase -f -x false HEAD~1 &&
> + git branch --list rebasing >actual &&
> + echo " rebasing" >expect &&
> + test_cmp expect actual &&
> + git branch -vv --list rebasing >actual &&
> + test_grep ! "$(pwd)" actual &&
> + git branch >actual &&
> + test_grep "^\* (no branch, rebasing rebasing)" actual
> + )
> +'
> +
> test_expect_success 'branch being bisected in a worktree is marked' '
> test_when_finished "git -C bisect_dir bisect reset; rm -rf bisect_dir; git worktree prune; git branch -D bisecting" &&
> git worktree add -b bisecting bisect_dir branch-two &&
> @@ -464,7 +481,10 @@ test_expect_success 'branch being bisected in a worktree is marked' '
> git commit --allow-empty -m bisect-one &&
> git commit --allow-empty -m bisect-two &&
> git bisect start HEAD HEAD~2 &&
> - test_must_fail git symbolic-ref -q HEAD
> + test_must_fail git symbolic-ref -q HEAD &&
> + git branch --list bisecting >actual &&
> + echo " bisecting" >expect &&
> + test_cmp expect actual
> ) &&
> git branch --list bisecting >actual &&
> echo "+ bisecting" >expect &&
^ permalink raw reply [flat|nested] 50+ messages in thread
* Re: [PATCH v4 0/4] worktree: treat a branch being rebased or bisected as checked out
2026-11-02 18:29 ` [PATCH v4 0/4] " Jasper Mayone
` (3 preceding siblings ...)
2026-11-02 18:29 ` [PATCH v4 4/4] branch: do not mark the branch the current worktree is rebasing Jasper Mayone
@ 2026-11-10 16:49 ` Phillip Wood
2026-11-11 15:28 ` [PATCH v5 0/5] " Jasper Mayone
5 siblings, 0 replies; 50+ messages in thread
From: Phillip Wood @ 2026-11-10 16:49 UTC (permalink / raw)
To: Jasper Mayone, git
Cc: gitster, sunshine, kristofferhaugsbakk, ben.knoble,
haraldnordgren
Hi Jasper
This is moving in the right direction, but I think we want to use a hash
to lookup which worktrees are checked out, rather than iterating over
all worktress for every ref that we list.
Thanks
Phillip
On 02/11/2026 18:29, Jasper Mayone wrote:
> While a worktree is rebasing or bisecting a branch, its HEAD is
> detached. "git worktree add" and "git branch -d" already treat the
> branch as checked out there, but "git worktree remove" deletes such a
> worktree when its files are clean, and %(worktreepath) and "git
> branch" do not show which worktree has the branch.
>
> Patch 1 makes "git worktree remove" refuse a worktree in the middle of
> an operation unless --force is given. Patch 2 documents how to read
> %(worktreepath) safely. Patch 3 makes %(worktreepath) name the
> worktree that is rebasing or bisecting a branch, and patch 4 keeps
> "git branch" from marking that branch with "+" in that worktree.
>
> Changes since v3 [1]:
>
> * Patch 1: also refuse a worktree that has CHERRY_PICK_HEAD or
> REVERT_HEAD, for a single cherry-pick or revert whose files are
> clean, with tests. The git-worktree documentation now lists the
> cases that need --force. The tests use a shared setup and
> subshells. Dropped the Helped-by trailer, as Phillip asked.
>
> * Patch 2 is new. It adds the notes that Phillip suggested to the
> documentation of %(worktreepath): the path can contain a newline,
> and only one worktree is shown for a branch that is checked out in
> more than one.
>
> * Patch 3: %(worktreepath) now uses a new branch.c function,
> branch_checked_out_worktree(), that reads the list that
> prepare_checked_out_branches() builds, in place of a copy of that
> code in ref-filter.c. ref-filter.c loses its own worktree map. The
> commit message explains why branches that "rebase --update-refs"
> will update are not included, and the change for the HEAD of a
> bare repository, which now has a test.
>
> * Patch 4 is new. "git branch" no longer marks a branch with "+" in
> the worktree that is rebasing or bisecting it, because the
> "(no branch, ...)" line already names it.
>
> [1] https://lore.kernel.org/git/20261030142137.54750-1-me@jaspermayone.com/
>
> Jasper Mayone (4):
> worktree remove: refuse a worktree in the middle of an operation
> doc: for-each-ref: say how to read %(worktreepath) safely
> ref-filter: name the worktree of a branch being rebased or bisected
> branch: do not mark the branch the current worktree is rebasing
>
> Documentation/git-branch.adoc | 6 +-
> Documentation/git-for-each-ref.adoc | 12 +++-
> Documentation/git-worktree.adoc | 17 +++--
> branch.c | 13 ++++
> branch.h | 9 +++
> builtin/branch.c | 103 +++++++++++++++++++++-------
> builtin/worktree.c | 37 +++++++++-
> ref-filter.c | 74 +-------------------
> t/t2403-worktree-move.sh | 94 +++++++++++++++++++++++++
> t/t3203-branch-output.sh | 49 +++++++++++++
> t/t6302-for-each-ref-filter.sh | 76 ++++++++++++++++++++
> 11 files changed, 386 insertions(+), 104 deletions(-)
>
> Range-diff against v3:
> 1: a79b69a0e4 ! 1: bca692b844 worktree remove: refuse a worktree in the middle of a rebase or bisect
> @@ Metadata
> Author: Jasper Mayone <me@jaspermayone.com>
>
> ## Commit message ##
> - worktree remove: refuse a worktree in the middle of a rebase or bisect
> + worktree remove: refuse a worktree in the middle of an operation
>
> - "git worktree remove" refuses a worktree with modified or untracked
> - files unless --force is given, but it removes a worktree that is in
> - the middle of a rebase, a "git am" session, a bisect or a cherry-pick
> - or revert sequence when its files happen to be clean. The state of
> - that operation lives in the worktree's own $GIT_DIR, so it is lost
> - with the worktree. A rebase stopped at an "edit" or "break" line, an
> - "exec" that failed, or a cherry-pick sequence after a conflict was
> - resolved and committed is clean in this sense and still has work in
> - progress.
> + "git worktree remove" refuses to remove a worktree with modified or
> + untracked files unless --force is given, but it removes a worktree
> + that is in the middle of a rebase, a "git am" session, a bisect, a
> + cherry-pick or a revert when its files happen to be clean. The state
> + of that operation lives in the worktree's own $GIT_DIR, so it is lost
> + with the worktree. Examples of such a clean worktree are a rebase
> + stopped at an "edit" or "break" line or after a failed "exec", a
> + cherry-pick sequence after a conflict was resolved and committed, and
> + a single cherry-pick or revert whose conflict was resolved to the
> + current contents, which leaves CHERRY_PICK_HEAD or REVERT_HEAD behind.
>
> Treat such a worktree as unclean: refuse to remove it unless --force
> is given, and name the operation in the message.
> @@ Commit message
> worktree, which is only done when the directory exists.
>
> Reported-by: D. Ben Knoble <ben.knoble@gmail.com>
> - Helped-by: Phillip Wood <phillip.wood123@gmail.com>
> Signed-off-by: Jasper Mayone <me@jaspermayone.com>
>
> ## Documentation/git-worktree.adoc ##
> @@ Documentation/git-worktree.adoc: the new working tree location.
> +
> `remove`::
>
> - Remove a worktree. Only clean worktrees (no untracked files and no
> +-Remove a worktree. Only clean worktrees (no untracked files and no
> -modification in tracked files) can be removed. Unclean worktrees or ones
> -with submodules can be removed with `--force`. The main worktree cannot be
> -removed.
> -+modification in tracked files) can be removed. A worktree in the middle
> -+of a rebase, a `git am` session, a bisect or a cherry-pick or revert
> -+sequence is not clean either, because removing it loses that state. Unclean worktrees or ones with submodules
> -+can be removed with `--force`. The main worktree cannot be removed.
> ++Remove a worktree. The main worktree cannot be removed. Without
> ++`--force`, a worktree is not removed when:
> +++
> ++--
> ++* it has untracked files or modifications in tracked files,
> ++* it has submodules, or
> ++* it is in the middle of a rebase, a `git am` session, a bisect, a
> ++ cherry-pick or a revert, because removing it loses the state of that
> ++ operation.
> ++--
>
> `repair [<path>...]`::
>
> @@ Documentation/git-worktree.adoc: twice. If the destination is already assigned t
> allows the move to proceed; use `--force` twice if the destination is locked.
> +
> -`remove` refuses to remove an unclean worktree unless `--force` is used.
> -+`remove` refuses to remove an unclean worktree, or one in the middle of a
> -+rebase, `git am` session, bisect, or cherry-pick or revert sequence,
> -+unless `--force` is used.
> ++`remove` refuses to remove a worktree in the cases listed under
> ++`remove` above unless `--force` is used.
> To remove a locked worktree, specify `--force` twice.
>
> `-b <new-branch>`::
> @@ builtin/worktree.c: static void check_clean_worktree(struct worktree *wt,
> }
>
> +/*
> -+ * A rebase, "git am", bisect or cherry-pick/revert sequence keeps its
> -+ * state in the worktree's own $GIT_DIR, and removing the worktree
> -+ * throws that away even when "git status" says the files are clean.
> ++ * A rebase, "git am", bisect, cherry-pick or revert keeps its state in
> ++ * the worktree's own $GIT_DIR, and removing the worktree throws that
> ++ * away even when "git status" says the files are clean.
> + */
> +static void check_no_operation_in_progress(struct worktree *wt,
> + const char *original_path)
> @@ builtin/worktree.c: static void check_clean_worktree(struct worktree *wt,
> + die(_("'%s' is in the middle of a bisect, use --force to delete it"),
> + original_path);
> + wt_status_state_free_buffers(&state);
> ++ if (refs_ref_exists(get_worktree_ref_store(wt), "CHERRY_PICK_HEAD"))
> ++ die(_("'%s' is in the middle of a cherry-pick, use --force to delete it"),
> ++ original_path);
> ++ if (refs_ref_exists(get_worktree_ref_store(wt), "REVERT_HEAD"))
> ++ die(_("'%s' is in the middle of a revert, use --force to delete it"),
> ++ original_path);
> + if (is_directory(worktree_git_path(wt, "sequencer")))
> + die(_("'%s' is in the middle of a cherry-pick or revert, use --force to delete it"),
> + original_path);
> @@ t/t2403-worktree-move.sh: test_expect_success 'move worktree with relative path
> test_cmp expect .git/worktrees/absolute/gitdir
> '
>
> ++test_expect_success 'setup commits that conflict for am, cherry-pick and revert' '
> ++ git worktree add --detach conflict-setup init &&
> ++ (
> ++ cd conflict-setup &&
> ++ test_commit base conflict.t base &&
> ++ git switch --detach init &&
> ++ test_commit side conflict.t side &&
> ++ test_commit side-two
> ++ ) &&
> ++ git worktree remove conflict-setup
> ++'
> ++
> +test_expect_success 'remove worktree in the middle of a rebase' '
> + test_when_finished "rm -rf mid-rebase; git worktree prune" &&
> -+ git worktree add --detach mid-rebase init &&
> -+ test_commit -C mid-rebase rebase-me &&
> ++ git worktree add --detach mid-rebase side &&
> + test_must_fail git -C mid-rebase rebase -f -x false init &&
> + test_must_fail git worktree remove mid-rebase 2>err &&
> + test_grep "in the middle of a rebase" err &&
> @@ t/t2403-worktree-move.sh: test_expect_success 'move worktree with relative path
> +
> +test_expect_success 'remove worktree in the middle of an am session' '
> + test_when_finished "rm -rf mid-am patch; git worktree prune" &&
> -+ git worktree add --detach mid-am init &&
> -+ test_commit -C mid-am am-me &&
> -+ git -C mid-am format-patch -1 --stdout >patch &&
> -+ git -C mid-am reset --hard init &&
> -+ echo conflict >mid-am/am-me.t &&
> -+ git -C mid-am add am-me.t &&
> -+ git -C mid-am commit -m conflict &&
> -+ test_must_fail git -C mid-am am ../patch &&
> -+ git -C mid-am reset --hard &&
> ++ git format-patch -1 --stdout side >patch &&
> ++ git worktree add --detach mid-am base &&
> ++ (
> ++ cd mid-am &&
> ++ test_must_fail git am ../patch &&
> ++ git reset --hard
> ++ ) &&
> + test_must_fail git worktree remove mid-am 2>err &&
> + test_grep "in the middle of an am session" err &&
> + git worktree remove --force mid-am
> @@ t/t2403-worktree-move.sh: test_expect_success 'move worktree with relative path
> +
> +test_expect_success 'remove worktree in the middle of a bisect' '
> + test_when_finished "rm -rf mid-bisect; git worktree prune" &&
> -+ git worktree add --detach mid-bisect init &&
> -+ test_commit -C mid-bisect bisect-one &&
> -+ test_commit -C mid-bisect bisect-two &&
> -+ git -C mid-bisect bisect start HEAD init &&
> ++ git worktree add --detach mid-bisect side-two &&
> ++ git -C mid-bisect bisect start side-two init &&
> + test_must_fail git worktree remove mid-bisect 2>err &&
> + test_grep "in the middle of a bisect" err &&
> + git worktree remove --force mid-bisect
> +'
> +
> -+test_expect_success 'remove worktree in the middle of a cherry-pick sequence' '
> ++test_expect_success 'remove worktree in the middle of a cherry-pick' '
> + test_when_finished "rm -rf mid-pick; git worktree prune" &&
> -+ git worktree add --detach mid-pick init &&
> -+ git -C mid-pick switch -c pick-src &&
> -+ test_commit -C mid-pick pick-one &&
> -+ test_commit -C mid-pick pick-two &&
> -+ git -C mid-pick switch --detach init &&
> -+ echo conflict >mid-pick/pick-one.t &&
> -+ git -C mid-pick add pick-one.t &&
> -+ git -C mid-pick commit -m conflict &&
> -+ test_must_fail git -C mid-pick cherry-pick pick-src~1 pick-src &&
> -+ git -C mid-pick checkout --theirs pick-one.t &&
> -+ git -C mid-pick add pick-one.t &&
> -+ git -C mid-pick commit --no-edit &&
> -+ test_path_is_dir .git/worktrees/mid-pick/sequencer &&
> ++ git worktree add --detach mid-pick base &&
> ++ (
> ++ cd mid-pick &&
> ++ test_must_fail git cherry-pick side &&
> ++ git checkout --ours conflict.t &&
> ++ git add conflict.t &&
> ++ git rev-parse --verify CHERRY_PICK_HEAD
> ++ ) &&
> ++ test_path_is_missing .git/worktrees/mid-pick/sequencer &&
> + test_must_fail git worktree remove mid-pick 2>err &&
> -+ test_grep "in the middle of a cherry-pick or revert" err &&
> ++ test_grep "in the middle of a cherry-pick," err &&
> + git worktree remove --force mid-pick
> +'
> ++
> ++test_expect_success 'remove worktree in the middle of a revert' '
> ++ test_when_finished "rm -rf mid-revert; git worktree prune" &&
> ++ git worktree add --detach mid-revert base &&
> ++ (
> ++ cd mid-revert &&
> ++ test_must_fail git revert side &&
> ++ git checkout --ours conflict.t &&
> ++ git add conflict.t &&
> ++ git rev-parse --verify REVERT_HEAD
> ++ ) &&
> ++ test_path_is_missing .git/worktrees/mid-revert/sequencer &&
> ++ test_must_fail git worktree remove mid-revert 2>err &&
> ++ test_grep "in the middle of a revert" err &&
> ++ git worktree remove --force mid-revert
> ++'
> ++
> ++test_expect_success 'remove worktree in the middle of a cherry-pick sequence' '
> ++ test_when_finished "rm -rf mid-pick-seq; git worktree prune" &&
> ++ git worktree add --detach mid-pick-seq base &&
> ++ (
> ++ cd mid-pick-seq &&
> ++ test_must_fail git cherry-pick side side-two &&
> ++ git checkout --theirs conflict.t &&
> ++ git add conflict.t &&
> ++ git commit --no-edit
> ++ ) &&
> ++ test_path_is_dir .git/worktrees/mid-pick-seq/sequencer &&
> ++ test_must_fail git worktree remove mid-pick-seq 2>err &&
> ++ test_grep "in the middle of a cherry-pick or revert" err &&
> ++ git worktree remove --force mid-pick-seq
> ++'
> +
> test_done
> -: ---------- > 2: 39eb92d441 doc: for-each-ref: say how to read %(worktreepath) safely
> 2: 0cd13347e1 ! 3: 990a19fd11 ref-filter: name the worktree of a branch being rebased or bisected
> @@ Commit message
>
> The rest of Git already treats that branch as checked out: "git
> worktree add" and "git branch -d" refuse it, because finishing the
> - rebase or bisect will check it out again. "git worktree list" shows
> - such a worktree as "(detached HEAD)", so %(worktreepath) is the only
> - place a script could find out which worktree is working on the
> - branch.
> + rebase or bisect will check it out again. But no command shows which
> + worktree is working on the branch: "git worktree list" shows such a
> + worktree as "(detached HEAD)", and %(worktreepath) is empty.
>
> - Also map the branch that a worktree is rebasing or bisecting to that
> - worktree, as prepare_checked_out_branches() in branch.c does.
> - Branches that "rebase --update-refs" will update are not mapped: that
> - worktree has not checked them out, it only moves them when the rebase
> - is done.
> + branch.c already knows the answer, as prepare_checked_out_branches()
> + records the branch that each worktree has checked out, is rebasing or
> + is bisecting. Add branch_checked_out_worktree() to look a branch up in
> + that list, and use it for %(worktreepath) in place of the map that
> + ref-filter.c builds from the HEAD of each worktree.
>
> - As a result "git branch" marks such a branch with "+", also when it
> - runs in the worktree that is rebasing or bisecting it, where it shows
> - both "* (no branch, rebasing topic)" and "+ topic". Document that.
> + The new function skips the branches that "rebase --update-refs" will
> + update. branch_checked_out() includes them, so that they cannot be
> + checked out or deleted during the rebase, but the worktree that runs
> + the rebase has not checked them out. %(worktreepath) therefore stays
> + empty for them, unlike branch_checked_out().
> +
> + This also changes the result for a bare repository: its HEAD names a
> + branch, and %(worktreepath) used to give the path of the bare
> + repository for that branch. "git branch" in a linked worktree marked
> + the branch with "+", although no worktree has it checked out and "git
> + worktree add" lets you check it out. prepare_checked_out_branches()
> + skips bare repositories, so %(worktreepath) is now empty for that
> + branch and "git branch" no longer marks it.
>
> Reported-by: Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com>
> Helped-by: Phillip Wood <phillip.wood123@gmail.com>
> @@ Documentation/git-branch.adoc: DESCRIPTION
> branches are listed; the current branch will be highlighted in green and
> marked with an asterisk. Any branches checked out in linked worktrees will
> -be highlighted in cyan and marked with a plus sign. Option `-r` causes the
> +-remote-tracking branches to be listed,
> +be highlighted in cyan and marked with a plus sign. A branch that a
> -+worktree is rebasing or bisecting counts as checked out there, so it is
> -+marked in the same way, also in the worktree that is rebasing or
> -+bisecting it. Option `-r` causes the
> - remote-tracking branches to be listed,
> ++worktree is rebasing or bisecting counts as checked out in that
> ++worktree, so the other worktrees mark it in the same way. Option `-r`
> ++causes the remote-tracking branches to be listed,
> and option `-a` shows both local and remote branches.
>
> + If a `<pattern>`
>
> ## Documentation/git-for-each-ref.adoc ##
> @@ Documentation/git-for-each-ref.adoc: but if used together the last option is selected.
> @@ Documentation/git-for-each-ref.adoc: but if used together the last option is sel
> - out, if it is checked out in any linked worktree. Empty string
> + out, if it is checked out in any linked worktree. A branch that
> + a worktree is rebasing or bisecting counts as checked out there,
> -+ although that worktree's `HEAD` is detached. Empty string
> - otherwise.
> -
> - `ahead-behind:<commit-ish>`::
> ++ although that worktree's `HEAD` is detached. A branch that
> ++ `git rebase --update-refs` will update does not. Empty string
> + otherwise. If the ref is checked out in more than one worktree,
> + only one of them is shown. The path can contain a newline, so
> + use one of the quoting options such as `--shell`, or follow
>
> - ## ref-filter.c ##
> -@@ ref-filter.c: static struct expand_data {
> -
> - struct ref_to_worktree_entry {
> - struct hashmap_entry ent;
> -- struct worktree *wt; /* key is wt->head_ref */
> -+ struct worktree *wt;
> -+ char refname[FLEX_ARRAY]; /* the key */
> - };
> -
> - static int ref_to_worktree_map_cmpfnc(const void *lookupdata UNUSED,
> -@@ ref-filter.c: static int ref_to_worktree_map_cmpfnc(const void *lookupdata UNUSED,
> - e = container_of(eptr, const struct ref_to_worktree_entry, ent);
> - k = container_of(kptr, const struct ref_to_worktree_entry, ent);
> -
> -- return strcmp(e->wt->head_ref,
> -- keydata_aka_refname ? keydata_aka_refname : k->wt->head_ref);
> -+ return strcmp(e->refname,
> -+ keydata_aka_refname ? keydata_aka_refname : k->refname);
> + ## branch.c ##
> +@@ branch.c: const char *branch_checked_out(const char *refname)
> + return strmap_get(¤t_checked_out_branches, refname);
> }
>
> - static struct ref_to_worktree_map {
> ++const char *branch_checked_out_worktree(const char *refname)
> ++{
> ++ const char *path = NULL;
> ++
> ++ prepare_checked_out_branches();
> ++ for (size_t i = 0; i < checked_out_branches_nr; i++) {
> ++ if (checked_out_branches[i].kind != BRANCH_CHECKOUT_KIND_UPDATE_REF &&
> ++ !strcmp(refname, checked_out_branches[i].refname))
> ++ path = checked_out_branches[i].path;
> ++ }
> ++ return path;
> ++}
> ++
> + const char *branch_bisecting(const char *refname)
> + {
> + prepare_checked_out_branches();
> +
> + ## branch.h ##
> +@@ branch.h: void create_branches_recursively(struct repository *r, const char *name,
> + */
> + const char *branch_checked_out(const char *refname);
> +
> ++/*
> ++ * If a worktree has the branch 'refname' checked out, or is rebasing or
> ++ * bisecting it, return the path of that worktree, or of the last one if
> ++ * there are more. Otherwise return NULL. Unlike branch_checked_out(),
> ++ * this ignores the branches that "git rebase --update-refs" will
> ++ * update, because no worktree has them checked out.
> ++ */
> ++const char *branch_checked_out_worktree(const char *refname);
> ++
> + /*
> + * If the branch at 'refname' is currently used for bisecting in a
> + * worktree, then return the path to that worktree.
> +
> + ## ref-filter.c ##
> +@@
> + #define DISABLE_SIGN_COMPARE_WARNINGS
> +
> + #include "git-compat-util.h"
> ++#include "branch.h"
> + #include "environment.h"
> + #include "gettext.h"
> + #include "config.h"
> +@@ ref-filter.c: static struct expand_data {
> + struct object_info info;
> + } oi, oi_deref;
> +
> +-struct ref_to_worktree_entry {
> +- struct hashmap_entry ent;
> +- struct worktree *wt; /* key is wt->head_ref */
> +-};
> +-
> +-static int ref_to_worktree_map_cmpfnc(const void *lookupdata UNUSED,
> +- const struct hashmap_entry *eptr,
> +- const struct hashmap_entry *kptr,
> +- const void *keydata_aka_refname)
> +-{
> +- const struct ref_to_worktree_entry *e, *k;
> +-
> +- e = container_of(eptr, const struct ref_to_worktree_entry, ent);
> +- k = container_of(kptr, const struct ref_to_worktree_entry, ent);
> +-
> +- return strcmp(e->wt->head_ref,
> +- keydata_aka_refname ? keydata_aka_refname : k->wt->head_ref);
> +-}
> +-
> +-static struct ref_to_worktree_map {
> +- struct hashmap map;
> +- struct worktree **worktrees;
> +-} ref_to_worktree_map;
> +-
> + /*
> + * The enum atom_type is used as the index of valid_atom array.
> + * In the atom parsing stage, it will be passed to used_atom.atom_type
> @@ ref-filter.c: static int get_object(struct ref_array_item *ref, int deref,
> return ret;
> }
>
> -+static void add_worktree_entry(struct hashmap *map, struct worktree *wt,
> -+ const char *refname)
> -+{
> -+ struct ref_to_worktree_entry *entry;
> -+
> -+ FLEX_ALLOC_STR(entry, refname, refname);
> -+ entry->wt = wt;
> -+ hashmap_entry_init(&entry->ent, strhash(refname));
> -+ hashmap_add(map, &entry->ent);
> -+}
> -+
> -+/*
> -+ * Map each branch to the worktree that has it checked out. A worktree
> -+ * that is rebasing or bisecting a branch has it checked out, too,
> -+ * even though its HEAD may be detached, as for
> -+ * prepare_checked_out_branches() in branch.c.
> -+ */
> - static void populate_worktree_map(struct hashmap *map, struct worktree **worktrees)
> - {
> +-static void populate_worktree_map(struct hashmap *map, struct worktree **worktrees)
> +-{
> - int i;
> -+ struct strbuf ref = STRBUF_INIT;
> -
> +-
> - for (i = 0; worktrees[i]; i++) {
> - if (worktrees[i]->head_ref) {
> - struct ref_to_worktree_entry *entry;
> @@ ref-filter.c: static int get_object(struct ref_array_item *ref, int deref,
> - entry->wt = worktrees[i];
> - hashmap_entry_init(&entry->ent,
> - strhash(worktrees[i]->head_ref));
> -+ for (size_t i = 0; worktrees[i]; i++) {
> -+ struct worktree *wt = worktrees[i];
> -+ struct wt_status_state state = { 0 };
> -
> +-
> - hashmap_add(map, &entry->ent);
> -+ if (wt->is_bare)
> -+ continue;
> -+
> -+ if (wt->head_ref)
> -+ add_worktree_entry(map, wt, wt->head_ref);
> -+
> -+ if (wt_status_check_rebase(wt, &state) &&
> -+ (state.rebase_in_progress ||
> -+ state.rebase_interactive_in_progress) &&
> -+ state.branch) {
> -+ strbuf_reset(&ref);
> -+ strbuf_addf(&ref, "refs/heads/%s", state.branch);
> -+ add_worktree_entry(map, wt, ref.buf);
> - }
> -+ wt_status_state_free_buffers(&state);
> -+
> -+ if (wt_status_check_bisect(wt, &state) &&
> -+ state.bisecting_from) {
> -+ strbuf_reset(&ref);
> -+ strbuf_addf(&ref, "refs/heads/%s", state.bisecting_from);
> -+ add_worktree_entry(map, wt, ref.buf);
> -+ }
> -+ wt_status_state_free_buffers(&state);
> - }
> -+ strbuf_release(&ref);
> +- }
> +- }
> +-}
> +-
> +-static void lazy_init_worktree_map(void)
> +-{
> +- if (ref_to_worktree_map.worktrees)
> +- return;
> +-
> +- ref_to_worktree_map.worktrees = get_worktrees(the_repository);
> +- hashmap_init(&(ref_to_worktree_map.map), ref_to_worktree_map_cmpfnc, NULL, 0);
> +- populate_worktree_map(&(ref_to_worktree_map.map), ref_to_worktree_map.worktrees);
> +-}
> +-
> + static char *get_worktree_path(const struct ref_array_item *ref)
> + {
> +- struct hashmap_entry entry, *e;
> +- struct ref_to_worktree_entry *lookup_result;
> ++ const char *path = branch_checked_out_worktree(ref->refname);
> +
> +- lazy_init_worktree_map();
> +-
> +- hashmap_entry_init(&entry, strhash(ref->refname));
> +- e = hashmap_get(&(ref_to_worktree_map.map), &entry, ref->refname);
> +-
> +- if (!e)
> +- return xstrdup("");
> +-
> +- lookup_result = container_of(e, struct ref_to_worktree_entry, ent);
> +-
> +- return xstrdup(lookup_result->wt->path);
> ++ return xstrdup(path ? path : "");
> + }
> +
> + /*
> +@@ ref-filter.c: void ref_array_clear(struct ref_array *array)
> + FREE_AND_NULL(used_atom);
> + used_atom_cnt = 0;
> +
> +- if (ref_to_worktree_map.worktrees) {
> +- hashmap_clear_and_free(&(ref_to_worktree_map.map),
> +- struct ref_to_worktree_entry, ent);
> +- free_worktrees(ref_to_worktree_map.worktrees);
> +- ref_to_worktree_map.worktrees = NULL;
> +- }
> +-
> + FREE_AND_NULL(array->counts);
> }
>
> - static void lazy_init_worktree_map(void)
>
> ## t/t3203-branch-output.sh ##
> @@ t/t3203-branch-output.sh: test_expect_success 'worktree colors correct' '
> @@ t/t3203-branch-output.sh: test_expect_success 'worktree colors correct' '
> +test_expect_success 'branch being rebased in a worktree is marked' '
> + test_when_finished "git -C rebase_dir rebase --abort; rm -rf rebase_dir; git worktree prune; git branch -D rebasing" &&
> + git worktree add -b rebasing rebase_dir branch-two &&
> -+ git -C rebase_dir commit --allow-empty -m rebase-me &&
> -+ test_must_fail git -C rebase_dir rebase -f -x false HEAD~1 &&
> -+ test_must_fail git -C rebase_dir symbolic-ref -q HEAD &&
> ++ (
> ++ cd rebase_dir &&
> ++ git commit --allow-empty -m rebase-me &&
> ++ test_must_fail git rebase -f -x false HEAD~1 &&
> ++ test_must_fail git symbolic-ref -q HEAD
> ++ ) &&
> + git branch --list rebasing >actual &&
> + echo "+ rebasing" >expect &&
> + test_cmp expect actual
> +'
> +
> -+test_expect_success 'branch being rebased is marked in its own worktree' '
> -+ test_when_finished "git -C rebase_dir rebase --abort; rm -rf rebase_dir; git worktree prune; git branch -D rebasing" &&
> -+ git worktree add -b rebasing rebase_dir branch-two &&
> -+ git -C rebase_dir commit --allow-empty -m rebase-me &&
> -+ test_must_fail git -C rebase_dir rebase -f -x false HEAD~1 &&
> -+ git -C rebase_dir branch --list rebasing >actual &&
> -+ echo "+ rebasing" >expect &&
> -+ test_cmp expect actual &&
> -+ git -C rebase_dir branch >actual &&
> -+ test_grep "^\* (no branch, rebasing rebasing)" actual
> -+'
> -+
> +test_expect_success 'branch being bisected in a worktree is marked' '
> + test_when_finished "git -C bisect_dir bisect reset; rm -rf bisect_dir; git worktree prune; git branch -D bisecting" &&
> + git worktree add -b bisecting bisect_dir branch-two &&
> -+ git -C bisect_dir commit --allow-empty -m bisect-one &&
> -+ git -C bisect_dir commit --allow-empty -m bisect-two &&
> -+ git -C bisect_dir bisect start HEAD HEAD~2 &&
> -+ test_must_fail git -C bisect_dir symbolic-ref -q HEAD &&
> ++ (
> ++ cd bisect_dir &&
> ++ git commit --allow-empty -m bisect-one &&
> ++ git commit --allow-empty -m bisect-two &&
> ++ git bisect start HEAD HEAD~2 &&
> ++ test_must_fail git symbolic-ref -q HEAD
> ++ ) &&
> + git branch --list bisecting >actual &&
> + echo "+ bisecting" >expect &&
> + test_cmp expect actual
> @@ t/t6302-for-each-ref-filter.sh: test_expect_success 'validate worktree atom' '
> + git for-each-ref --format="%(worktreepath)" refs/heads/ >actual &&
> + test_grep ! detached-rebase actual
> +'
> ++
> ++test_expect_success 'worktree atom ignores the HEAD of a bare repository' '
> ++ test_when_finished "rm -rf bare.git bare-wt" &&
> ++ git clone --bare . bare.git &&
> ++ git -C bare.git symbolic-ref HEAD refs/heads/main &&
> ++ git -C bare.git worktree add --detach ../bare-wt main &&
> ++ git -C bare.git for-each-ref --format="%(worktreepath)" refs/heads/main >actual &&
> ++ echo >expect &&
> ++ test_cmp expect actual &&
> ++ git -C bare-wt branch --list main >actual &&
> ++ echo " main" >expect &&
> ++ test_cmp expect actual
> ++'
> +
> test_expect_success 'start after with empty value' '
> cat >expect <<-\EOF &&
> -: ---------- > 4: 56e29c448f branch: do not mark the branch the current worktree is rebasing
^ permalink raw reply [flat|nested] 50+ messages in thread* [PATCH v5 0/5] worktree: treat a branch being rebased or bisected as checked out
2026-11-02 18:29 ` [PATCH v4 0/4] " Jasper Mayone
` (4 preceding siblings ...)
2026-11-10 16:49 ` [PATCH v4 0/4] worktree: treat a branch being rebased or bisected as checked out Phillip Wood
@ 2026-11-11 15:28 ` Jasper Mayone
2026-11-11 15:28 ` [PATCH v5 1/5] worktree remove: refuse a worktree in the middle of an operation Jasper Mayone
` (5 more replies)
5 siblings, 6 replies; 50+ messages in thread
From: Jasper Mayone @ 2026-11-11 15:28 UTC (permalink / raw)
To: git
Cc: gitster, phillip.wood123, sunshine, kristofferhaugsbakk,
ben.knoble, haraldnordgren, Jasper Mayone
While a worktree is rebasing or bisecting a branch, its HEAD is
detached. "git worktree add" and "git branch -d" already treat the
branch as checked out there, but "git worktree remove" deletes such a
worktree when its files are clean, and %(worktreepath) and "git
branch" do not show which worktree has the branch.
Patch 1 makes "git worktree remove" refuse a worktree in the middle of
an operation unless --force is given. Patch 2 documents how to read
%(worktreepath) safely. Patch 3 makes %(worktreepath) name the
worktree that is rebasing or bisecting a branch. Patch 4 adds
%(currentworktree), and patch 5 uses it so that "git branch" does not
mark that branch with "+" in the worktree that is rebasing it.
Changes since v4 [1]:
* Patch 1: each item in the list of cases that need --force is now a
complete sentence, and all the commands are in the same font, as
Phillip asked. Ben was happy with the old wording, and I hope this
suits both of you.
* Patch 2: no change.
* Patch 3: branch.c records the branches in a map, so
branch_checked_out_worktree() is one map lookup in place of a loop
over every worktree for each ref.
* Patch 4 is new. It adds %(currentworktree), which Phillip
suggested. It uses a second map in branch.c for the branches that
the current worktree has.
* Patch 5 replaces v4's patch 4. The default "git branch" format now
uses %(currentworktree), so print_ref_list() is unchanged and the
patch is a few lines in build_format().
[1] https://lore.kernel.org/git/20261102182930.3511-1-me@jaspermayone.com/
Jasper Mayone (5):
worktree remove: refuse a worktree in the middle of an operation
doc: for-each-ref: say how to read %(worktreepath) safely
ref-filter: name the worktree of a branch being rebased or bisected
ref-filter: add %(currentworktree)
branch: do not mark the branch the current worktree is rebasing
Documentation/git-branch.adoc | 6 +-
Documentation/git-for-each-ref.adoc | 19 +++++-
Documentation/git-worktree.adoc | 17 +++--
branch.c | 33 ++++++++--
branch.h | 16 +++++
builtin/branch.c | 10 ++-
builtin/worktree.c | 37 ++++++++++-
ref-filter.c | 84 ++++---------------------
t/t2403-worktree-move.sh | 94 ++++++++++++++++++++++++++++
t/t3203-branch-output.sh | 49 +++++++++++++++
t/t6302-for-each-ref-filter.sh | 97 +++++++++++++++++++++++++++++
11 files changed, 373 insertions(+), 89 deletions(-)
Range-diff against v4:
1: bca692b844 ! 1: 69668bf9b1 worktree remove: refuse a worktree in the middle of an operation
@@ Documentation/git-worktree.adoc: the new working tree location.
-with submodules can be removed with `--force`. The main worktree cannot be
-removed.
+Remove a worktree. The main worktree cannot be removed. Without
-+`--force`, a worktree is not removed when:
++`--force`, a worktree is not removed in these cases:
++
+--
-+* it has untracked files or modifications in tracked files,
-+* it has submodules, or
-+* it is in the middle of a rebase, a `git am` session, a bisect, a
-+ cherry-pick or a revert, because removing it loses the state of that
-+ operation.
++* The worktree has untracked files or modifications in tracked files.
++* The worktree has submodules.
++* The worktree is in the middle of a `git rebase`, `git am`,
++ `git bisect`, `git cherry-pick` or `git revert`. Removing the
++ worktree would lose the state of that operation.
+--
`repair [<path>...]`::
2: 39eb92d441 = 2: 35035d30e2 doc: for-each-ref: say how to read %(worktreepath) safely
3: 990a19fd11 ! 3: 4484dfff22 ref-filter: name the worktree of a branch being rebased or bisected
@@ Commit message
branch.c already knows the answer, as prepare_checked_out_branches()
records the branch that each worktree has checked out, is rebasing or
- is bisecting. Add branch_checked_out_worktree() to look a branch up in
- that list, and use it for %(worktreepath) in place of the map that
- ref-filter.c builds from the HEAD of each worktree.
+ is bisecting. Record those branches in a map as well, add
+ branch_checked_out_worktree() to look a branch up in it, and use that
+ for %(worktreepath) in place of the map that ref-filter.c builds from
+ the HEAD of each worktree.
The new function skips the branches that "rebase --update-refs" will
update. branch_checked_out() includes them, so that they cannot be
@@ Documentation/git-for-each-ref.adoc: but if used together the last option is sel
use one of the quoting options such as `--shell`, or follow
## branch.c ##
+@@ branch.c: int validate_branchname(const char *name, struct strbuf *ref)
+
+ static int initialized_checked_out_branches;
+ static struct strmap current_checked_out_branches = STRMAP_INIT;
++/* Like current_checked_out_branches, without the "update-ref" branches. */
++static struct strmap checked_out_worktrees = STRMAP_INIT;
+
+ enum branch_checkout_kind {
+ BRANCH_CHECKOUT_KIND_CHECKOUT,
+@@ branch.c: static void register_checked_out_branch(const char *prefix, const char *name,
+ checked_out_branches_nr++;
+
+ strmap_put(¤t_checked_out_branches, refname, path_copy);
++ if (kind != BRANCH_CHECKOUT_KIND_UPDATE_REF)
++ strmap_put(&checked_out_worktrees, refname, path_copy);
+ }
+
+ static void prepare_checked_out_branches(void)
@@ branch.c: const char *branch_checked_out(const char *refname)
return strmap_get(¤t_checked_out_branches, refname);
}
+const char *branch_checked_out_worktree(const char *refname)
+{
-+ const char *path = NULL;
-+
+ prepare_checked_out_branches();
-+ for (size_t i = 0; i < checked_out_branches_nr; i++) {
-+ if (checked_out_branches[i].kind != BRANCH_CHECKOUT_KIND_UPDATE_REF &&
-+ !strcmp(refname, checked_out_branches[i].refname))
-+ path = checked_out_branches[i].path;
-+ }
-+ return path;
++ return strmap_get(&checked_out_worktrees, refname);
+}
+
const char *branch_bisecting(const char *refname)
4: 56e29c448f ! 4: 944ce70324 branch: do not mark the branch the current worktree is rebasing
@@ Metadata
Author: Jasper Mayone <me@jaspermayone.com>
## Commit message ##
- branch: do not mark the branch the current worktree is rebasing
+ ref-filter: add %(currentworktree)
- Since the previous commit, "git branch" marks a branch with "+" when a
- worktree is rebasing or bisecting it. In that worktree itself, the
- list already starts with "* (no branch, rebasing topic)", so the
- "+ topic" line below it says the same thing twice.
+ %(worktreepath) names the worktree that has a branch checked out, or
+ is rebasing or bisecting it, but a format cannot tell if that worktree
+ is the current one. Comparing %(worktreepath) with a fixed path in
+ %(if:equals=...) does not work for every path, because a ")" ends the
+ atom.
- The default format cannot tell that apart: %(worktreepath) gives the
- same path either way, and comparing it with %(if:equals=...) is not
- safe because a path, or a branch name, can contain the ")" that ends
- the atom. So build a second format that has no "+" marker and no
- "(path)" for -vv, and use it for each branch that
- branch_checked_out_worktree() maps to the current worktree. A branch
- that is checked out normally in the current worktree also gets that
- format, but %(HEAD) still marks it with "*" and -vv already hides
- its path, so its output does not change.
+ Add %(currentworktree), which gives the path of the current worktree
+ when that worktree has the branch, and an empty string otherwise.
+ branch.c records these branches when it reads the worktrees, in a
+ second map next to the one that %(worktreepath) uses, so the lookup is
+ one map lookup per ref.
- To pick a format for each branch, print_ref_list() now formats the
- branches itself in place of print_formatted_ref_array(), keeping
- --omit-empty. A format given with --format is used for every branch,
- as before.
+ The next commit uses it in "git branch".
Suggested-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Jasper Mayone <me@jaspermayone.com>
- ## builtin/branch.c ##
-@@ builtin/branch.c: static const char *quote_literal_for_format(const char *s)
- return buf.buf;
- }
+ ## Documentation/git-for-each-ref.adoc ##
+@@ Documentation/git-for-each-ref.adoc: but if used together the last option is selected.
+ the `NUL` after the path and then for the `LF` that ends each
+ record.
--static char *build_format(struct ref_filter *filter, int maxwidth, const char *remote_prefix)
-+/*
-+ * With "show_worktree" unset, the format does not mark a branch that is
-+ * checked out in a worktree, nor show the path of that worktree. It is
-+ * used for a branch that the current worktree is rebasing or bisecting,
-+ * because the "(no branch, ...)" line already names it.
-+ */
-+static char *build_format(struct ref_filter *filter, int maxwidth,
-+ const char *remote_prefix, int show_worktree)
- {
- struct strbuf fmt = STRBUF_INIT;
- struct strbuf local = STRBUF_INIT;
- struct strbuf remote = STRBUF_INIT;
-
-- strbuf_addf(&local, "%%(if)%%(HEAD)%%(then)* %s%%(else)%%(if)%%(worktreepath)%%(then)+ %s%%(else) %s%%(end)%%(end)",
-- branch_get_color(BRANCH_COLOR_CURRENT),
-- branch_get_color(BRANCH_COLOR_WORKTREE),
-- branch_get_color(BRANCH_COLOR_LOCAL));
-+ if (show_worktree)
-+ strbuf_addf(&local, "%%(if)%%(HEAD)%%(then)* %s%%(else)%%(if)%%(worktreepath)%%(then)+ %s%%(else) %s%%(end)%%(end)",
-+ branch_get_color(BRANCH_COLOR_CURRENT),
-+ branch_get_color(BRANCH_COLOR_WORKTREE),
-+ branch_get_color(BRANCH_COLOR_LOCAL));
-+ else
-+ strbuf_addf(&local, "%%(if)%%(HEAD)%%(then)* %s%%(else) %s%%(end)",
-+ branch_get_color(BRANCH_COLOR_CURRENT),
-+ branch_get_color(BRANCH_COLOR_LOCAL));
- strbuf_addf(&remote, " %s",
- branch_get_color(BRANCH_COLOR_REMOTE));
-
-@@ builtin/branch.c: static char *build_format(struct ref_filter *filter, int maxwidth, const char *r
-
- if (filter->verbose > 1)
- {
-- strbuf_addf(&local, "%%(if:notequals=*)%%(HEAD)%%(then)%%(if)%%(worktreepath)%%(then)(%s%%(worktreepath)%s) %%(end)%%(end)",
-- branch_get_color(BRANCH_COLOR_WORKTREE), branch_get_color(BRANCH_COLOR_RESET));
-+ if (show_worktree)
-+ strbuf_addf(&local, "%%(if:notequals=*)%%(HEAD)%%(then)%%(if)%%(worktreepath)%%(then)(%s%%(worktreepath)%s) %%(end)%%(end)",
-+ branch_get_color(BRANCH_COLOR_WORKTREE), branch_get_color(BRANCH_COLOR_RESET));
- strbuf_addf(&local, "%%(if)%%(upstream)%%(then)[%s%%(upstream:short)%s%%(if)%%(upstream:track)"
- "%%(then): %%(upstream:track,nobracket)%%(end)] %%(end)%%(contents:subject)",
- branch_get_color(BRANCH_COLOR_UPSTREAM), branch_get_color(BRANCH_COLOR_RESET));
-@@ builtin/branch.c: static char *build_format(struct ref_filter *filter, int maxwidth, const char *r
- return strbuf_detach(&fmt, NULL);
- }
-
-+static char *current_worktree_path(void)
-+{
-+ struct worktree **worktrees = get_worktrees(the_repository);
-+ char *path = NULL;
++`currentworktree`::
++ The absolute path to the current worktree, if the ref is checked
++ out there, in the same sense as for `worktreepath`. Empty string
++ otherwise, also when the ref is checked out only in other
++ worktrees. Use it with `%(if)` to treat the current worktree
++ differently from the others.
+
-+ for (size_t i = 0; worktrees[i]; i++) {
-+ if (worktrees[i]->is_current) {
-+ path = xstrdup(worktrees[i]->path);
-+ break;
-+ }
-+ }
-+ free_worktrees(worktrees);
-+ return path;
-+}
-+
-+/*
-+ * Is "refname" checked out in the worktree at "here", or is that
-+ * worktree rebasing or bisecting it?
-+ */
-+static int checked_out_here(const char *refname, const char *here)
-+{
-+ const char *path = branch_checked_out_worktree(refname);
-+
-+ return path && !strcmp(path, here);
-+}
-+
- static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sorting,
- struct ref_format *format, struct string_list *output)
- {
-@@ builtin/branch.c: static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sortin
- int maxwidth = 0;
- const char *remote_prefix = "";
- char *to_free = NULL;
-+ struct ref_format here_format = REF_FORMAT_INIT;
-+ char *here_to_free = NULL, *here = NULL;
-+ struct strbuf out = STRBUF_INIT, err = STRBUF_INIT;
-
- /*
- * If we are listing more than just remote branches,
-@@ builtin/branch.c: static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sortin
- if (filter->verbose)
- maxwidth = calc_maxwidth(&array, strlen(remote_prefix));
-
-- if (!format->format)
-- format->format = to_free = build_format(filter, maxwidth, remote_prefix);
-+ if (!format->format) {
-+ format->format = to_free = build_format(filter, maxwidth,
-+ remote_prefix, 1);
-+ here = current_worktree_path();
-+ here_format = *format;
-+ here_format.format = here_to_free = build_format(filter, maxwidth,
-+ remote_prefix, 0);
-+ }
- format->use_color = branch_use_color;
-+ here_format.use_color = branch_use_color;
-
-- if (verify_ref_format(format))
-+ if (verify_ref_format(format) ||
-+ (here && verify_ref_format(&here_format)))
- die(_("unable to parse format string"));
-
- filter_ahead_behind(the_repository, &array);
- ref_array_sort(sorting, &array);
-
-- if (column_active(colopts)) {
-- struct strbuf out = STRBUF_INIT, err = STRBUF_INIT;
--
-+ if (column_active(colopts))
- assert(!filter->verbose && "--column and --verbose are incompatible");
-
-- for (i = 0; i < array.nr; i++) {
-- strbuf_reset(&err);
-- strbuf_reset(&out);
-- if (format_ref_array_item(array.items[i], format, &out, &err))
-- die("%s", err.buf);
-+ for (i = 0; i < array.nr; i++) {
-+ struct ref_array_item *item = array.items[i];
-+ struct ref_format *f = format;
-
-+ if (here && checked_out_here(item->refname, here))
-+ f = &here_format;
-+
-+ strbuf_reset(&err);
-+ strbuf_reset(&out);
-+ if (format_ref_array_item(item, f, &out, &err))
-+ die("%s", err.buf);
-+
-+ if (column_active(colopts))
- /* format to a string_list to let print_columns() do its job */
- string_list_append(output, out.buf);
-+ else if (out.len || !format->array_opts.omit_empty) {
-+ fwrite(out.buf, 1, out.len, stdout);
-+ putchar('\n');
- }
--
-- strbuf_release(&err);
-- strbuf_release(&out);
-- } else {
-- print_formatted_ref_array(&array, format);
- }
-
-+ strbuf_release(&err);
-+ strbuf_release(&out);
- ref_array_clear(&array);
- free(to_free);
-+ free(here_to_free);
-+ free(here);
- }
-
- static void print_current_branch_name(void)
+ `ahead-behind:<commit-ish>`::
+ Two integers, separated by a space, demonstrating the number of
+ commits ahead and behind, respectively, when comparing the output
- ## t/t3203-branch-output.sh ##
-@@ t/t3203-branch-output.sh: test_expect_success 'branch being rebased in a worktree is marked' '
+ ## branch.c ##
+@@ branch.c: static int initialized_checked_out_branches;
+ static struct strmap current_checked_out_branches = STRMAP_INIT;
+ /* Like current_checked_out_branches, without the "update-ref" branches. */
+ static struct strmap checked_out_worktrees = STRMAP_INIT;
++/* The subset of checked_out_worktrees that the current worktree has. */
++static struct strmap current_worktree_branches = STRMAP_INIT;
+
+ enum branch_checkout_kind {
+ BRANCH_CHECKOUT_KIND_CHECKOUT,
+@@ branch.c: static struct checked_out_branch *checked_out_branches;
+ static size_t checked_out_branches_alloc, checked_out_branches_nr;
+
+ static void register_checked_out_branch(const char *prefix, const char *name,
+- const char *path,
++ const struct worktree *wt,
+ enum branch_checkout_kind kind)
+ {
+ char *refname = xstrfmt("%s%s", prefix, name);
+- char *path_copy = xstrdup(path);
++ char *path_copy = xstrdup(wt->path);
+
+ ALLOC_GROW(checked_out_branches, checked_out_branches_nr + 1,
+ checked_out_branches_alloc);
+@@ branch.c: static void register_checked_out_branch(const char *prefix, const char *name,
+ checked_out_branches_nr++;
+
+ strmap_put(¤t_checked_out_branches, refname, path_copy);
+- if (kind != BRANCH_CHECKOUT_KIND_UPDATE_REF)
++ if (kind != BRANCH_CHECKOUT_KIND_UPDATE_REF) {
+ strmap_put(&checked_out_worktrees, refname, path_copy);
++ if (wt->is_current)
++ strmap_put(¤t_worktree_branches, refname, path_copy);
++ }
+ }
+
+ static void prepare_checked_out_branches(void)
+@@ branch.c: static void prepare_checked_out_branches(void)
+ continue;
+
+ if (wt->head_ref) {
+- register_checked_out_branch("", wt->head_ref, wt->path,
++ register_checked_out_branch("", wt->head_ref, wt,
+ BRANCH_CHECKOUT_KIND_CHECKOUT);
+ }
+
+@@ branch.c: static void prepare_checked_out_branches(void)
+ (state.rebase_in_progress || state.rebase_interactive_in_progress) &&
+ state.branch) {
+ register_checked_out_branch("refs/heads/", state.branch,
+- wt->path,
++ wt,
+ BRANCH_CHECKOUT_KIND_REBASE);
+ }
+ wt_status_state_free_buffers(&state);
+@@ branch.c: static void prepare_checked_out_branches(void)
+ state.bisecting_from) {
+ register_checked_out_branch("refs/heads/",
+ state.bisecting_from,
+- wt->path,
++ wt,
+ BRANCH_CHECKOUT_KIND_BISECT);
+ }
+ wt_status_state_free_buffers(&state);
+@@ branch.c: static void prepare_checked_out_branches(void)
+ struct string_list_item *item;
+ for_each_string_list_item(item, &update_refs) {
+ register_checked_out_branch("", item->string,
+- wt->path,
++ wt,
+ BRANCH_CHECKOUT_KIND_UPDATE_REF);
+ }
+ string_list_clear(&update_refs, 1);
+@@ branch.c: const char *branch_checked_out_worktree(const char *refname)
+ return strmap_get(&checked_out_worktrees, refname);
+ }
+
++const char *branch_checked_out_current_worktree(const char *refname)
++{
++ prepare_checked_out_branches();
++ return strmap_get(¤t_worktree_branches, refname);
++}
++
+ const char *branch_bisecting(const char *refname)
+ {
+ prepare_checked_out_branches();
+
+ ## branch.h ##
+@@ branch.h: const char *branch_checked_out(const char *refname);
+ */
+ const char *branch_checked_out_worktree(const char *refname);
+
++/*
++ * Like branch_checked_out_worktree(), but only for the current
++ * worktree: return its path if it has the branch 'refname' checked
++ * out, or is rebasing or bisecting it. Otherwise return NULL.
++ */
++const char *branch_checked_out_current_worktree(const char *refname);
++
+ /*
+ * If the branch at 'refname' is currently used for bisecting in a
+ * worktree, then return the path to that worktree.
+
+ ## ref-filter.c ##
+@@ ref-filter.c: enum atom_type {
+ ATOM_HEAD,
+ ATOM_COLOR,
+ ATOM_WORKTREEPATH,
++ ATOM_CURRENTWORKTREE,
+ ATOM_ALIGN,
+ ATOM_END,
+ ATOM_IF,
+@@ ref-filter.c: static struct {
+ [ATOM_HEAD] = { "HEAD", SOURCE_NONE, FIELD_STR, head_atom_parser },
+ [ATOM_COLOR] = { "color", SOURCE_NONE, FIELD_STR, color_atom_parser },
+ [ATOM_WORKTREEPATH] = { "worktreepath", SOURCE_NONE },
++ [ATOM_CURRENTWORKTREE] = { "currentworktree", SOURCE_NONE },
+ [ATOM_ALIGN] = { "align", SOURCE_NONE, FIELD_STR, align_atom_parser },
+ [ATOM_END] = { "end", SOURCE_NONE },
+ [ATOM_IF] = { "if", SOURCE_NONE, FIELD_STR, if_atom_parser },
+@@ ref-filter.c: static int populate_value(struct ref_array_item *ref, struct strbuf *err)
+ v->s = xstrdup("");
+ continue;
+ }
++ else if (atom_type == ATOM_CURRENTWORKTREE) {
++ const char *path = NULL;
++
++ if (ref->kind == FILTER_REFS_BRANCHES)
++ path = branch_checked_out_current_worktree(ref->refname);
++ v->s = xstrdup(path ? path : "");
++ continue;
++ }
+ else if (atom_type == ATOM_SYMREF)
+ refname = get_symref(atom, ref);
+ else if (atom_type == ATOM_UPSTREAM) {
+
+ ## t/t6302-for-each-ref-filter.sh ##
+@@ t/t6302-for-each-ref-filter.sh: test_expect_success 'worktree atom ignores the HEAD of a bare repository' '
test_cmp expect actual
'
-+test_expect_success 'branch being rebased is not marked in its own worktree' '
-+ test_when_finished "git -C rebase_dir rebase --abort; rm -rf rebase_dir; git worktree prune; git branch -D rebasing" &&
-+ git worktree add -b rebasing rebase_dir branch-two &&
-+ (
-+ cd rebase_dir &&
-+ git commit --allow-empty -m rebase-me &&
-+ test_must_fail git rebase -f -x false HEAD~1 &&
-+ git branch --list rebasing >actual &&
-+ echo " rebasing" >expect &&
-+ test_cmp expect actual &&
-+ git branch -vv --list rebasing >actual &&
-+ test_grep ! "$(pwd)" actual &&
-+ git branch >actual &&
-+ test_grep "^\* (no branch, rebasing rebasing)" actual
-+ )
++test_expect_success 'currentworktree atom' '
++ test_when_finished "git -C current rebase --abort; rm -rf current; git worktree prune; git branch -D current" &&
++ git worktree add -b current current main &&
++ git -C current commit --allow-empty -m rebase-me &&
++ test_must_fail git -C current rebase -f -x false main &&
++ git for-each-ref --format="%(refname:short) [%(currentworktree)]" \
++ refs/heads/main refs/heads/current >actual &&
++ cat >expect <<-EOF &&
++ current []
++ main [$(pwd)]
++ EOF
++ test_cmp expect actual &&
++ git -C current for-each-ref --format="%(refname:short) [%(currentworktree)]" \
++ refs/heads/main refs/heads/current >actual &&
++ cat >expect <<-EOF &&
++ current [$(pwd)/current]
++ main []
++ EOF
++ test_cmp expect actual
+'
+
- test_expect_success 'branch being bisected in a worktree is marked' '
- test_when_finished "git -C bisect_dir bisect reset; rm -rf bisect_dir; git worktree prune; git branch -D bisecting" &&
- git worktree add -b bisecting bisect_dir branch-two &&
-@@ t/t3203-branch-output.sh: test_expect_success 'branch being bisected in a worktree is marked' '
- git commit --allow-empty -m bisect-one &&
- git commit --allow-empty -m bisect-two &&
- git bisect start HEAD HEAD~2 &&
-- test_must_fail git symbolic-ref -q HEAD
-+ test_must_fail git symbolic-ref -q HEAD &&
-+ git branch --list bisecting >actual &&
-+ echo " bisecting" >expect &&
-+ test_cmp expect actual
- ) &&
- git branch --list bisecting >actual &&
- echo "+ bisecting" >expect &&
+ test_expect_success 'start after with empty value' '
+ cat >expect <<-\EOF &&
+ refs/heads/main
-: ---------- > 5: 4177c84d31 branch: do not mark the branch the current worktree is rebasing
--
2.56.0
^ permalink raw reply [flat|nested] 50+ messages in thread* [PATCH v5 1/5] worktree remove: refuse a worktree in the middle of an operation
2026-11-11 15:28 ` [PATCH v5 0/5] " Jasper Mayone
@ 2026-11-11 15:28 ` Jasper Mayone
2026-11-11 15:28 ` [PATCH v5 2/5] doc: for-each-ref: say how to read %(worktreepath) safely Jasper Mayone
` (4 subsequent siblings)
5 siblings, 0 replies; 50+ messages in thread
From: Jasper Mayone @ 2026-11-11 15:28 UTC (permalink / raw)
To: git
Cc: gitster, phillip.wood123, sunshine, kristofferhaugsbakk,
ben.knoble, haraldnordgren, Jasper Mayone
"git worktree remove" refuses to remove a worktree with modified or
untracked files unless --force is given, but it removes a worktree
that is in the middle of a rebase, a "git am" session, a bisect, a
cherry-pick or a revert when its files happen to be clean. The state
of that operation lives in the worktree's own $GIT_DIR, so it is lost
with the worktree. Examples of such a clean worktree are a rebase
stopped at an "edit" or "break" line or after a failed "exec", a
cherry-pick sequence after a conflict was resolved and committed, and
a single cherry-pick or revert whose conflict was resolved to the
current contents, which leaves CHERRY_PICK_HEAD or REVERT_HEAD behind.
Treat such a worktree as unclean: refuse to remove it unless --force
is given, and name the operation in the message.
A worktree whose directory is already gone is still removed without
--force, as before: the check is part of the check for a clean
worktree, which is only done when the directory exists.
Reported-by: D. Ben Knoble <ben.knoble@gmail.com>
Signed-off-by: Jasper Mayone <me@jaspermayone.com>
---
Documentation/git-worktree.adoc | 17 ++++--
builtin/worktree.c | 37 ++++++++++++-
t/t2403-worktree-move.sh | 94 +++++++++++++++++++++++++++++++++
3 files changed, 142 insertions(+), 6 deletions(-)
diff --git a/Documentation/git-worktree.adoc b/Documentation/git-worktree.adoc
index e6e77252ab..cde17c9ab5 100644
--- a/Documentation/git-worktree.adoc
+++ b/Documentation/git-worktree.adoc
@@ -141,10 +141,16 @@ the new working tree location.
`remove`::
-Remove a worktree. Only clean worktrees (no untracked files and no
-modification in tracked files) can be removed. Unclean worktrees or ones
-with submodules can be removed with `--force`. The main worktree cannot be
-removed.
+Remove a worktree. The main worktree cannot be removed. Without
+`--force`, a worktree is not removed in these cases:
++
+--
+* The worktree has untracked files or modifications in tracked files.
+* The worktree has submodules.
+* The worktree is in the middle of a `git rebase`, `git am`,
+ `git bisect`, `git cherry-pick` or `git revert`. Removing the
+ worktree would lose the state of that operation.
+--
`repair [<path>...]`::
@@ -189,7 +195,8 @@ twice. If the destination is already assigned to some other worktree but is
missing (for instance, if _<new-path>_ was deleted manually), then `--force`
allows the move to proceed; use `--force` twice if the destination is locked.
+
-`remove` refuses to remove an unclean worktree unless `--force` is used.
+`remove` refuses to remove a worktree in the cases listed under
+`remove` above unless `--force` is used.
To remove a locked worktree, specify `--force` twice.
`-b <new-branch>`::
diff --git a/builtin/worktree.c b/builtin/worktree.c
index 77ecd0f71f..7f53215cda 100644
--- a/builtin/worktree.c
+++ b/builtin/worktree.c
@@ -26,6 +26,7 @@
#include "submodule.h"
#include "utf8.h"
#include "worktree.h"
+#include "wt-status.h"
#include "quote.h"
#define BUILTIN_WORKTREE_ADD_USAGE \
@@ -1408,6 +1409,38 @@ static void check_clean_worktree(struct worktree *wt,
original_path, ret);
}
+/*
+ * A rebase, "git am", bisect, cherry-pick or revert keeps its state in
+ * the worktree's own $GIT_DIR, and removing the worktree throws that
+ * away even when "git status" says the files are clean.
+ */
+static void check_no_operation_in_progress(struct worktree *wt,
+ const char *original_path)
+{
+ struct wt_status_state state = { 0 };
+
+ if (wt_status_check_rebase(wt, &state)) {
+ if (state.am_in_progress)
+ die(_("'%s' is in the middle of an am session, use --force to delete it"),
+ original_path);
+ die(_("'%s' is in the middle of a rebase, use --force to delete it"),
+ original_path);
+ }
+ if (wt_status_check_bisect(wt, &state))
+ die(_("'%s' is in the middle of a bisect, use --force to delete it"),
+ original_path);
+ wt_status_state_free_buffers(&state);
+ if (refs_ref_exists(get_worktree_ref_store(wt), "CHERRY_PICK_HEAD"))
+ die(_("'%s' is in the middle of a cherry-pick, use --force to delete it"),
+ original_path);
+ if (refs_ref_exists(get_worktree_ref_store(wt), "REVERT_HEAD"))
+ die(_("'%s' is in the middle of a revert, use --force to delete it"),
+ original_path);
+ if (is_directory(worktree_git_path(wt, "sequencer")))
+ die(_("'%s' is in the middle of a cherry-pick or revert, use --force to delete it"),
+ original_path);
+}
+
static int delete_git_work_tree(struct worktree *wt)
{
struct strbuf sb = STRBUF_INIT;
@@ -1461,8 +1494,10 @@ static int remove_worktree(int ac, const char **av, const char *prefix,
strbuf_release(&errmsg);
if (file_exists(wt->path)) {
- if (!force)
+ if (!force) {
+ check_no_operation_in_progress(wt, av[0]);
check_clean_worktree(wt, av[0]);
+ }
ret |= delete_git_work_tree(wt);
}
diff --git a/t/t2403-worktree-move.sh b/t/t2403-worktree-move.sh
index 69768c1207..2e45cd77f9 100755
--- a/t/t2403-worktree-move.sh
+++ b/t/t2403-worktree-move.sh
@@ -271,4 +271,98 @@ test_expect_success 'move worktree with relative path to absolute path' '
test_cmp expect .git/worktrees/absolute/gitdir
'
+test_expect_success 'setup commits that conflict for am, cherry-pick and revert' '
+ git worktree add --detach conflict-setup init &&
+ (
+ cd conflict-setup &&
+ test_commit base conflict.t base &&
+ git switch --detach init &&
+ test_commit side conflict.t side &&
+ test_commit side-two
+ ) &&
+ git worktree remove conflict-setup
+'
+
+test_expect_success 'remove worktree in the middle of a rebase' '
+ test_when_finished "rm -rf mid-rebase; git worktree prune" &&
+ git worktree add --detach mid-rebase side &&
+ test_must_fail git -C mid-rebase rebase -f -x false init &&
+ test_must_fail git worktree remove mid-rebase 2>err &&
+ test_grep "in the middle of a rebase" err &&
+ test_path_is_dir mid-rebase &&
+ git worktree remove --force mid-rebase &&
+ test_path_is_missing mid-rebase
+'
+
+test_expect_success 'remove worktree in the middle of an am session' '
+ test_when_finished "rm -rf mid-am patch; git worktree prune" &&
+ git format-patch -1 --stdout side >patch &&
+ git worktree add --detach mid-am base &&
+ (
+ cd mid-am &&
+ test_must_fail git am ../patch &&
+ git reset --hard
+ ) &&
+ test_must_fail git worktree remove mid-am 2>err &&
+ test_grep "in the middle of an am session" err &&
+ git worktree remove --force mid-am
+'
+
+test_expect_success 'remove worktree in the middle of a bisect' '
+ test_when_finished "rm -rf mid-bisect; git worktree prune" &&
+ git worktree add --detach mid-bisect side-two &&
+ git -C mid-bisect bisect start side-two init &&
+ test_must_fail git worktree remove mid-bisect 2>err &&
+ test_grep "in the middle of a bisect" err &&
+ git worktree remove --force mid-bisect
+'
+
+test_expect_success 'remove worktree in the middle of a cherry-pick' '
+ test_when_finished "rm -rf mid-pick; git worktree prune" &&
+ git worktree add --detach mid-pick base &&
+ (
+ cd mid-pick &&
+ test_must_fail git cherry-pick side &&
+ git checkout --ours conflict.t &&
+ git add conflict.t &&
+ git rev-parse --verify CHERRY_PICK_HEAD
+ ) &&
+ test_path_is_missing .git/worktrees/mid-pick/sequencer &&
+ test_must_fail git worktree remove mid-pick 2>err &&
+ test_grep "in the middle of a cherry-pick," err &&
+ git worktree remove --force mid-pick
+'
+
+test_expect_success 'remove worktree in the middle of a revert' '
+ test_when_finished "rm -rf mid-revert; git worktree prune" &&
+ git worktree add --detach mid-revert base &&
+ (
+ cd mid-revert &&
+ test_must_fail git revert side &&
+ git checkout --ours conflict.t &&
+ git add conflict.t &&
+ git rev-parse --verify REVERT_HEAD
+ ) &&
+ test_path_is_missing .git/worktrees/mid-revert/sequencer &&
+ test_must_fail git worktree remove mid-revert 2>err &&
+ test_grep "in the middle of a revert" err &&
+ git worktree remove --force mid-revert
+'
+
+test_expect_success 'remove worktree in the middle of a cherry-pick sequence' '
+ test_when_finished "rm -rf mid-pick-seq; git worktree prune" &&
+ git worktree add --detach mid-pick-seq base &&
+ (
+ cd mid-pick-seq &&
+ test_must_fail git cherry-pick side side-two &&
+ git checkout --theirs conflict.t &&
+ git add conflict.t &&
+ git commit --no-edit
+ ) &&
+ test_path_is_dir .git/worktrees/mid-pick-seq/sequencer &&
+ test_must_fail git worktree remove mid-pick-seq 2>err &&
+ test_grep "in the middle of a cherry-pick or revert" err &&
+ git worktree remove --force mid-pick-seq
+'
+
test_done
--
2.56.0
^ permalink raw reply related [flat|nested] 50+ messages in thread* [PATCH v5 2/5] doc: for-each-ref: say how to read %(worktreepath) safely
2026-11-11 15:28 ` [PATCH v5 0/5] " Jasper Mayone
2026-11-11 15:28 ` [PATCH v5 1/5] worktree remove: refuse a worktree in the middle of an operation Jasper Mayone
@ 2026-11-11 15:28 ` Jasper Mayone
2026-11-11 15:28 ` [PATCH v5 3/5] ref-filter: name the worktree of a branch being rebased or bisected Jasper Mayone
` (3 subsequent siblings)
5 siblings, 0 replies; 50+ messages in thread
From: Jasper Mayone @ 2026-11-11 15:28 UTC (permalink / raw)
To: git
Cc: gitster, phillip.wood123, sunshine, kristofferhaugsbakk,
ben.knoble, haraldnordgren, Jasper Mayone
The path that %(worktreepath) gives can contain a newline, which
breaks a script that reads one record per line. Say how to avoid
that: use a quoting option, or end the atom with %00 and look for the
NUL before the LF that ends the record.
Also say that only one worktree is shown when a branch is checked out
in more than one worktree.
Suggested-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Jasper Mayone <me@jaspermayone.com>
---
Documentation/git-for-each-ref.adoc | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/Documentation/git-for-each-ref.adoc b/Documentation/git-for-each-ref.adoc
index a7b31e8aaf..9247737aa7 100644
--- a/Documentation/git-for-each-ref.adoc
+++ b/Documentation/git-for-each-ref.adoc
@@ -183,7 +183,12 @@ but if used together the last option is selected.
`worktreepath`::
The absolute path to the worktree in which the ref is checked
out, if it is checked out in any linked worktree. Empty string
- otherwise.
+ otherwise. If the ref is checked out in more than one worktree,
+ only one of them is shown. The path can contain a newline, so
+ use one of the quoting options such as `--shell`, or follow
+ `%(worktreepath)` with `%00` and parse the output by looking for
+ the `NUL` after the path and then for the `LF` that ends each
+ record.
`ahead-behind:<commit-ish>`::
Two integers, separated by a space, demonstrating the number of
--
2.56.0
^ permalink raw reply related [flat|nested] 50+ messages in thread* [PATCH v5 3/5] ref-filter: name the worktree of a branch being rebased or bisected
2026-11-11 15:28 ` [PATCH v5 0/5] " Jasper Mayone
2026-11-11 15:28 ` [PATCH v5 1/5] worktree remove: refuse a worktree in the middle of an operation Jasper Mayone
2026-11-11 15:28 ` [PATCH v5 2/5] doc: for-each-ref: say how to read %(worktreepath) safely Jasper Mayone
@ 2026-11-11 15:28 ` Jasper Mayone
2026-11-11 15:28 ` [PATCH v5 4/5] ref-filter: add %(currentworktree) Jasper Mayone
` (2 subsequent siblings)
5 siblings, 0 replies; 50+ messages in thread
From: Jasper Mayone @ 2026-11-11 15:28 UTC (permalink / raw)
To: git
Cc: gitster, phillip.wood123, sunshine, kristofferhaugsbakk,
ben.knoble, haraldnordgren, Jasper Mayone
%(worktreepath) gives the worktree that has a branch checked out, and
"git branch" uses it to mark such branches with "+". It only looks at
the HEAD of each worktree, so while a worktree is rebasing or
bisecting a branch, its HEAD is detached and the branch shows no
worktree at all.
The rest of Git already treats that branch as checked out: "git
worktree add" and "git branch -d" refuse it, because finishing the
rebase or bisect will check it out again. But no command shows which
worktree is working on the branch: "git worktree list" shows such a
worktree as "(detached HEAD)", and %(worktreepath) is empty.
branch.c already knows the answer, as prepare_checked_out_branches()
records the branch that each worktree has checked out, is rebasing or
is bisecting. Record those branches in a map as well, add
branch_checked_out_worktree() to look a branch up in it, and use that
for %(worktreepath) in place of the map that ref-filter.c builds from
the HEAD of each worktree.
The new function skips the branches that "rebase --update-refs" will
update. branch_checked_out() includes them, so that they cannot be
checked out or deleted during the rebase, but the worktree that runs
the rebase has not checked them out. %(worktreepath) therefore stays
empty for them, unlike branch_checked_out().
This also changes the result for a bare repository: its HEAD names a
branch, and %(worktreepath) used to give the path of the bare
repository for that branch. "git branch" in a linked worktree marked
the branch with "+", although no worktree has it checked out and "git
worktree add" lets you check it out. prepare_checked_out_branches()
skips bare repositories, so %(worktreepath) is now empty for that
branch and "git branch" no longer marks it.
Reported-by: Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com>
Helped-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Jasper Mayone <me@jaspermayone.com>
---
Documentation/git-branch.adoc | 6 ++-
Documentation/git-for-each-ref.adoc | 5 +-
branch.c | 10 ++++
branch.h | 9 ++++
ref-filter.c | 74 ++--------------------------
t/t3203-branch-output.sh | 29 +++++++++++
t/t6302-for-each-ref-filter.sh | 76 +++++++++++++++++++++++++++++
7 files changed, 135 insertions(+), 74 deletions(-)
diff --git a/Documentation/git-branch.adoc b/Documentation/git-branch.adoc
index bfdf459329..510b0afc74 100644
--- a/Documentation/git-branch.adoc
+++ b/Documentation/git-branch.adoc
@@ -33,8 +33,10 @@ DESCRIPTION
If `--list` is given, or if there are no non-option arguments, existing
branches are listed; the current branch will be highlighted in green and
marked with an asterisk. Any branches checked out in linked worktrees will
-be highlighted in cyan and marked with a plus sign. Option `-r` causes the
-remote-tracking branches to be listed,
+be highlighted in cyan and marked with a plus sign. A branch that a
+worktree is rebasing or bisecting counts as checked out in that
+worktree, so the other worktrees mark it in the same way. Option `-r`
+causes the remote-tracking branches to be listed,
and option `-a` shows both local and remote branches.
If a `<pattern>`
diff --git a/Documentation/git-for-each-ref.adoc b/Documentation/git-for-each-ref.adoc
index 9247737aa7..4456b4809a 100644
--- a/Documentation/git-for-each-ref.adoc
+++ b/Documentation/git-for-each-ref.adoc
@@ -182,7 +182,10 @@ but if used together the last option is selected.
`worktreepath`::
The absolute path to the worktree in which the ref is checked
- out, if it is checked out in any linked worktree. Empty string
+ out, if it is checked out in any linked worktree. A branch that
+ a worktree is rebasing or bisecting counts as checked out there,
+ although that worktree's `HEAD` is detached. A branch that
+ `git rebase --update-refs` will update does not. Empty string
otherwise. If the ref is checked out in more than one worktree,
only one of them is shown. The path can contain a newline, so
use one of the quoting options such as `--shell`, or follow
diff --git a/branch.c b/branch.c
index 5bb1c28915..87f3ffc35f 100644
--- a/branch.c
+++ b/branch.c
@@ -384,6 +384,8 @@ int validate_branchname(const char *name, struct strbuf *ref)
static int initialized_checked_out_branches;
static struct strmap current_checked_out_branches = STRMAP_INIT;
+/* Like current_checked_out_branches, without the "update-ref" branches. */
+static struct strmap checked_out_worktrees = STRMAP_INIT;
enum branch_checkout_kind {
BRANCH_CHECKOUT_KIND_CHECKOUT,
@@ -416,6 +418,8 @@ static void register_checked_out_branch(const char *prefix, const char *name,
checked_out_branches_nr++;
strmap_put(¤t_checked_out_branches, refname, path_copy);
+ if (kind != BRANCH_CHECKOUT_KIND_UPDATE_REF)
+ strmap_put(&checked_out_worktrees, refname, path_copy);
}
static void prepare_checked_out_branches(void)
@@ -485,6 +489,12 @@ const char *branch_checked_out(const char *refname)
return strmap_get(¤t_checked_out_branches, refname);
}
+const char *branch_checked_out_worktree(const char *refname)
+{
+ prepare_checked_out_branches();
+ return strmap_get(&checked_out_worktrees, refname);
+}
+
const char *branch_bisecting(const char *refname)
{
prepare_checked_out_branches();
diff --git a/branch.h b/branch.h
index 95b2431f24..60cb92a3e4 100644
--- a/branch.h
+++ b/branch.h
@@ -106,6 +106,15 @@ void create_branches_recursively(struct repository *r, const char *name,
*/
const char *branch_checked_out(const char *refname);
+/*
+ * If a worktree has the branch 'refname' checked out, or is rebasing or
+ * bisecting it, return the path of that worktree, or of the last one if
+ * there are more. Otherwise return NULL. Unlike branch_checked_out(),
+ * this ignores the branches that "git rebase --update-refs" will
+ * update, because no worktree has them checked out.
+ */
+const char *branch_checked_out_worktree(const char *refname);
+
/*
* If the branch at 'refname' is currently used for bisecting in a
* worktree, then return the path to that worktree.
diff --git a/ref-filter.c b/ref-filter.c
index bdf54f6f59..b636e56199 100644
--- a/ref-filter.c
+++ b/ref-filter.c
@@ -2,6 +2,7 @@
#define DISABLE_SIGN_COMPARE_WARNINGS
#include "git-compat-util.h"
+#include "branch.h"
#include "environment.h"
#include "gettext.h"
#include "config.h"
@@ -95,30 +96,6 @@ static struct expand_data {
struct object_info info;
} oi, oi_deref;
-struct ref_to_worktree_entry {
- struct hashmap_entry ent;
- struct worktree *wt; /* key is wt->head_ref */
-};
-
-static int ref_to_worktree_map_cmpfnc(const void *lookupdata UNUSED,
- const struct hashmap_entry *eptr,
- const struct hashmap_entry *kptr,
- const void *keydata_aka_refname)
-{
- const struct ref_to_worktree_entry *e, *k;
-
- e = container_of(eptr, const struct ref_to_worktree_entry, ent);
- k = container_of(kptr, const struct ref_to_worktree_entry, ent);
-
- return strcmp(e->wt->head_ref,
- keydata_aka_refname ? keydata_aka_refname : k->wt->head_ref);
-}
-
-static struct ref_to_worktree_map {
- struct hashmap map;
- struct worktree **worktrees;
-} ref_to_worktree_map;
-
/*
* The enum atom_type is used as the index of valid_atom array.
* In the atom parsing stage, it will be passed to used_atom.atom_type
@@ -2380,49 +2357,11 @@ static int get_object(struct ref_array_item *ref, int deref,
return ret;
}
-static void populate_worktree_map(struct hashmap *map, struct worktree **worktrees)
-{
- int i;
-
- for (i = 0; worktrees[i]; i++) {
- if (worktrees[i]->head_ref) {
- struct ref_to_worktree_entry *entry;
- entry = xmalloc(sizeof(*entry));
- entry->wt = worktrees[i];
- hashmap_entry_init(&entry->ent,
- strhash(worktrees[i]->head_ref));
-
- hashmap_add(map, &entry->ent);
- }
- }
-}
-
-static void lazy_init_worktree_map(void)
-{
- if (ref_to_worktree_map.worktrees)
- return;
-
- ref_to_worktree_map.worktrees = get_worktrees(the_repository);
- hashmap_init(&(ref_to_worktree_map.map), ref_to_worktree_map_cmpfnc, NULL, 0);
- populate_worktree_map(&(ref_to_worktree_map.map), ref_to_worktree_map.worktrees);
-}
-
static char *get_worktree_path(const struct ref_array_item *ref)
{
- struct hashmap_entry entry, *e;
- struct ref_to_worktree_entry *lookup_result;
+ const char *path = branch_checked_out_worktree(ref->refname);
- lazy_init_worktree_map();
-
- hashmap_entry_init(&entry, strhash(ref->refname));
- e = hashmap_get(&(ref_to_worktree_map.map), &entry, ref->refname);
-
- if (!e)
- return xstrdup("");
-
- lookup_result = container_of(e, struct ref_to_worktree_entry, ent);
-
- return xstrdup(lookup_result->wt->path);
+ return xstrdup(path ? path : "");
}
/*
@@ -3195,13 +3134,6 @@ void ref_array_clear(struct ref_array *array)
FREE_AND_NULL(used_atom);
used_atom_cnt = 0;
- if (ref_to_worktree_map.worktrees) {
- hashmap_clear_and_free(&(ref_to_worktree_map.map),
- struct ref_to_worktree_entry, ent);
- free_worktrees(ref_to_worktree_map.worktrees);
- ref_to_worktree_map.worktrees = NULL;
- }
-
FREE_AND_NULL(array->counts);
}
diff --git a/t/t3203-branch-output.sh b/t/t3203-branch-output.sh
index 7c44186341..debfac5f26 100755
--- a/t/t3203-branch-output.sh
+++ b/t/t3203-branch-output.sh
@@ -442,6 +442,35 @@ test_expect_success 'worktree colors correct' '
test_cmp expect actual
'
+test_expect_success 'branch being rebased in a worktree is marked' '
+ test_when_finished "git -C rebase_dir rebase --abort; rm -rf rebase_dir; git worktree prune; git branch -D rebasing" &&
+ git worktree add -b rebasing rebase_dir branch-two &&
+ (
+ cd rebase_dir &&
+ git commit --allow-empty -m rebase-me &&
+ test_must_fail git rebase -f -x false HEAD~1 &&
+ test_must_fail git symbolic-ref -q HEAD
+ ) &&
+ git branch --list rebasing >actual &&
+ echo "+ rebasing" >expect &&
+ test_cmp expect actual
+'
+
+test_expect_success 'branch being bisected in a worktree is marked' '
+ test_when_finished "git -C bisect_dir bisect reset; rm -rf bisect_dir; git worktree prune; git branch -D bisecting" &&
+ git worktree add -b bisecting bisect_dir branch-two &&
+ (
+ cd bisect_dir &&
+ git commit --allow-empty -m bisect-one &&
+ git commit --allow-empty -m bisect-two &&
+ git bisect start HEAD HEAD~2 &&
+ test_must_fail git symbolic-ref -q HEAD
+ ) &&
+ git branch --list bisecting >actual &&
+ echo "+ bisecting" >expect &&
+ test_cmp expect actual
+'
+
test_expect_success "set up color tests" '
echo "<RED>main<RESET>" >expect.color &&
echo "main" >expect.bare &&
diff --git a/t/t6302-for-each-ref-filter.sh b/t/t6302-for-each-ref-filter.sh
index 7f060d97bf..317ed6f6cd 100755
--- a/t/t6302-for-each-ref-filter.sh
+++ b/t/t6302-for-each-ref-filter.sh
@@ -3,6 +3,7 @@
test_description='test for-each-refs usage of ref-filter APIs'
. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-rebase.sh
. "$TEST_DIRECTORY"/lib-gpg.sh
test_expect_success 'setup some history and refs' '
@@ -541,6 +542,81 @@ test_expect_success 'validate worktree atom' '
test_cmp expect actual
'
+test_expect_success 'worktree atom names a worktree rebasing the branch' '
+ test_when_finished "git -C rebasing rebase --abort; rm -rf rebasing; git worktree prune; git branch -D rebasing" &&
+ git worktree add -b rebasing rebasing main &&
+ git -C rebasing commit --allow-empty -m rebase-me &&
+ test_must_fail git -C rebasing rebase -f -x false main &&
+ test_must_fail git -C rebasing symbolic-ref -q HEAD &&
+ echo "$(pwd)/rebasing" >expect &&
+ git for-each-ref --format="%(worktreepath)" refs/heads/rebasing >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'worktree atom names a worktree bisecting the branch' '
+ test_when_finished "git -C bisecting bisect reset; rm -rf bisecting; git worktree prune; git branch -D bisecting" &&
+ git worktree add -b bisecting bisecting main &&
+ git -C bisecting commit --allow-empty -m bisect-one &&
+ git -C bisecting commit --allow-empty -m bisect-two &&
+ git -C bisecting bisect start HEAD main &&
+ test_must_fail git -C bisecting symbolic-ref -q HEAD &&
+ echo "$(pwd)/bisecting" >expect &&
+ git for-each-ref --format="%(worktreepath)" refs/heads/bisecting >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'worktree atom with an interactive rebase stopped at edit' '
+ test_when_finished "git -C editing rebase --abort; rm -rf editing; git worktree prune; git branch -D editing" &&
+ git worktree add -b editing editing main &&
+ git -C editing commit --allow-empty -m edit-me &&
+ (
+ set_fake_editor &&
+ FAKE_LINES="edit 1" git -C editing rebase -i --keep-empty HEAD~1
+ ) &&
+ test_must_fail git -C editing symbolic-ref -q HEAD &&
+ echo "$(pwd)/editing" >expect &&
+ git for-each-ref --format="%(worktreepath)" refs/heads/editing >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'worktree atom ignores branches rebase --update-refs will move' '
+ test_when_finished "git -C updating rebase --abort; rm -rf updating; git worktree prune; git branch -D updating mid" &&
+ git worktree add -b updating updating main &&
+ git -C updating commit --allow-empty -m one &&
+ git -C updating branch mid &&
+ git -C updating commit --allow-empty -m two &&
+ test_must_fail git -C updating rebase -f --update-refs -x false main &&
+ git for-each-ref --format="%(refname:short) [%(worktreepath)]" \
+ refs/heads/updating refs/heads/mid >actual &&
+ cat >expect <<-EOF &&
+ mid []
+ updating [$(pwd)/updating]
+ EOF
+ test_cmp expect actual
+'
+
+test_expect_success 'worktree atom ignores a rebase of a detached HEAD' '
+ test_when_finished "git -C detached-rebase rebase --abort; rm -rf detached-rebase; git worktree prune" &&
+ git worktree add --detach detached-rebase main &&
+ git -C detached-rebase commit --allow-empty -m detached &&
+ test_must_fail git -C detached-rebase rebase -f -x false main &&
+ git for-each-ref --format="%(worktreepath)" refs/heads/ >actual &&
+ test_grep ! detached-rebase actual
+'
+
+test_expect_success 'worktree atom ignores the HEAD of a bare repository' '
+ test_when_finished "rm -rf bare.git bare-wt" &&
+ git clone --bare . bare.git &&
+ git -C bare.git symbolic-ref HEAD refs/heads/main &&
+ git -C bare.git worktree add --detach ../bare-wt main &&
+ git -C bare.git for-each-ref --format="%(worktreepath)" refs/heads/main >actual &&
+ echo >expect &&
+ test_cmp expect actual &&
+ git -C bare-wt branch --list main >actual &&
+ echo " main" >expect &&
+ test_cmp expect actual
+'
+
test_expect_success 'start after with empty value' '
cat >expect <<-\EOF &&
refs/heads/main
--
2.56.0
^ permalink raw reply related [flat|nested] 50+ messages in thread* [PATCH v5 4/5] ref-filter: add %(currentworktree)
2026-11-11 15:28 ` [PATCH v5 0/5] " Jasper Mayone
` (2 preceding siblings ...)
2026-11-11 15:28 ` [PATCH v5 3/5] ref-filter: name the worktree of a branch being rebased or bisected Jasper Mayone
@ 2026-11-11 15:28 ` Jasper Mayone
2026-11-11 15:28 ` [PATCH v5 5/5] branch: do not mark the branch the current worktree is rebasing Jasper Mayone
2026-11-11 15:42 ` [PATCH v5 0/5] worktree: treat a branch being rebased or bisected as checked out Ben Knoble
5 siblings, 0 replies; 50+ messages in thread
From: Jasper Mayone @ 2026-11-11 15:28 UTC (permalink / raw)
To: git
Cc: gitster, phillip.wood123, sunshine, kristofferhaugsbakk,
ben.knoble, haraldnordgren, Jasper Mayone
%(worktreepath) names the worktree that has a branch checked out, or
is rebasing or bisecting it, but a format cannot tell if that worktree
is the current one. Comparing %(worktreepath) with a fixed path in
%(if:equals=...) does not work for every path, because a ")" ends the
atom.
Add %(currentworktree), which gives the path of the current worktree
when that worktree has the branch, and an empty string otherwise.
branch.c records these branches when it reads the worktrees, in a
second map next to the one that %(worktreepath) uses, so the lookup is
one map lookup per ref.
The next commit uses it in "git branch".
Suggested-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Jasper Mayone <me@jaspermayone.com>
---
Documentation/git-for-each-ref.adoc | 7 +++++++
branch.c | 25 ++++++++++++++++++-------
branch.h | 7 +++++++
ref-filter.c | 10 ++++++++++
t/t6302-for-each-ref-filter.sh | 21 +++++++++++++++++++++
5 files changed, 63 insertions(+), 7 deletions(-)
diff --git a/Documentation/git-for-each-ref.adoc b/Documentation/git-for-each-ref.adoc
index 4456b4809a..5e2da92ee2 100644
--- a/Documentation/git-for-each-ref.adoc
+++ b/Documentation/git-for-each-ref.adoc
@@ -193,6 +193,13 @@ but if used together the last option is selected.
the `NUL` after the path and then for the `LF` that ends each
record.
+`currentworktree`::
+ The absolute path to the current worktree, if the ref is checked
+ out there, in the same sense as for `worktreepath`. Empty string
+ otherwise, also when the ref is checked out only in other
+ worktrees. Use it with `%(if)` to treat the current worktree
+ differently from the others.
+
`ahead-behind:<commit-ish>`::
Two integers, separated by a space, demonstrating the number of
commits ahead and behind, respectively, when comparing the output
diff --git a/branch.c b/branch.c
index 87f3ffc35f..4de789b7ae 100644
--- a/branch.c
+++ b/branch.c
@@ -386,6 +386,8 @@ static int initialized_checked_out_branches;
static struct strmap current_checked_out_branches = STRMAP_INIT;
/* Like current_checked_out_branches, without the "update-ref" branches. */
static struct strmap checked_out_worktrees = STRMAP_INIT;
+/* The subset of checked_out_worktrees that the current worktree has. */
+static struct strmap current_worktree_branches = STRMAP_INIT;
enum branch_checkout_kind {
BRANCH_CHECKOUT_KIND_CHECKOUT,
@@ -404,11 +406,11 @@ static struct checked_out_branch *checked_out_branches;
static size_t checked_out_branches_alloc, checked_out_branches_nr;
static void register_checked_out_branch(const char *prefix, const char *name,
- const char *path,
+ const struct worktree *wt,
enum branch_checkout_kind kind)
{
char *refname = xstrfmt("%s%s", prefix, name);
- char *path_copy = xstrdup(path);
+ char *path_copy = xstrdup(wt->path);
ALLOC_GROW(checked_out_branches, checked_out_branches_nr + 1,
checked_out_branches_alloc);
@@ -418,8 +420,11 @@ static void register_checked_out_branch(const char *prefix, const char *name,
checked_out_branches_nr++;
strmap_put(¤t_checked_out_branches, refname, path_copy);
- if (kind != BRANCH_CHECKOUT_KIND_UPDATE_REF)
+ if (kind != BRANCH_CHECKOUT_KIND_UPDATE_REF) {
strmap_put(&checked_out_worktrees, refname, path_copy);
+ if (wt->is_current)
+ strmap_put(¤t_worktree_branches, refname, path_copy);
+ }
}
static void prepare_checked_out_branches(void)
@@ -443,7 +448,7 @@ static void prepare_checked_out_branches(void)
continue;
if (wt->head_ref) {
- register_checked_out_branch("", wt->head_ref, wt->path,
+ register_checked_out_branch("", wt->head_ref, wt,
BRANCH_CHECKOUT_KIND_CHECKOUT);
}
@@ -451,7 +456,7 @@ static void prepare_checked_out_branches(void)
(state.rebase_in_progress || state.rebase_interactive_in_progress) &&
state.branch) {
register_checked_out_branch("refs/heads/", state.branch,
- wt->path,
+ wt,
BRANCH_CHECKOUT_KIND_REBASE);
}
wt_status_state_free_buffers(&state);
@@ -460,7 +465,7 @@ static void prepare_checked_out_branches(void)
state.bisecting_from) {
register_checked_out_branch("refs/heads/",
state.bisecting_from,
- wt->path,
+ wt,
BRANCH_CHECKOUT_KIND_BISECT);
}
wt_status_state_free_buffers(&state);
@@ -471,7 +476,7 @@ static void prepare_checked_out_branches(void)
struct string_list_item *item;
for_each_string_list_item(item, &update_refs) {
register_checked_out_branch("", item->string,
- wt->path,
+ wt,
BRANCH_CHECKOUT_KIND_UPDATE_REF);
}
string_list_clear(&update_refs, 1);
@@ -495,6 +500,12 @@ const char *branch_checked_out_worktree(const char *refname)
return strmap_get(&checked_out_worktrees, refname);
}
+const char *branch_checked_out_current_worktree(const char *refname)
+{
+ prepare_checked_out_branches();
+ return strmap_get(¤t_worktree_branches, refname);
+}
+
const char *branch_bisecting(const char *refname)
{
prepare_checked_out_branches();
diff --git a/branch.h b/branch.h
index 60cb92a3e4..744e1eec1d 100644
--- a/branch.h
+++ b/branch.h
@@ -115,6 +115,13 @@ const char *branch_checked_out(const char *refname);
*/
const char *branch_checked_out_worktree(const char *refname);
+/*
+ * Like branch_checked_out_worktree(), but only for the current
+ * worktree: return its path if it has the branch 'refname' checked
+ * out, or is rebasing or bisecting it. Otherwise return NULL.
+ */
+const char *branch_checked_out_current_worktree(const char *refname);
+
/*
* If the branch at 'refname' is currently used for bisecting in a
* worktree, then return the path to that worktree.
diff --git a/ref-filter.c b/ref-filter.c
index b636e56199..498d09da8f 100644
--- a/ref-filter.c
+++ b/ref-filter.c
@@ -142,6 +142,7 @@ enum atom_type {
ATOM_HEAD,
ATOM_COLOR,
ATOM_WORKTREEPATH,
+ ATOM_CURRENTWORKTREE,
ATOM_ALIGN,
ATOM_END,
ATOM_IF,
@@ -960,6 +961,7 @@ static struct {
[ATOM_HEAD] = { "HEAD", SOURCE_NONE, FIELD_STR, head_atom_parser },
[ATOM_COLOR] = { "color", SOURCE_NONE, FIELD_STR, color_atom_parser },
[ATOM_WORKTREEPATH] = { "worktreepath", SOURCE_NONE },
+ [ATOM_CURRENTWORKTREE] = { "currentworktree", SOURCE_NONE },
[ATOM_ALIGN] = { "align", SOURCE_NONE, FIELD_STR, align_atom_parser },
[ATOM_END] = { "end", SOURCE_NONE },
[ATOM_IF] = { "if", SOURCE_NONE, FIELD_STR, if_atom_parser },
@@ -2420,6 +2422,14 @@ static int populate_value(struct ref_array_item *ref, struct strbuf *err)
v->s = xstrdup("");
continue;
}
+ else if (atom_type == ATOM_CURRENTWORKTREE) {
+ const char *path = NULL;
+
+ if (ref->kind == FILTER_REFS_BRANCHES)
+ path = branch_checked_out_current_worktree(ref->refname);
+ v->s = xstrdup(path ? path : "");
+ continue;
+ }
else if (atom_type == ATOM_SYMREF)
refname = get_symref(atom, ref);
else if (atom_type == ATOM_UPSTREAM) {
diff --git a/t/t6302-for-each-ref-filter.sh b/t/t6302-for-each-ref-filter.sh
index 317ed6f6cd..32522f1ff3 100755
--- a/t/t6302-for-each-ref-filter.sh
+++ b/t/t6302-for-each-ref-filter.sh
@@ -617,6 +617,27 @@ test_expect_success 'worktree atom ignores the HEAD of a bare repository' '
test_cmp expect actual
'
+test_expect_success 'currentworktree atom' '
+ test_when_finished "git -C current rebase --abort; rm -rf current; git worktree prune; git branch -D current" &&
+ git worktree add -b current current main &&
+ git -C current commit --allow-empty -m rebase-me &&
+ test_must_fail git -C current rebase -f -x false main &&
+ git for-each-ref --format="%(refname:short) [%(currentworktree)]" \
+ refs/heads/main refs/heads/current >actual &&
+ cat >expect <<-EOF &&
+ current []
+ main [$(pwd)]
+ EOF
+ test_cmp expect actual &&
+ git -C current for-each-ref --format="%(refname:short) [%(currentworktree)]" \
+ refs/heads/main refs/heads/current >actual &&
+ cat >expect <<-EOF &&
+ current [$(pwd)/current]
+ main []
+ EOF
+ test_cmp expect actual
+'
+
test_expect_success 'start after with empty value' '
cat >expect <<-\EOF &&
refs/heads/main
--
2.56.0
^ permalink raw reply related [flat|nested] 50+ messages in thread* [PATCH v5 5/5] branch: do not mark the branch the current worktree is rebasing
2026-11-11 15:28 ` [PATCH v5 0/5] " Jasper Mayone
` (3 preceding siblings ...)
2026-11-11 15:28 ` [PATCH v5 4/5] ref-filter: add %(currentworktree) Jasper Mayone
@ 2026-11-11 15:28 ` Jasper Mayone
2026-11-11 15:42 ` [PATCH v5 0/5] worktree: treat a branch being rebased or bisected as checked out Ben Knoble
5 siblings, 0 replies; 50+ messages in thread
From: Jasper Mayone @ 2026-11-11 15:28 UTC (permalink / raw)
To: git
Cc: gitster, phillip.wood123, sunshine, kristofferhaugsbakk,
ben.knoble, haraldnordgren, Jasper Mayone
Since "ref-filter: name the worktree of a branch being rebased or
bisected", "git branch" marks a branch with "+" when a worktree is
rebasing or bisecting it. In that worktree itself, the list already
starts with "* (no branch, rebasing topic)", so a "+ topic" line
below it shows the same branch twice.
Use %(currentworktree) in the default format, so that a branch that
the current worktree has gets no "+" mark and no "(path)" with -vv.
A branch that is checked out normally in the current worktree is not
affected, as %(HEAD) already marks it with "*" before these checks.
Suggested-by: Phillip Wood <phillip.wood123@gmail.com>
Signed-off-by: Jasper Mayone <me@jaspermayone.com>
---
builtin/branch.c | 10 ++++++++--
t/t3203-branch-output.sh | 22 +++++++++++++++++++++-
2 files changed, 29 insertions(+), 3 deletions(-)
diff --git a/builtin/branch.c b/builtin/branch.c
index a613148fc7..87670d16e4 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -424,8 +424,14 @@ static char *build_format(struct ref_filter *filter, int maxwidth, const char *r
struct strbuf local = STRBUF_INIT;
struct strbuf remote = STRBUF_INIT;
- strbuf_addf(&local, "%%(if)%%(HEAD)%%(then)* %s%%(else)%%(if)%%(worktreepath)%%(then)+ %s%%(else) %s%%(end)%%(end)",
+ /*
+ * Mark a branch that another worktree has checked out with "+".
+ * A branch that the current worktree is rebasing or bisecting has
+ * no mark, as the "(no branch, ...)" line already shows its name.
+ */
+ strbuf_addf(&local, "%%(if)%%(HEAD)%%(then)* %s%%(else)%%(if)%%(currentworktree)%%(then) %s%%(else)%%(if)%%(worktreepath)%%(then)+ %s%%(else) %s%%(end)%%(end)%%(end)",
branch_get_color(BRANCH_COLOR_CURRENT),
+ branch_get_color(BRANCH_COLOR_LOCAL),
branch_get_color(BRANCH_COLOR_WORKTREE),
branch_get_color(BRANCH_COLOR_LOCAL));
strbuf_addf(&remote, " %s",
@@ -447,7 +453,7 @@ static char *build_format(struct ref_filter *filter, int maxwidth, const char *r
if (filter->verbose > 1)
{
- strbuf_addf(&local, "%%(if:notequals=*)%%(HEAD)%%(then)%%(if)%%(worktreepath)%%(then)(%s%%(worktreepath)%s) %%(end)%%(end)",
+ strbuf_addf(&local, "%%(if:notequals=*)%%(HEAD)%%(then)%%(if)%%(currentworktree)%%(then)%%(else)%%(if)%%(worktreepath)%%(then)(%s%%(worktreepath)%s) %%(end)%%(end)%%(end)",
branch_get_color(BRANCH_COLOR_WORKTREE), branch_get_color(BRANCH_COLOR_RESET));
strbuf_addf(&local, "%%(if)%%(upstream)%%(then)[%s%%(upstream:short)%s%%(if)%%(upstream:track)"
"%%(then): %%(upstream:track,nobracket)%%(end)] %%(end)%%(contents:subject)",
diff --git a/t/t3203-branch-output.sh b/t/t3203-branch-output.sh
index debfac5f26..5541f1429b 100755
--- a/t/t3203-branch-output.sh
+++ b/t/t3203-branch-output.sh
@@ -456,6 +456,23 @@ test_expect_success 'branch being rebased in a worktree is marked' '
test_cmp expect actual
'
+test_expect_success 'branch being rebased is not marked in its own worktree' '
+ test_when_finished "git -C rebase_dir rebase --abort; rm -rf rebase_dir; git worktree prune; git branch -D rebasing" &&
+ git worktree add -b rebasing rebase_dir branch-two &&
+ (
+ cd rebase_dir &&
+ git commit --allow-empty -m rebase-me &&
+ test_must_fail git rebase -f -x false HEAD~1 &&
+ git branch --list rebasing >actual &&
+ echo " rebasing" >expect &&
+ test_cmp expect actual &&
+ git branch -vv --list rebasing >actual &&
+ test_grep ! "$(pwd)" actual &&
+ git branch >actual &&
+ test_grep "^\* (no branch, rebasing rebasing)" actual
+ )
+'
+
test_expect_success 'branch being bisected in a worktree is marked' '
test_when_finished "git -C bisect_dir bisect reset; rm -rf bisect_dir; git worktree prune; git branch -D bisecting" &&
git worktree add -b bisecting bisect_dir branch-two &&
@@ -464,7 +481,10 @@ test_expect_success 'branch being bisected in a worktree is marked' '
git commit --allow-empty -m bisect-one &&
git commit --allow-empty -m bisect-two &&
git bisect start HEAD HEAD~2 &&
- test_must_fail git symbolic-ref -q HEAD
+ test_must_fail git symbolic-ref -q HEAD &&
+ git branch --list bisecting >actual &&
+ echo " bisecting" >expect &&
+ test_cmp expect actual
) &&
git branch --list bisecting >actual &&
echo "+ bisecting" >expect &&
--
2.56.0
^ permalink raw reply related [flat|nested] 50+ messages in thread* Re: [PATCH v5 0/5] worktree: treat a branch being rebased or bisected as checked out
2026-11-11 15:28 ` [PATCH v5 0/5] " Jasper Mayone
` (4 preceding siblings ...)
2026-11-11 15:28 ` [PATCH v5 5/5] branch: do not mark the branch the current worktree is rebasing Jasper Mayone
@ 2026-11-11 15:42 ` Ben Knoble
5 siblings, 0 replies; 50+ messages in thread
From: Ben Knoble @ 2026-11-11 15:42 UTC (permalink / raw)
To: Jasper Mayone
Cc: git, gitster, phillip.wood123, sunshine, kristofferhaugsbakk,
haraldnordgren, Jasper Mayone
> Le 11 nov. 2026 à 10:28, Jasper Mayone <me@jaspermayone.com> a écrit :
>
> While a worktree is rebasing or bisecting a branch, its HEAD is
> detached. "git worktree add" and "git branch -d" already treat the
> branch as checked out there, but "git worktree remove" deletes such a
> worktree when its files are clean, and %(worktreepath) and "git
> branch" do not show which worktree has the branch.
>
> Patch 1 makes "git worktree remove" refuse a worktree in the middle of
> an operation unless --force is given. Patch 2 documents how to read
> %(worktreepath) safely. Patch 3 makes %(worktreepath) name the
> worktree that is rebasing or bisecting a branch. Patch 4 adds
> %(currentworktree), and patch 5 uses it so that "git branch" does not
> mark that branch with "+" in the worktree that is rebasing it.
>
> Changes since v4 [1]:
>
> * Patch 1: each item in the list of cases that need --force is now a
> complete sentence, and all the commands are in the same font, as
> Phillip asked. Ben was happy with the old wording, and I hope this
> suits both of you.
It was a tiny nit (for me at least) to begin with, but the new version is
also OK by me.
^ permalink raw reply [flat|nested] 50+ messages in thread