Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ When a remote stack is referenced, the command fetches the stack on GitHub, pull

When a branch name is provided, the command resolves it against locally tracked stacks only.

When run without arguments in an interactive terminal, opens a searchable picker listing every stack available to you — both the stacks tracked locally and the stacks that exist only on GitHub. Each row shows the stack number, its bottom and top branch, base branch, a status bar summarizing how many of its pull requests are merged, open, closed, or not yet pushed, and whether the stack is available locally or only on the remote. Filter with the All / Local / Remote tabs or type `/` to search; fully merged stacks are omitted. Selecting a remote-only stack clones it locally before switching to it.
When run without arguments in an interactive terminal, first checks whether the current branch belongs to a stack on remote that is not tracked locally, and offers to check it out. If there is no unique match or you decline, it opens a searchable picker listing every stack available to you — both the stacks tracked locally and the stacks that exist only on GitHub. Each row shows the stack number, its bottom and top branch, base branch, a status bar summarizing how many of its pull requests are merged, open, closed, or not yet pushed, and whether the stack is available locally or only on the remote. Filter with the All / Local / Remote tabs or type `/` to search; fully merged stacks are omitted. Selecting a remote-only stack clones it locally before switching to it.

**Examples:**

Expand Down
106 changes: 99 additions & 7 deletions cmd/checkout.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,12 @@ it simply switches to the branch.
When a branch name is provided, the command resolves it against
locally tracked stacks only.

When run without arguments, opens an interactive picker listing every
stack available to you — both the stacks tracked locally and the stacks
that exist only on GitHub — so you can search, filter, and check one out.
Fully merged stacks are omitted.`,
When run without arguments, first checks whether the current branch belongs
to a stack on remote that is not tracked locally, and offers to check
it out. Otherwise, it opens an interactive picker listing every stack available
to you — both the stacks tracked locally and the stacks that exist only on
GitHub — so you can search, filter, and check one out. Fully merged stacks are
omitted.`,
Example: ` # Check out a stack by its stack number
$ gh stack checkout 7

Expand Down Expand Up @@ -643,7 +645,22 @@ func interactiveCheckout(cfg *config.Config, sf *stack.StackFile, gitDir string)
return nil, "", fmt.Errorf("no target specified; provide a branch name or PR number, or run interactively to select a stack")
}

rows := gatherCheckoutRows(cfg, sf)
rows, remoteStacks := gatherCheckoutRows(cfg, sf)
if currentBranch, branchErr := git.CurrentBranch(); branchErr == nil {
stackNumber, confirmed, confirmErr := offerRemoteStackForBranch(cfg, sf, remoteStacks, currentBranch)
if confirmErr != nil {
// The confirmation helper only returns an error for an explicit
// interrupt and has already printed the friendly message.
return nil, "", ErrSilent
}
if confirmed {
return resolveCheckoutSelection(cfg, sf, gitDir, checkoutview.StackRow{
Number: stackNumber,
Type: checkoutview.TypeRemote,
})
}
}

if len(rows) == 0 {
cfg.Infof("No stacks available to check out")
cfg.Printf("Create a stack with `%s` or check out a stack by number with `%s`",
Expand All @@ -668,14 +685,89 @@ func interactiveCheckout(cfg *config.Config, sf *stack.StackFile, gitDir string)
// with the local stacks into the picker's rows. Any GitHub failure (stacks not
// enabled for the repo, no auth, network error) gracefully degrades to a
// local-only list.
func gatherCheckoutRows(cfg *config.Config, sf *stack.StackFile) []checkoutview.StackRow {
func gatherCheckoutRows(cfg *config.Config, sf *stack.StackFile) ([]checkoutview.StackRow, []github.RemoteStack) {
var remote []github.RemoteStack
if client, err := cfg.GitHubClient(); err == nil {
if stacks, err := client.ListStacks(); err == nil {
remote = stacks
}
}
return checkoutview.BuildRows(sf.Stacks, remote)
return checkoutview.BuildRows(sf.Stacks, remote), remote
}

// offerRemoteStackForBranch asks to check out the unique active remote stack
// containing branch when the branch is not already associated with a local
// stack. Every outcome except confirmation or Ctrl+C falls through to the
// existing picker.
func offerRemoteStackForBranch(cfg *config.Config, sf *stack.StackFile, remote []github.RemoteStack, branch string) (int, bool, error) {
if branch == "" || len(sf.FindAllStacksForBranch(branch)) > 0 {
return 0, false, nil
}

matches := matchingRemoteStacksForBranch(remote, branch)
if len(matches) != 1 {
return 0, false, nil
Comment thread
skarim marked this conversation as resolved.
}

stackNumber := matches[0].Number
prompt := fmt.Sprintf("Found stack #%d that includes branch %q. Check out stack #%d?", stackNumber, branch, stackNumber)
confirmed, err := confirmRemoteStackCheckout(cfg, prompt)
if err != nil {
if errors.Is(err, errInterrupt) {
return 0, false, err
}
return 0, false, nil
}
if !confirmed {
return 0, false, nil
Comment thread
skarim marked this conversation as resolved.
}
return stackNumber, true, nil
}

// matchingRemoteStacksForBranch returns picker-eligible remote stacks that
// contain branch exactly once per stack. Empty and fully merged stacks are not
// actionable and are omitted, matching the picker.
func matchingRemoteStacksForBranch(remote []github.RemoteStack, branch string) []*github.RemoteStack {
var matches []*github.RemoteStack
for i := range remote {
rs := &remote[i]
if rs.Number <= 0 || len(rs.PRDetails) == 0 {
continue
}

containsBranch := false
hasUnmergedPR := false
for _, pr := range rs.PRDetails {
if pr.Head.Ref == branch {
containsBranch = true
}
if !pr.IsMerged() {
hasUnmergedPR = true
}
}
if containsBranch && hasUnmergedPR {
matches = append(matches, rs)
}
}
return matches
}

func confirmRemoteStackCheckout(cfg *config.Config, prompt string) (bool, error) {
var (
confirmed bool
err error
)
if cfg.ConfirmFn != nil {
confirmed, err = cfg.ConfirmFn(prompt, true)
} else {
p := prompter.New(cfg.In, cfg.Out, cfg.Err)
confirmed, err = p.Confirm(prompt, true)
}
if isInterruptError(err) {
printInterrupt(cfg)
return false, errInterrupt
}
return confirmed, err
}

// resolveCheckoutSelection resolves a picker selection to a local stack and the
Expand Down
Loading