Repository: git-town/git-town

Issue:
branch: displays branches multiple times when there is an upstream remote
### Steps to reproduce

- add an `upstream` remote
- run `git fetch upstream`
- run `git town branch`

### Command output

```shell
* main
* main
```

### Additional information

The main branch occurs several times in the output of `git branch -vva`, hence the duplicate entries.

Discussion:
@kevgo: This is now available as part of [Git Town 21](https://github.com/git-town/git-town/releases/tag/v21.0.0).

Solution patch:
diff --git a/features/branch/upstream.feature b/features/branch/upstream.feature
index ff4c2f92a..b49824798 100644
--- a/features/branch/upstream.feature
+++ b/features/branch/upstream.feature
@@ -8,9 +8,7 @@ Feature: display the local branch hierarchy when there is an upstream
 
   Scenario: result
     Then Git Town runs no commands
-    # TODO: fix this
     And Git Town prints:
       """
       * main
-      * main
       """
diff --git a/internal/cli/dialog/switch_branch.go b/internal/cli/dialog/switch_branch.go
index f445379a7..3e3a34519 100644
--- a/internal/cli/dialog/switch_branch.go
+++ b/internal/cli/dialog/switch_branch.go
@@ -25,6 +25,17 @@ func (sbe SwitchBranchEntry) String() string {
 	return sbe.Indentation + sbe.Branch.String()
 }
 
+type SwitchBranchEntries []SwitchBranchEntry
+
+func (sbes SwitchBranchEntries) ContainsBranch(branch gitdomain.LocalBranchName) bool {
+	for _, entry := range sbes {
+		if entry.Branch == branch {
+			return true
+		}
+	}
+	return false
+}
+
 type SwitchModel struct {
 	list.List[SwitchBranchEntry]
 	DisplayBranchTypes configdomain.DisplayTypes
diff --git a/internal/cmd/branch.go b/internal/cmd/branch.go
index 7d994a513..d9da87de0 100644
--- a/internal/cmd/branch.go
+++ b/internal/cmd/branch.go
@@ -120,7 +120,7 @@ type branchData struct {
 	lineage           configdomain.Lineage
 }
 
-func branchLayout(entries []dialog.SwitchBranchEntry, data branchData) string {
+func branchLayout(entries dialog.SwitchBranchEntries, data branchData) string {
 	s := strings.Builder{}
 	initialBranch, hasInitialBranch := data.initialBranchOpt.Get()
 	for _, entry := range entries {
diff --git a/internal/cmd/debug/switch.go b/internal/cmd/debug/switch.go
index 42f2f2720..e35e52af8 100644
--- a/internal/cmd/debug/switch.go
+++ b/internal/cmd/debug/switch.go
@@ -21,7 +21,7 @@ func switchBranch() *cobra.Command {
 			if err != nil {
 				return err
 			}
-			entries := make([]dialog.SwitchBranchEntry, amount)
+			entries := make(dialog.SwitchBranchEntries, amount)
 			for a := range amount {
 				entries[a] = dialog.SwitchBranchEntry{
 					Branch:        gitdomain.NewLocalBranchName(fmt.Sprintf("branch-%d", a)),
diff --git a/internal/cmd/switch.go b/internal/cmd/switch.go
index 10948d643..c427cf5cf 100644
--- a/internal/cmd/switch.go
+++ b/internal/cmd/switch.go
@@ -167,7 +167,7 @@ func determineSwitchData(args []string, repo execute.OpenRepoResult, verbose con
 }
 
 // SwitchBranchCursorPos provides the initial cursor position for the "switch branch" components.
-func SwitchBranchCursorPos(entries []dialog.SwitchBranchEntry, initialBranch gitdomain.LocalBranchName) int {
+func SwitchBranchCursorPos(entries dialog.SwitchBranchEntries, initialBranch gitdomain.LocalBranchName) int {
 	for e, entry := range entries {
 		if entry.Branch == initialBranch {
 			return e
@@ -177,8 +177,8 @@ func SwitchBranchCursorPos(entries []dialog.SwitchBranchEntry, initialBranch git
 }
 
 // SwitchBranchEntries provides the entries for the "switch branch" components.
-func SwitchBranchEntries(branchInfos gitdomain.BranchInfos, branchTypes []configdomain.BranchType, branchesAndTypes configdomain.BranchesAndTypes, lineage configdomain.Lineage, defaultBranchType configdomain.BranchType, allBranches configdomain.AllBranches, regexes []*regexp.Regexp) []dialog.SwitchBranchEntry {
-	entries := make([]dialog.SwitchBranchEntry, 0, lineage.Len())
+func SwitchBranchEntries(branchInfos gitdomain.BranchInfos, branchTypes []configdomain.BranchType, branchesAndTypes configdomain.BranchesAndTypes, lineage configdomain.Lineage, defaultBranchType configdomain.BranchType, allBranches configdomain.AllBranches, regexes []*regexp.Regexp) dialog.SwitchBranchEntries {
+	entries := make(dialog.SwitchBranchEntries, 0, lineage.Len())
 	roots := lineage.Roots()
 	// add all entries from the lineage
 	for _, root := range roots {
@@ -194,6 +194,9 @@ func SwitchBranchEntries(branchInfos gitdomain.BranchInfos, branchTypes []config
 		if slices.Contains(branchesInLineage, localBranch) {
 			continue
 		}
+		if entries.ContainsBranch(localBranch) {
+			continue
+		}
 		layoutBranches(&entries, localBranch, "", lineage, branchInfos, allBranches, branchTypes, branchesAndTypes, defaultBranchType, regexes)
 	}
 	return entries
@@ -201,7 +204,7 @@ func SwitchBranchEntries(branchInfos gitdomain.BranchInfos, branchTypes []config
 
 // layoutBranches adds entries for the given branch and its children to the given entry list.
 // The entries are indented according to their position in the given lineage.
-func layoutBranches(result *[]dialog.SwitchBranchEntry, branch gitdomain.LocalBranchName, indentation string, lineage configdomain.Lineage, branchInfos gitdomain.BranchInfos, allBranches configdomain.AllBranches, branchTypes []configdomain.BranchType, branchesAndTypes configdomain.BranchesAndTypes, defaultBranchType configdomain.BranchType, regexes regexes.Regexes) {
+func layoutBranches(result *dialog.SwitchBranchEntries, branch gitdomain.LocalBranchName, indentation string, lineage configdomain.Lineage, branchInfos gitdomain.BranchInfos, allBranches configdomain.AllBranches, branchTypes []configdomain.BranchType, branchesAndTypes configdomain.BranchesAndTypes, defaultBranchType configdomain.BranchType, regexes regexes.Regexes) {
 	if branchInfos.HasLocalBranch(branch) || allBranches.Enabled() {
 		var otherWorktree bool
 		if branchInfo, hasBranchInfo := branchInfos.FindByLocalName(branch).Get(); hasBranchInfo {



Test patch:
diff --git a/internal/cli/dialog/switch_branch_test.go b/internal/cli/dialog/switch_branch_test.go
index 1d5378432..fdd6b3623 100644
--- a/internal/cli/dialog/switch_branch_test.go
+++ b/internal/cli/dialog/switch_branch_test.go
@@ -12,6 +12,33 @@ import (
 func TestSwitchBranch(t *testing.T) {
 	t.Parallel()
 
+	t.Run("SwitchBranchEntries", func(t *testing.T) {
+		t.Parallel()
+		t.Run("ContainsBranch", func(t *testing.T) {
+			t.Parallel()
+			t.Run("contains the branch", func(t *testing.T) {
+				t.Parallel()
+				entries := dialog.SwitchBranchEntries{
+					{Branch: "branch-1"},
+					{Branch: "branch-2"},
+				}
+				must.True(t, entries.ContainsBranch("branch-1"))
+			})
+			t.Run("does not contain the branch", func(t *testing.T) {
+				t.Parallel()
+				entries := dialog.SwitchBranchEntries{
+					{Branch: "branch-1"},
+				}
+				must.False(t, entries.ContainsBranch("branch-2"))
+			})
+			t.Run("empty", func(t *testing.T) {
+				t.Parallel()
+				entries := dialog.SwitchBranchEntries{}
+				must.False(t, entries.ContainsBranch("branch-2"))
+			})
+		})
+	})
+
 	t.Run("View", func(t *testing.T) {
 		t.Parallel()
 		t.Run("only the main branch exists", func(t *testing.T) {
@@ -19,7 +46,7 @@ func TestSwitchBranch(t *testing.T) {
 			model := dialog.SwitchModel{
 				List: list.List[dialog.SwitchBranchEntry]{
 					Cursor: 0,
-					Entries: newSwitchBranchBubbleListEntries([]dialog.SwitchBranchEntry{
+					Entries: newSwitchBranchBubbleListEntries(dialog.SwitchBranchEntries{
 						{Branch: "main", Indentation: "", OtherWorktree: false},
 					}),
 					MaxDigits:    1,
@@ -43,7 +70,7 @@ func TestSwitchBranch(t *testing.T) {
 			model := dialog.SwitchModel{
 				List: list.List[dialog.SwitchBranchEntry]{
 					Cursor: 0,
-					Entries: newSwitchBranchBubbleListEntries([]dialog.SwitchBranchEntry{
+					Entries: newSwitchBranchBubbleListEntries(dialog.SwitchBranchEntries{
 						{Branch: "main", Indentation: "", OtherWorktree: false, Type: configdomain.BranchTypeMainBranch},
 						{Branch: "one", Indentation: "", OtherWorktree: false, Type: configdomain.BranchTypeFeatureBranch},
 						{Branch: "two", Indentation: "", OtherWorktree: true, Type: configdomain.BranchTypeFeatureBranch},
@@ -74,7 +101,7 @@ func TestSwitchBranch(t *testing.T) {
 			model := dialog.SwitchModel{
 				List: list.List[dialog.SwitchBranchEntry]{
 					Cursor: 0,
-					Entries: newSwitchBranchBubbleListEntries([]dialog.SwitchBranchEntry{
+					Entries: newSwitchBranchBubbleListEntries(dialog.SwitchBranchEntries{
 						{Branch: "main", Indentation: "", OtherWorktree: false, Type: configdomain.BranchTypeMainBranch},
 						{Branch: "alpha", Indentation: "  ", OtherWorktree: false, Type: configdomain.BranchTypeFeatureBranch},
 						{Branch: "alpha1", Indentation: "    ", OtherWorktree: false, Type: configdomain.BranchTypeFeatureBranch},
@@ -113,7 +140,7 @@ func TestSwitchBranch(t *testing.T) {
 			model := dialog.SwitchModel{
 				List: list.List[dialog.SwitchBranchEntry]{
 					Cursor: 0,
-					Entries: newSwitchBranchBubbleListEntries([]dialog.SwitchBranchEntry{
+					Entries: newSwitchBranchBubbleListEntries(dialog.SwitchBranchEntries{
 						{Branch: "main", Indentation: "", OtherWorktree: false, Type: configdomain.BranchTypeMainBranch},
 						{Branch: "alpha", Indentation: "  ", OtherWorktree: false, Type: configdomain.BranchTypeFeatureBranch},
 						{Branch: "alpha1", Indentation: "    ", OtherWorktree: false, Type: configdomain.BranchTypeFeatureBranch},
@@ -152,7 +179,7 @@ func TestSwitchBranch(t *testing.T) {
 			model := dialog.SwitchModel{
 				List: list.List[dialog.SwitchBranchEntry]{
 					Cursor: 0,
-					Entries: newSwitchBranchBubbleListEntries([]dialog.SwitchBranchEntry{
+					Entries: newSwitchBranchBubbleListEntries(dialog.SwitchBranchEntries{
 						{Branch: "main", Indentation: "", OtherWorktree: false, Type: configdomain.BranchTypeMainBranch},
 					}),
 					MaxDigits:    1,
@@ -178,7 +205,7 @@ func TestSwitchBranch(t *testing.T) {
 	})
 }
 
-func newSwitchBranchBubbleListEntries(entries []dialog.SwitchBranchEntry) []list.Entry[dialog.SwitchBranchEntry] {
+func newSwitchBranchBubbleListEntries(entries dialog.SwitchBranchEntries) []list.Entry[dialog.SwitchBranchEntry] {
 	result := make([]list.Entry[dialog.SwitchBranchEntry], len(entries))
 	for e, entry := range entries {
 		result[e] = list.Entry[dialog.SwitchBranchEntry]{
diff --git a/internal/cmd/switch_test.go b/internal/cmd/switch_test.go
index 105464a14..2935e71f1 100644
--- a/internal/cmd/switch_test.go
+++ b/internal/cmd/switch_test.go
@@ -25,7 +25,7 @@ func TestSwitchBranch(t *testing.T) {
 		t.Parallel()
 		t.Run("initialBranch is in the entry list", func(t *testing.T) {
 			t.Parallel()
-			entries := []dialog.SwitchBranchEntry{
+			entries := dialog.SwitchBranchEntries{
 				{Branch: "main", Indentation: "", OtherWorktree: false},
 				{Branch: "alpha", Indentation: "", OtherWorktree: false},
 				{Branch: "alpha1", Indentation: "", OtherWorktree: false},
@@ -38,7 +38,7 @@ func TestSwitchBranch(t *testing.T) {
 		})
 		t.Run("initialBranch is not in the entry list", func(t *testing.T) {
 			t.Parallel()
-			entries := []dialog.SwitchBranchEntry{
+			entries := dialog.SwitchBranchEntries{
 				{Branch: "main", Indentation: "", OtherWorktree: false},
 				{Branch: "alpha", Indentation: "", OtherWorktree: false},
 				{Branch: "beta", Indentation: "", OtherWorktree: false},
@@ -70,7 +70,7 @@ func TestSwitchBranch(t *testing.T) {
 				defaultBranchType := configdomain.BranchTypeFeatureBranch
 				regexes := []*regexp.Regexp{}
 				have := cmd.SwitchBranchEntries(branchInfos, branchTypes, branchesAndTypes, lineage, defaultBranchType, false, regexes)
-				want := []dialog.SwitchBranchEntry{
+				want := dialog.SwitchBranchEntries{
 					{Branch: "main", Indentation: "", OtherWorktree: false},
 					{Branch: "alpha", Indentation: "  ", OtherWorktree: false},
 					{Branch: "beta", Indentation: "  ", OtherWorktree: false},
@@ -93,7 +93,7 @@ func TestSwitchBranch(t *testing.T) {
 				defaultBranchType := configdomain.BranchTypeFeatureBranch
 				regexes := []*regexp.Regexp{}
 				have := cmd.SwitchBranchEntries(branchInfos, branchTypes, branchesAndTypes, lineage, defaultBranchType, false, regexes)
-				want := []dialog.SwitchBranchEntry{
+				want := dialog.SwitchBranchEntries{
 					{Branch: "main", Indentation: "", OtherWorktree: false},
 					{Branch: "alpha", Indentation: "  ", OtherWorktree: false},
 					{Branch: "beta", Indentation: "  ", OtherWorktree: true},
@@ -120,7 +120,7 @@ func TestSwitchBranch(t *testing.T) {
 			defaultBranchType := configdomain.BranchTypeFeatureBranch
 			regexes := []*regexp.Regexp{}
 			have := cmd.SwitchBranchEntries(branchInfos, branchTypes, branchesAndTypes, lineage, defaultBranchType, false, regexes)
-			want := []dialog.SwitchBranchEntry{
+			want := dialog.SwitchBranchEntries{
 				{Branch: "main", Indentation: "", OtherWorktree: false},
 				{Branch: "alpha", Indentation: "  ", OtherWorktree: false},
 				{Branch: "beta", Indentation: "  ", OtherWorktree: false},
@@ -146,7 +146,7 @@ func TestSwitchBranch(t *testing.T) {
 				defaultBranchType := configdomain.BranchTypeFeatureBranch
 				regexes := []*regexp.Regexp{}
 				have := cmd.SwitchBranchEntries(branchInfos, branchTypes, branchesAndTypes, lineage, defaultBranchType, false, regexes)
-				want := []dialog.SwitchBranchEntry{
+				want := dialog.SwitchBranchEntries{
 					{Branch: "main", Indentation: "", OtherWorktree: false},
 					{Branch: "local", Indentation: "", OtherWorktree: false},
 				}
@@ -172,7 +172,7 @@ func TestSwitchBranch(t *testing.T) {
 				defaultBranchType := configdomain.BranchTypeFeatureBranch
 				regexes := []*regexp.Regexp{}
 				have := cmd.SwitchBranchEntries(branchInfos, branchTypes, branchesAndTypes, lineage, defaultBranchType, false, regexes)
-				want := []dialog.SwitchBranchEntry{
+				want := dialog.SwitchBranchEntries{
 					{Branch: "main", Indentation: "", OtherWorktree: false},
 					{Branch: "grandchild", Indentation: "    ", OtherWorktree: false},
 				}
@@ -195,7 +195,7 @@ func TestSwitchBranch(t *testing.T) {
 				defaultBranchType := configdomain.BranchTypeFeatureBranch
 				regexes := []*regexp.Regexp{}
 				have := cmd.SwitchBranchEntries(branchInfos, branchTypes, branchesAndTypes, lineage, defaultBranchType, true, regexes)
-				want := []dialog.SwitchBranchEntry{
+				want := dialog.SwitchBranchEntries{
 					{Branch: "main", Indentation: "", OtherWorktree: false},
 					{Branch: "local", Indentation: "  ", OtherWorktree: false},
 					{Branch: "remote", Indentation: "", OtherWorktree: false},
@@ -229,7 +229,7 @@ func TestSwitchBranch(t *testing.T) {
 				defaultBranchType := configdomain.BranchTypeFeatureBranch
 				regexes := []*regexp.Regexp{}
 				have := cmd.SwitchBranchEntries(branchInfos, branchTypes, branchesAndTypes, lineage, defaultBranchType, false, regexes)
-				want := []dialog.SwitchBranchEntry{
+				want := dialog.SwitchBranchEntries{
 					{Branch: "observed-1", Indentation: "", OtherWorktree: false, Type: configdomain.BranchTypeObservedBranch},
 					{Branch: "observed-2", Indentation: "", OtherWorktree: false, Type: configdomain.BranchTypeObservedBranch},
 				}
@@ -261,7 +261,7 @@ func TestSwitchBranch(t *testing.T) {
 				defaultBranchType := configdomain.BranchTypeFeatureBranch
 				regexes := []*regexp.Regexp{}
 				have := cmd.SwitchBranchEntries(branchInfos, branchTypes, branchesAndTypes, lineage, defaultBranchType, false, regexes)
-				want := []dialog.SwitchBranchEntry{
+				want := dialog.SwitchBranchEntries{
 					{Branch: "observed-1", Indentation: "", OtherWorktree: false, Type: configdomain.BranchTypeObservedBranch},
 					{Branch: "observed-2", Indentation: "", OtherWorktree: false, Type: configdomain.BranchTypeObservedBranch},
 					{Branch: "perennial", Indentation: "", OtherWorktree: false, Type: configdomain.BranchTypePerennialBranch},
@@ -290,7 +290,7 @@ func TestSwitchBranch(t *testing.T) {
 				regexes, err := regexes.NewRegexes([]string{})
 				must.NoError(t, err)
 				have := cmd.SwitchBranchEntries(branchInfos, branchTypes, branchesAndTypes, lineage, defaultBranchType, false, regexes)
-				want := []dialog.SwitchBranchEntry{
+				want := dialog.SwitchBranchEntries{
 					{Branch: "main", Indentation: "", OtherWorktree: false},
 					{Branch: "observed-1", Indentation: "", OtherWorktree: false},
 					{Branch: "observed-2", Indentation: "", OtherWorktree: false},
@@ -317,7 +317,7 @@ func TestSwitchBranch(t *testing.T) {
 				regexes, err := regexes.NewRegexes([]string{"observed-"})
 				must.NoError(t, err)
 				have := cmd.SwitchBranchEntries(branchInfos, branchTypes, branchesAndTypes, lineage, defaultBranchType, false, regexes)
-				want := []dialog.SwitchBranchEntry{
+				want := dialog.SwitchBranchEntries{
 					{Branch: "observed-1", Indentation: "", OtherWorktree: false},
 					{Branch: "observed-2", Indentation: "", OtherWorktree: false},
 				}
@@ -341,7 +341,7 @@ func TestSwitchBranch(t *testing.T) {
 				regexes, err := regexes.NewRegexes([]string{"observed-", "main"})
 				must.NoError(t, err)
 				have := cmd.SwitchBranchEntries(branchInfos, branchTypes, branchesAndTypes, lineage, defaultBranchType, false, regexes)
-				want := []dialog.SwitchBranchEntry{
+				want := dialog.SwitchBranchEntries{
 					{Branch: "main", Indentation: "", OtherWorktree: false},
 					{Branch: "observed-1", Indentation: "", OtherWorktree: false},
 					{Branch: "observed-2", Indentation: "", OtherWorktree: false},