Repository: git-town/git-town

Issue:
walk: tries to iterate a branch that is checked out in another worktree
### Steps to reproduce

- have some branches
- check out one branch in another worktree
- run `git town walk --all git town propose`

Expected behavior: it skips the branch that is checked out in the other worktree

Actual behavior: it tries to check out the branch that is already checked out in another worktree

### Command output

```shell
~/git-town [kg-main-branch-name]  walk  > git town continue

[kg-main-branch-name] git checkout kg-merge-close-proposal
fatal: 'kg-merge-close-proposal' is already used by worktree at '/home/kevlar/git-town-2'


Error: cannot check out branch "kg-merge-close-proposal": exit status 128

To continue after having resolved conflicts, run "git town continue".
To go back to where you started, run "git town undo".
To continue by skipping the current branch, run "git town skip".
```

### Additional information

_No response_

Discussion:

Solution patch:
diff --git a/cucumber-sort.json b/cucumber-sort.json
index f257b10fd..1e1541373 100644
--- a/cucumber-sort.json
+++ b/cucumber-sort.json
@@ -61,6 +61,7 @@
     "features/walk/failing_script/failing_script.feature",
     "features/walk/stack/no_args.feature",
     "features/walk/undo/manual.feature",
+    "features/walk/worktree/no_args.feature",
     "vendor/**/*.feature"
   ],
   "record": true,
diff --git a/features/walk/worktree/no_args.feature b/features/walk/worktree/no_args.feature
new file mode 100644
index 000000000..a76f7a490
--- /dev/null
+++ b/features/walk/worktree/no_args.feature
@@ -0,0 +1,35 @@
+Feature: walk each branch of a stack without arguments when some branches are checked out in another worktree
+
+  Scenario: action
+    Given a Git repo with origin
+    And the branches
+      | NAME     | TYPE    | PARENT   | LOCATIONS |
+      | branch-1 | feature | main     | local     |
+      | branch-2 | feature | branch-1 | local     |
+      | branch-3 | feature | branch-2 | local     |
+    And the current branch is "branch-2"
+    And branch "branch-1" is active in another worktree
+    When I run "git-town walk --stack"
+    Then Git Town runs no commands
+    And Git Town prints:
+      """
+      Run "git town continue" to go to the next branch.
+      """
+    And the current branch is still "branch-2"
+    When I run "git-town continue"
+    Then Git Town runs the commands
+      | BRANCH   | COMMAND               |
+      | branch-2 | git checkout branch-3 |
+    And Git Town prints:
+      """
+      Run "git town continue" to go to the next branch.
+      """
+    And the current branch is now "branch-3"
+    When I run "git-town continue"
+    Then Git Town runs the commands
+      | BRANCH   | COMMAND               |
+      | branch-3 | git checkout branch-2 |
+    And Git Town prints:
+      """
+      Branch walk done.
+      """
diff --git a/features/walk/worktree/with_args.feature b/features/walk/worktree/with_args.feature
new file mode 100644
index 000000000..f77b822d3
--- /dev/null
+++ b/features/walk/worktree/with_args.feature
@@ -0,0 +1,22 @@
+Feature: walk each branch of a stack with arguments when some branches are checked out in another worktree
+
+  Scenario: iterate the full stack
+    Given a Git repo with origin
+    And the branches
+      | NAME     | TYPE    | PARENT   | LOCATIONS |
+      | branch-1 | feature | main     | local     |
+      | branch-2 | feature | branch-1 | local     |
+      | branch-3 | feature | branch-2 | local     |
+    And the current branch is "branch-2"
+    And branch "branch-1" is active in another worktree
+    When I run "git-town walk --stack echo hello"
+    Then Git Town runs the commands
+      | BRANCH   | COMMAND               |
+      | branch-2 | echo hello            |
+      |          | git checkout branch-3 |
+      | branch-3 | echo hello            |
+      |          | git checkout branch-2 |
+    And Git Town prints:
+      """
+      Branch walk done.
+      """
diff --git a/internal/cmd/walk.go b/internal/cmd/walk.go
index 0191437eb..13d02adfb 100644
--- a/internal/cmd/walk.go
+++ b/internal/cmd/walk.go
@@ -287,10 +287,12 @@ func determineWalkData(repo execute.OpenRepoResult, all configdomain.AllBranches
 	case stack.Enabled():
 		branchesToWalk = validatedConfig.NormalConfig.Lineage.BranchLineageWithoutRoot(initialBranch, perennialBranchNames, validatedConfig.NormalConfig.Order)
 	}
+	branchesInOtherWorktrees := branchesSnapshot.Branches.BranchesInOtherWorktrees()
+	branchesInCurrentWorktree := branchesToWalk.Remove(branchesInOtherWorktrees...)
 	return walkData{
 		branchInfosLastRun: branchInfosLastRun,
 		branchesSnapshot:   branchesSnapshot,
-		branchesToWalk:     branchesToWalk,
+		branchesToWalk:     branchesInCurrentWorktree,
 		config:             validatedConfig,
 		connector:          connector,
 		hasOpenChanges:     repoStatus.OpenChanges,
diff --git a/internal/git/gitdomain/branch_infos.go b/internal/git/gitdomain/branch_infos.go
index cb2747197..bf404b81e 100644
--- a/internal/git/gitdomain/branch_infos.go
+++ b/internal/git/gitdomain/branch_infos.go
@@ -20,6 +20,16 @@ func (self BranchInfos) BranchIsActiveInAnotherWorktree(branch LocalBranchName)
 	return branchInfo.SyncStatus == SyncStatusOtherWorktree
 }
 
