Skip to content

Commit b39609a

Browse files
committed
disambiguate for multiple stacks
1 parent f3db397 commit b39609a

9 files changed

Lines changed: 144 additions & 12 deletions

File tree

cmd/add.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,23 @@ func runAdd(cfg *config.Config, args []string) error {
4040
return nil
4141
}
4242

43-
s := sf.FindStackForBranch(currentBranch)
43+
s, err := sf.ResolveStack(currentBranch, cfg)
44+
if err != nil {
45+
cfg.Errorf("%s", err)
46+
return nil
47+
}
4448
if s == nil {
4549
cfg.Errorf("current branch %q is not part of a stack; run 'gh stack init' first", currentBranch)
4650
return nil
4751
}
4852

53+
// Re-read current branch in case disambiguation caused a checkout
54+
currentBranch, err = git.CurrentBranch()
55+
if err != nil {
56+
cfg.Errorf("failed to get current branch: %s", err)
57+
return nil
58+
}
59+
4960
idx := s.IndexOf(currentBranch)
5061
if idx >= 0 && idx < len(s.Branches)-1 {
5162
cfg.Errorf("can only add branches on top of the stack; checkout the top branch %q first", s.Branches[len(s.Branches)-1].Branch)

cmd/init.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ func runInit(cfg *config.Config, opts *initOptions) error {
7878

7979
// Don't allow initializing a stack if the current branch is already part of another stack
8080
if currentBranch != "" {
81-
if existing := sf.FindStackForBranch(currentBranch); existing != nil {
81+
if stacks := sf.FindAllStacksForBranch(currentBranch); len(stacks) > 0 {
8282
cfg.Errorf("current branch %q is already part of a stack", currentBranch)
8383
return nil
8484
}

cmd/navigate.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,14 +170,26 @@ func loadCurrentStack(cfg *config.Config) (*stack.Stack, string, error) {
170170
return nil, "", fmt.Errorf("%s", errMsg)
171171
}
172172

173-
s := sf.FindStackForBranch(currentBranch)
173+
s, err := sf.ResolveStack(currentBranch, cfg)
174+
if err != nil {
175+
cfg.Errorf("%s", err)
176+
return nil, "", err
177+
}
174178
if s == nil {
175179
errMsg := fmt.Sprintf("current branch %q is not part of a stack", currentBranch)
176180
cfg.Errorf("current branch %q is not part of a stack", currentBranch)
177181
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"))
178182
return nil, "", fmt.Errorf("%s", errMsg)
179183
}
180184

185+
// Re-read current branch in case disambiguation caused a checkout
186+
currentBranch, err = git.CurrentBranch()
187+
if err != nil {
188+
errMsg := fmt.Sprintf("failed to get current branch: %s", err)
189+
cfg.Errorf("%s", errMsg)
190+
return nil, "", fmt.Errorf("%s", errMsg)
191+
}
192+
181193
return s, currentBranch, nil
182194
}
183195

cmd/push.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,23 @@ func runPush(cfg *config.Config, opts *pushOptions) error {
5252
return nil
5353
}
5454

55-
s := sf.FindStackForBranch(currentBranch)
55+
s, err := sf.ResolveStack(currentBranch, cfg)
56+
if err != nil {
57+
cfg.Errorf("%s", err)
58+
return nil
59+
}
5660
if s == nil {
5761
cfg.Errorf("current branch %q is not part of a stack", currentBranch)
5862
return nil
5963
}
6064

65+
// Re-read current branch in case disambiguation caused a checkout
66+
currentBranch, err = git.CurrentBranch()
67+
if err != nil {
68+
cfg.Errorf("failed to get current branch: %s", err)
69+
return nil
70+
}
71+
6172
client, err := cfg.GitHubClient()
6273
if err != nil {
6374
cfg.Errorf("failed to create GitHub client: %s", err)

cmd/rebase.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,23 @@ func runRebase(cfg *config.Config, opts *rebaseOptions) error {
9191
}
9292
}
9393

94-
s := sf.FindStackForBranch(currentBranch)
94+
s, err := sf.ResolveStack(currentBranch, cfg)
95+
if err != nil {
96+
cfg.Errorf("%s", err)
97+
return nil
98+
}
9599
if s == nil {
96100
cfg.Errorf("no stack found for branch %s", currentBranch)
97101
return nil
98102
}
99103

104+
// Re-read current branch in case disambiguation caused a checkout
105+
currentBranch, err = git.CurrentBranch()
106+
if err != nil {
107+
cfg.Errorf("failed to get current branch: %s", err)
108+
return nil
109+
}
110+
100111
cfg.Printf("Fetching origin ...")
101112
if err := git.Fetch("origin"); err != nil {
102113
cfg.Warningf("Failed to fetch origin: %v", err)
@@ -223,7 +234,10 @@ func continueRebase(cfg *config.Config, gitDir string) error {
223234

224235
// Use the saved original branch to find the stack, since git may be in
225236
// a detached HEAD state during an active rebase.
226-
s := sf.FindStackForBranch(state.OriginalBranch)
237+
s, err := sf.ResolveStack(state.OriginalBranch, cfg)
238+
if err != nil {
239+
return err
240+
}
227241
if s == nil {
228242
return fmt.Errorf("no stack found for branch %s", state.OriginalBranch)
229243
}

cmd/unstack.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@ func runUnstack(cfg *config.Config, opts *unstackOptions) error {
5555
}
5656
}
5757

58-
s := sf.FindStackForBranch(target)
58+
s, err := sf.ResolveStack(target, cfg)
59+
if err != nil {
60+
cfg.Errorf("%s", err)
61+
return nil
62+
}
5963
if s == nil {
6064
cfg.Errorf("branch %q is not part of a stack", target)
6165
return nil

cmd/view.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,24 @@ func runView(cfg *config.Config, opts *viewOptions) error {
5656
return nil
5757
}
5858

59-
s := sf.FindStackForBranch(currentBranch)
59+
s, err := sf.ResolveStack(currentBranch, cfg)
60+
if err != nil {
61+
cfg.Errorf("%s", err)
62+
return nil
63+
}
6064
if s == nil {
6165
cfg.Errorf("current branch %q is not part of a stack", currentBranch)
6266
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"))
6367
return nil
6468
}
6569

70+
// Re-read current branch in case disambiguation caused a checkout
71+
currentBranch, err = git.CurrentBranch()
72+
if err != nil {
73+
cfg.Errorf("failed to get current branch: %s", err)
74+
return nil
75+
}
76+
6677
if opts.web {
6778
return viewWeb(cfg, s)
6879
}

internal/stack/resolve.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package stack
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/cli/go-gh/v2/pkg/prompter"
8+
"github.com/github/gh-stack/internal/config"
9+
"github.com/github/gh-stack/internal/git"
10+
)
11+
12+
// ResolveStack finds the stack for the given branch, handling ambiguity when
13+
// a branch (typically a trunk) belongs to multiple stacks. If exactly one
14+
// stack matches, it is returned directly. If multiple stacks match, the user
15+
// is prompted to select one and the working tree is switched to the top branch
16+
// of the selected stack. Returns nil with no error if no stack contains the
17+
// branch.
18+
func (sf *StackFile) ResolveStack(branch string, cfg *config.Config) (*Stack, error) {
19+
stacks := sf.FindAllStacksForBranch(branch)
20+
21+
switch len(stacks) {
22+
case 0:
23+
return nil, nil
24+
case 1:
25+
return stacks[0], nil
26+
}
27+
28+
if !cfg.IsInteractive() {
29+
return nil, fmt.Errorf("branch %q belongs to multiple stacks; use an interactive terminal to select one", branch)
30+
}
31+
32+
cfg.Warningf("Branch %q is the trunk of multiple stacks\n", branch)
33+
34+
options := make([]string, len(stacks))
35+
for i, s := range stacks {
36+
options[i] = s.DisplayName()
37+
}
38+
39+
p := prompter.New(os.Stdin, os.Stdout, os.Stderr)
40+
selected, err := p.Select("Which stack would you like to use?", "", options)
41+
if err != nil {
42+
return nil, fmt.Errorf("stack selection: %w", err)
43+
}
44+
45+
s := stacks[selected]
46+
47+
// Switch to the top branch of the selected stack so future commands
48+
// resolve unambiguously.
49+
topBranch := s.Branches[len(s.Branches)-1].Branch
50+
if topBranch != branch {
51+
if err := git.CheckoutBranch(topBranch); err != nil {
52+
return nil, fmt.Errorf("failed to checkout branch %s: %w", topBranch, err)
53+
}
54+
cfg.Successf("Switched to %s\n", topBranch)
55+
}
56+
57+
return s, nil
58+
}

internal/stack/stack.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ type Stack struct {
2525
Branches []BranchRef `json:"branches"`
2626
}
2727

28+
// DisplayName returns a human-readable chain representation of the stack.
29+
// Format: (trunk) <- branch1 <- branch2 <- branch3
30+
func (s *Stack) DisplayName() string {
31+
result := "(" + s.Trunk.Branch + ")"
32+
for _, b := range s.Branches {
33+
result += " <- " + b.Branch
34+
}
35+
return result
36+
}
37+
2838
// BranchNames returns the list of branch names in order.
2939
func (s *Stack) BranchNames() []string {
3040
names := make([]string, len(s.Branches))
@@ -69,14 +79,15 @@ type StackFile struct {
6979
Stacks []Stack `json:"stacks"`
7080
}
7181

72-
// FindStackForBranch returns the stack that contains the given branch, or nil.
73-
func (sf *StackFile) FindStackForBranch(branch string) *Stack {
82+
// FindAllStacksForBranch returns all stacks that contain the given branch.
83+
func (sf *StackFile) FindAllStacksForBranch(branch string) []*Stack {
84+
var stacks []*Stack
7485
for i := range sf.Stacks {
7586
if sf.Stacks[i].Contains(branch) {
76-
return &sf.Stacks[i]
87+
stacks = append(stacks, &sf.Stacks[i])
7788
}
7889
}
79-
return nil
90+
return stacks
8091
}
8192

8293
// ValidateNoDuplicateBranch checks that the branch is not already in any stack.

0 commit comments

Comments
 (0)