All of lore.kernel.org
 help / color / mirror / Atom feed
From: Jasper Mayone <me@jaspermayone.com>
To: git@vger.kernel.org
Cc: gitster@pobox.com, phillip.wood123@gmail.com,
	sunshine@sunshineco.com, kristofferhaugsbakk@fastmail.com,
	ben.knoble@gmail.com, haraldnordgren@gmail.com,
	Jasper Mayone <me@jaspermayone.com>
Subject: [PATCH v5 3/5] ref-filter: name the worktree of a branch being rebased or bisected
Date: Wed, 11 Nov 2026 10:28:08 -0500	[thread overview]
Message-ID: <20261111152811.99986-4-me@jaspermayone.com> (raw)
In-Reply-To: <20261111152811.99986-1-me@jaspermayone.com>

%(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(&current_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(&current_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


  parent reply	other threads:[~2026-11-11 15:28 UTC|newest]

Thread overview: 50+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-21 17:33 [RFC PATCH 0/1] worktree list: add --merged and --no-merged Jasper Mayone
2026-10-21 17:33 ` [RFC PATCH 1/1] " Jasper Mayone
2026-10-23 18:30   ` Junio C Hamano
2026-10-24  6:25     ` Eric Sunshine
2026-10-24 16:53       ` [PATCH v2] worktree list: add --branch to select worktrees by branch Jasper Mayone
2026-10-25 14:47         ` Phillip Wood
2026-10-26 11:02           ` Phillip Wood
2026-10-28  6:02           ` Eric Sunshine
2026-10-28 15:27             ` Junio C Hamano
2026-10-28 11:32         ` Kristoffer Haugsbakk
2026-10-29  4:46           ` Jasper Mayone
2026-10-29  5:01             ` Eric Sunshine
2026-10-29 11:14             ` Phillip Wood
2026-10-29 14:23               ` Ben Knoble
2026-10-29 16:10                 ` Phillip Wood
2026-10-30 14:21         ` [PATCH 0/2] worktree: treat a branch being rebased or bisected as checked out Jasper Mayone
2026-10-30 14:21           ` [PATCH 1/2] worktree remove: refuse a worktree in the middle of a rebase or bisect Jasper Mayone
2026-11-02 14:07             ` Phillip Wood
2026-11-02 17:57               ` Jasper Mayone
2026-10-30 14:21           ` [PATCH 2/2] ref-filter: name the worktree of a branch being rebased or bisected Jasper Mayone
2026-11-02 14:06             ` Phillip Wood
2026-11-02 17:57               ` Jasper Mayone
2026-10-30 22:55           ` [PATCH 0/2] worktree: treat a branch being rebased or bisected as checked out Junio C Hamano
2026-10-30 23:06             ` Jasper Mayone
2026-11-02 18:29           ` [PATCH v4 0/4] " Jasper Mayone
2026-11-02 18:29             ` [PATCH v4 1/4] worktree remove: refuse a worktree in the middle of an operation Jasper Mayone
2026-11-10 16:47               ` Phillip Wood
2026-11-10 17:09                 ` Ben Knoble
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-09 17:27                   ` Ben Knoble
2026-11-09 20:17                     ` Junio C Hamano
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
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
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
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 [this message]
2026-11-11 15:28               ` [PATCH v5 4/5] ref-filter: add %(currentworktree) 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
2026-10-21 21:36 ` [RFC PATCH 0/1] worktree list: add --merged and --no-merged Harald Nordgren
2026-10-21 22:12   ` Junio C Hamano
2026-10-22  7:21     ` Harald Nordgren
2026-10-22 17:58   ` Kristoffer Haugsbakk

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261111152811.99986-4-me@jaspermayone.com \
    --to=me@jaspermayone.com \
    --cc=ben.knoble@gmail.com \
    --cc=git@vger.kernel.org \
    --cc=gitster@pobox.com \
    --cc=haraldnordgren@gmail.com \
    --cc=kristofferhaugsbakk@fastmail.com \
    --cc=phillip.wood123@gmail.com \
    --cc=sunshine@sunshineco.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.