Skip to content

Commit 775d0b3

Browse files
committed
navigation commands
1 parent 2536e9a commit 775d0b3

2 files changed

Lines changed: 193 additions & 0 deletions

File tree

cmd/navigate.go

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/github/gh-stack/internal/config"
7+
"github.com/github/gh-stack/internal/git"
8+
"github.com/github/gh-stack/internal/stack"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
func UpCmd(cfg *config.Config) *cobra.Command {
13+
return &cobra.Command{
14+
Use: "up [n]",
15+
Short: "Move up in the stack (toward the top)",
16+
Args: cobra.MaximumNArgs(1),
17+
RunE: func(cmd *cobra.Command, args []string) error {
18+
n := 1
19+
if len(args) > 0 {
20+
fmt.Sscanf(args[0], "%d", &n)
21+
}
22+
return runNavigate(cfg, n)
23+
},
24+
}
25+
}
26+
27+
func DownCmd(cfg *config.Config) *cobra.Command {
28+
return &cobra.Command{
29+
Use: "down [n]",
30+
Short: "Move down in the stack (toward the trunk)",
31+
Args: cobra.MaximumNArgs(1),
32+
RunE: func(cmd *cobra.Command, args []string) error {
33+
n := 1
34+
if len(args) > 0 {
35+
fmt.Sscanf(args[0], "%d", &n)
36+
}
37+
return runNavigate(cfg, -n)
38+
},
39+
}
40+
}
41+
42+
func TopCmd(cfg *config.Config) *cobra.Command {
43+
return &cobra.Command{
44+
Use: "top",
45+
Short: "Move to the top of the stack",
46+
RunE: func(cmd *cobra.Command, args []string) error {
47+
return runNavigateToEnd(cfg, true)
48+
},
49+
}
50+
}
51+
52+
func BottomCmd(cfg *config.Config) *cobra.Command {
53+
return &cobra.Command{
54+
Use: "bottom",
55+
Short: "Move to the bottom of the stack",
56+
RunE: func(cmd *cobra.Command, args []string) error {
57+
return runNavigateToEnd(cfg, false)
58+
},
59+
}
60+
}
61+
62+
func runNavigate(cfg *config.Config, delta int) error {
63+
s, currentBranch, err := loadCurrentStack(cfg)
64+
if err != nil {
65+
return nil
66+
}
67+
68+
idx := s.IndexOf(currentBranch)
69+
if idx < 0 {
70+
// Might be on the trunk
71+
if currentBranch == s.Trunk.Branch {
72+
if delta > 0 && len(s.Branches) > 0 {
73+
target := s.Branches[0].Branch
74+
if err := git.CheckoutBranch(target); err != nil {
75+
return err
76+
}
77+
cfg.Successf("Switched to %s", target)
78+
return nil
79+
}
80+
cfg.Printf("already at the bottom of the stack")
81+
return nil
82+
}
83+
cfg.Errorf("current branch %q is not in the stack", currentBranch)
84+
return nil
85+
}
86+
87+
newIdx := idx + delta
88+
if newIdx < 0 {
89+
newIdx = 0
90+
}
91+
if newIdx >= len(s.Branches) {
92+
newIdx = len(s.Branches) - 1
93+
}
94+
95+
if newIdx == idx {
96+
if delta > 0 {
97+
cfg.Printf("Already at the top of the stack")
98+
} else {
99+
cfg.Printf("Already at the bottom of the stack")
100+
}
101+
return nil
102+
}
103+
104+
target := s.Branches[newIdx].Branch
105+
if err := git.CheckoutBranch(target); err != nil {
106+
return err
107+
}
108+
109+
moved := newIdx - idx
110+
direction := "up"
111+
if moved < 0 {
112+
direction = "down"
113+
moved = -moved
114+
}
115+
116+
cfg.Successf("Moved %s %d %s to %s", direction, moved, plural(moved, "branch", "branches"), target)
117+
return nil
118+
}
119+
120+
func runNavigateToEnd(cfg *config.Config, top bool) error {
121+
s, currentBranch, err := loadCurrentStack(cfg)
122+
if err != nil {
123+
cfg.Errorf("failed to load current stack: %s", err)
124+
return nil
125+
}
126+
127+
var target string
128+
if top {
129+
target = s.Branches[len(s.Branches)-1].Branch
130+
} else {
131+
target = s.Branches[0].Branch
132+
}
133+
134+
if target == currentBranch {
135+
if top {
136+
cfg.Printf("Already at the top of the stack")
137+
} else {
138+
cfg.Printf("Already at the bottom of the stack")
139+
}
140+
return nil
141+
}
142+
143+
if err := git.CheckoutBranch(target); err != nil {
144+
return err
145+
}
146+
147+
cfg.Successf("Switched to %s", target)
148+
return nil
149+
}
150+
151+
func loadCurrentStack(cfg *config.Config) (*stack.Stack, string, error) {
152+
gitDir, err := git.GitDir()
153+
if err != nil {
154+
errMsg := "not a git repository"
155+
cfg.Errorf("%s", errMsg)
156+
return nil, "", fmt.Errorf("%s", errMsg)
157+
}
158+
159+
sf, err := stack.Load(gitDir)
160+
if err != nil {
161+
errMsg := fmt.Sprintf("failed to load stack state: %s", err)
162+
cfg.Errorf("%s", errMsg)
163+
return nil, "", fmt.Errorf("%s", errMsg)
164+
}
165+
166+
currentBranch, err := git.CurrentBranch()
167+
if err != nil {
168+
errMsg := fmt.Sprintf("failed to get current branch: %s", err)
169+
cfg.Errorf("%s", errMsg)
170+
return nil, "", fmt.Errorf("%s", errMsg)
171+
}
172+
173+
s := sf.FindStackForBranch(currentBranch)
174+
if s == nil {
175+
errMsg := fmt.Sprintf("current branch %q is not part of a stack", currentBranch)
176+
cfg.Errorf("current branch %q is not part of a stack", currentBranch)
177+
cfg.Printf("Checkout an existing stack using %s or create a new stack using %s", cfg.ColorCyan("gh stack checkout"), cfg.ColorCyan("gh stack init"))
178+
return nil, "", fmt.Errorf("%s", errMsg)
179+
}
180+
181+
return s, currentBranch, nil
182+
}
183+
184+
func plural(n int, singular, pluralForm string) string {
185+
if n == 1 {
186+
return singular
187+
}
188+
return pluralForm
189+
}

cmd/root.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ func RootCmd() *cobra.Command {
2727

2828
// Helper commands
2929
root.AddCommand(ViewCmd(cfg))
30+
root.AddCommand(UpCmd(cfg))
31+
root.AddCommand(DownCmd(cfg))
32+
root.AddCommand(TopCmd(cfg))
33+
root.AddCommand(BottomCmd(cfg))
3034

3135
// Placeholders for upcoming features
3236
for _, ph := range placeholderCommands {

0 commit comments

Comments
 (0)