diff --git a/cmd/navigate.go b/cmd/navigate.go index 52a94b9..c459a9d 100644 --- a/cmd/navigate.go +++ b/cmd/navigate.go @@ -68,26 +68,22 @@ func runNavigate(cfg *config.Config, delta int) error { idx := s.IndexOf(currentBranch) if idx < 0 { - // Might be on the trunk - if currentBranch == s.Trunk.Branch { - if delta > 0 && len(s.Branches) > 0 { - targetIdx := s.FirstActiveBranchIndex() - if targetIdx < 0 { - // All merged — fall back to top branch with warning - targetIdx = len(s.Branches) - 1 - cfg.Warningf("Warning: all branches in this stack have been merged") - } - target := s.Branches[targetIdx].Branch - if err := git.CheckoutBranch(target); err != nil { - return err - } - cfg.Successf("Switched to %s", target) - return nil + // Current branch is the trunk (not in s.Branches). + // loadStack guarantees the branch is part of the stack. + if delta > 0 && len(s.Branches) > 0 { + targetIdx := s.FirstActiveBranchIndex() + if targetIdx < 0 { + targetIdx = len(s.Branches) - 1 + cfg.Warningf("Warning: all branches in this stack have been merged") } - cfg.Printf("Already at the bottom of the stack") + target := s.Branches[targetIdx].Branch + if err := git.CheckoutBranch(target); err != nil { + return err + } + cfg.Successf("Switched to %s", target) return nil } - cfg.Errorf("current branch %q is not in the stack", currentBranch) + cfg.Printf("Already at the bottom of the stack") return nil } @@ -110,12 +106,7 @@ func runNavigate(cfg *config.Config, delta int) error { } } else { // Build list of active (non-merged) branch indices - var activeIndices []int - for i, b := range s.Branches { - if !b.IsMerged() { - activeIndices = append(activeIndices, i) - } - } + activeIndices := s.ActiveBranchIndices() // Find current position in active list activePos := -1 diff --git a/internal/stack/stack.go b/internal/stack/stack.go index f35c83d..ef7296b 100644 --- a/internal/stack/stack.go +++ b/internal/stack/stack.go @@ -124,6 +124,17 @@ func (s *Stack) FirstActiveBranchIndex() int { return -1 } +// ActiveBranchIndices returns the indices of all non-merged branches. +func (s *Stack) ActiveBranchIndices() []int { + var indices []int + for i, b := range s.Branches { + if !b.IsMerged() { + indices = append(indices, i) + } + } + return indices +} + // ActiveBaseBranch returns the effective parent for a branch, skipping merged // ancestors. For the first active branch (or any branch whose downstack is all // merged), this returns the trunk. diff --git a/internal/stack/stack_test.go b/internal/stack/stack_test.go index d20f1a3..3fa65de 100644 --- a/internal/stack/stack_test.go +++ b/internal/stack/stack_test.go @@ -192,6 +192,39 @@ func TestFirstActiveBranchIndex(t *testing.T) { }) } +// --- ActiveBranchIndices: navigation --- + +func TestActiveBranchIndices(t *testing.T) { + t.Run("all active", func(t *testing.T) { + s := makeStack("main", "b1", "b2", "b3") + assert.Equal(t, []int{0, 1, 2}, s.ActiveBranchIndices()) + }) + + t.Run("some merged", func(t *testing.T) { + s := Stack{ + Trunk: BranchRef{Branch: "main"}, + Branches: []BranchRef{ + makeMergedBranch("b1", 1), + {Branch: "b2"}, + makeMergedBranch("b3", 3), + {Branch: "b4"}, + }, + } + assert.Equal(t, []int{1, 3}, s.ActiveBranchIndices()) + }) + + t.Run("all merged", func(t *testing.T) { + s := Stack{ + Trunk: BranchRef{Branch: "main"}, + Branches: []BranchRef{ + makeMergedBranch("b1", 1), + makeMergedBranch("b2", 2), + }, + } + assert.Empty(t, s.ActiveBranchIndices()) + }) +} + // --- Load / Save round-trip persistence --- func TestLoad_Save_RoundTrip(t *testing.T) {