+func (self BranchInfos) BranchesInOtherWorktrees() LocalBranchNames {
+	result := LocalBranchNames{}
+	for _, bi := range self {
+		if bi.SyncStatus == SyncStatusOtherWorktree {
+			result = append(result, bi.GetLocalOrRemoteNameAsLocalName())
+		}
+	}
+	return result
+}
+
 // FindByLocalName provides the branch with the given name if one exists.
 func (self BranchInfos) FindByLocalName(branchName LocalBranchName) OptionalMutable[BranchInfo] {
 	for bi, branch := range self {



Test patch:
diff --git a/internal/git/gitdomain/branch_infos_test.go b/internal/git/gitdomain/branch_infos_test.go
index 77d0ab353..5459ff4e5 100644
--- a/internal/git/gitdomain/branch_infos_test.go
+++ b/internal/git/gitdomain/branch_infos_test.go
@@ -63,6 +63,131 @@ func TestBranchInfos(t *testing.T) {
 		})
 	})
 
+	t.Run("BranchesInOtherWorktrees", func(t *testing.T) {
+		t.Parallel()
+		t.Run("empty BranchInfos", func(t *testing.T) {
+			t.Parallel()
+			branchInfos := gitdomain.BranchInfos{}
+			have := branchInfos.BranchesInOtherWorktrees()
+			want := gitdomain.LocalBranchNames{}
+			must.Eq(t, want, have)
+		})
+		t.Run("no branches in other worktrees", func(t *testing.T) {
+			t.Parallel()
+			branchInfos := gitdomain.BranchInfos{
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("branch-1"),
+					SyncStatus: gitdomain.SyncStatusLocalOnly,
+				},
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("branch-2"),
+					SyncStatus: gitdomain.SyncStatusUpToDate,
+					RemoteName: Some(gitdomain.NewRemoteBranchName("origin/branch-2")),
+				},
+			}
+			have := branchInfos.BranchesInOtherWorktrees()
+			want := gitdomain.LocalBranchNames{}
+			must.Eq(t, want, have)
+		})
+		t.Run("one branch in another worktree with local name", func(t *testing.T) {
+			t.Parallel()
+			branchInfos := gitdomain.BranchInfos{
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("branch-1"),
+					SyncStatus: gitdomain.SyncStatusLocalOnly,
+				},
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("branch-2"),
+					SyncStatus: gitdomain.SyncStatusOtherWorktree,
+				},
+			}
+			have := branchInfos.BranchesInOtherWorktrees()
+			want := gitdomain.NewLocalBranchNames("branch-2")
+			must.Eq(t, want, have)
+		})
+		t.Run("one branch in another worktree with remote name only", func(t *testing.T) {
+			t.Parallel()
+			branchInfos := gitdomain.BranchInfos{
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("branch-1"),
+					SyncStatus: gitdomain.SyncStatusLocalOnly,
+				},
+				{
+					LocalName:  None[gitdomain.LocalBranchName](),
+					RemoteName: Some(gitdomain.NewRemoteBranchName("origin/branch-2")),
+					SyncStatus: gitdomain.SyncStatusOtherWorktree,
+				},
+			}
+			have := branchInfos.BranchesInOtherWorktrees()
+			want := gitdomain.NewLocalBranchNames("branch-2")
+			must.Eq(t, want, have)
+		})
+		t.Run("multiple branches in other worktrees", func(t *testing.T) {
+			t.Parallel()
+			branchInfos := gitdomain.BranchInfos{
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("branch-1"),
+					SyncStatus: gitdomain.SyncStatusLocalOnly,
+				},
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("branch-2"),
+					SyncStatus: gitdomain.SyncStatusOtherWorktree,
+				},
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("branch-3"),
+					SyncStatus: gitdomain.SyncStatusUpToDate,
+					RemoteName: Some(gitdomain.NewRemoteBranchName("origin/branch-3")),
+				},
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("branch-4"),
+					SyncStatus: gitdomain.SyncStatusOtherWorktree,
+				},
+				{
+					LocalName:  None[gitdomain.LocalBranchName](),
+					RemoteName: Some(gitdomain.NewRemoteBranchName("origin/branch-5")),
+					SyncStatus: gitdomain.SyncStatusOtherWorktree,
+				},
+			}
+			have := branchInfos.BranchesInOtherWorktrees()
+			want := gitdomain.NewLocalBranchNames("branch-2", "branch-4", "branch-5")
+			must.Eq(t, want, have)
+		})
+		t.Run("mixed statuses including other worktrees", func(t *testing.T) {
+			t.Parallel()
+			branchInfos := gitdomain.BranchInfos{
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("local-only"),
+					SyncStatus: gitdomain.SyncStatusLocalOnly,
+				},
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("up-to-date"),
+					SyncStatus: gitdomain.SyncStatusUpToDate,
+					RemoteName: Some(gitdomain.NewRemoteBranchName("origin/up-to-date")),
+				},
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("other-worktree-1"),
+					SyncStatus: gitdomain.SyncStatusOtherWorktree,
+				},
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("ahead"),
+					SyncStatus: gitdomain.SyncStatusAhead,
+					RemoteName: Some(gitdomain.NewRemoteBranchName("origin/ahead")),
+				},
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("other-worktree-2"),
+					SyncStatus: gitdomain.SyncStatusOtherWorktree,
+				},
+				{
+					LocalName:  gitdomain.NewLocalBranchNameOption("deleted-at-remote"),
+					SyncStatus: gitdomain.SyncStatusDeletedAtRemote,
+				},
+			}
+			have := branchInfos.BranchesInOtherWorktrees()
+			want := gitdomain.NewLocalBranchNames("other-worktree-1", "other-worktree-2")
+			must.Eq(t, want, have)
+		})
+	})
+
 	t.Run("FindByRemote", func(t *testing.T) {
 		t.Parallel()
 		t.Run("has a local branch with matching tracking branch", func(t *testing.T) {