From e57c86e6a58331db9f27ead95cdeb886f98430c0 Mon Sep 17 00:00:00 2001 From: Sameen Karim Date: Mon, 24 Aug 2026 21:31:51 -0400 Subject: [PATCH 1/2] detect remote stack from current branch --- README.md | 2 +- cmd/checkout.go | 106 ++++++++- cmd/checkout_picker_test.go | 287 ++++++++++++++++++++++++- docs/src/content/docs/reference/cli.md | 2 +- internal/github/github.go | 15 +- internal/github/github_test.go | 51 +++++ 6 files changed, 449 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 2729d0a3..9e17700a 100644 --- a/README.md +++ b/README.md @@ -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 is untracked locally but belongs to exactly one active stack on GitHub 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:** diff --git a/cmd/checkout.go b/cmd/checkout.go index e1067e18..f291856e 100644 --- a/cmd/checkout.go +++ b/cmd/checkout.go @@ -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 exactly one stack on GitHub 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 @@ -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`", @@ -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 + } + + 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 + } + 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 diff --git a/cmd/checkout_picker_test.go b/cmd/checkout_picker_test.go index a97e0b55..60ca17f4 100644 --- a/cmd/checkout_picker_test.go +++ b/cmd/checkout_picker_test.go @@ -4,6 +4,7 @@ import ( "errors" "testing" + "github.com/AlecAivazis/survey/v2/terminal" "github.com/cli/go-gh/v2/pkg/api" "github.com/github/gh-stack/internal/config" "github.com/github/gh-stack/internal/git" @@ -40,8 +41,9 @@ func TestGatherCheckoutRows_FallbackToLocalOnListError(t *testing.T) { }, }}} - rows := gatherCheckoutRows(cfg, sf) + rows, remote := gatherCheckoutRows(cfg, sf) require.Len(t, rows, 1, "falls back to a local-only list when ListStacks fails") + assert.Empty(t, remote) assert.Equal(t, checkoutview.TypeLocal, rows[0].Type) assert.Equal(t, 5, rows[0].Number) } @@ -68,8 +70,9 @@ func TestGatherCheckoutRows_IncludesRemoteOnlyStacks(t *testing.T) { Branches: []stack.BranchRef{{Branch: "local-a"}}, }}} - rows := gatherCheckoutRows(cfg, sf) + rows, remote := gatherCheckoutRows(cfg, sf) require.Len(t, rows, 2, "local and remote-only stacks are both listed") + require.Len(t, remote, 1) var haveLocal, haveRemote bool for _, r := range rows { @@ -85,6 +88,286 @@ func TestGatherCheckoutRows_IncludesRemoteOnlyStacks(t *testing.T) { assert.True(t, haveRemote, "remote-only stack present") } +func TestMatchingRemoteStacksForBranch(t *testing.T) { + mergedAt := "2026-08-24T12:00:00Z" + activeMatch := github.RemoteStack{ + Number: 1, + PRDetails: []github.RemoteStackPR{ + {Number: 10, State: "open", Head: github.RemoteStackPRHead{Ref: "feature"}}, + {Number: 11, State: "open", Head: github.RemoteStackPRHead{Ref: "top"}}, + }, + } + secondActiveMatch := github.RemoteStack{ + Number: 2, + PRDetails: []github.RemoteStackPR{ + {Number: 20, State: "open", Head: github.RemoteStackPRHead{Ref: "feature"}}, + }, + } + fullyMergedMatch := github.RemoteStack{ + Number: 3, + PRDetails: []github.RemoteStackPR{ + {Number: 30, State: "closed", MergedAt: &mergedAt, Head: github.RemoteStackPRHead{Ref: "feature"}}, + }, + } + + tests := []struct { + name string + remote []github.RemoteStack + branch string + want []int + }{ + { + name: "one active match", + remote: []github.RemoteStack{activeMatch}, + branch: "feature", + want: []int{1}, + }, + { + name: "multiple active matches", + remote: []github.RemoteStack{activeMatch, secondActiveMatch}, + branch: "feature", + want: []int{1, 2}, + }, + { + name: "exact branch name only", + remote: []github.RemoteStack{{ + Number: 4, + PRDetails: []github.RemoteStackPR{ + {Number: 40, State: "open", Head: github.RemoteStackPRHead{Ref: "origin/feature"}}, + }, + }}, + branch: "feature", + want: []int{}, + }, + { + name: "fully merged match omitted", + remote: []github.RemoteStack{fullyMergedMatch}, + branch: "feature", + want: []int{}, + }, + { + name: "empty and unaddressable stacks omitted", + remote: []github.RemoteStack{ + {Number: 5}, + { + PRDetails: []github.RemoteStackPR{ + {Number: 60, State: "open", Head: github.RemoteStackPRHead{Ref: "feature"}}, + }, + }, + }, + branch: "feature", + want: []int{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + matches := matchingRemoteStacksForBranch(tt.remote, tt.branch) + numbers := make([]int, len(matches)) + for i, match := range matches { + numbers[i] = match.Number + } + assert.Equal(t, tt.want, numbers) + }) + } +} + +func TestOfferRemoteStackForBranch_Confirmed(t *testing.T) { + cfg, _, _ := config.NewTestConfig() + cfg.ConfirmFn = func(prompt string, defaultValue bool) (bool, error) { + assert.Equal(t, `Found stack #7 that includes branch "feature". Check out stack #7?`, prompt) + assert.True(t, defaultValue) + return true, nil + } + + number, confirmed, err := offerRemoteStackForBranch(cfg, &stack.StackFile{}, []github.RemoteStack{{ + Number: 7, + PRDetails: []github.RemoteStackPR{ + {Number: 10, State: "open", Head: github.RemoteStackPRHead{Ref: "feature"}}, + }, + }}, "feature") + + require.NoError(t, err) + assert.True(t, confirmed) + assert.Equal(t, 7, number) +} + +func TestOfferRemoteStackForBranch_FallsBack(t *testing.T) { + activeMatch := github.RemoteStack{ + Number: 7, + PRDetails: []github.RemoteStackPR{ + {Number: 10, State: "open", Head: github.RemoteStackPRHead{Ref: "feature"}}, + }, + } + secondMatch := github.RemoteStack{ + Number: 8, + PRDetails: []github.RemoteStackPR{ + {Number: 11, State: "open", Head: github.RemoteStackPRHead{Ref: "feature"}}, + }, + } + + tests := []struct { + name string + sf *stack.StackFile + remote []github.RemoteStack + wantPrompt bool + confirmation bool + confirmErr error + }{ + { + name: "branch already belongs to local stack", + sf: &stack.StackFile{Stacks: []stack.Stack{{ + Trunk: stack.BranchRef{Branch: "main"}, + Branches: []stack.BranchRef{{Branch: "feature"}}, + }}}, + remote: []github.RemoteStack{activeMatch}, + }, + { + name: "branch is a local stack trunk", + sf: &stack.StackFile{Stacks: []stack.Stack{{ + Trunk: stack.BranchRef{Branch: "feature"}, + Branches: []stack.BranchRef{{Branch: "other"}}, + }}}, + remote: []github.RemoteStack{activeMatch}, + }, + { + name: "multiple remote matches", + sf: &stack.StackFile{}, + remote: []github.RemoteStack{activeMatch, secondMatch}, + }, + { + name: "user declines", + sf: &stack.StackFile{}, + remote: []github.RemoteStack{activeMatch}, + wantPrompt: true, + }, + { + name: "confirmation fails", + sf: &stack.StackFile{}, + remote: []github.RemoteStack{activeMatch}, + wantPrompt: true, + confirmErr: errors.New("prompt failed"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg, _, _ := config.NewTestConfig() + prompted := false + cfg.ConfirmFn = func(string, bool) (bool, error) { + prompted = true + if !tt.wantPrompt { + t.Fatal("confirmation should not be shown") + } + return tt.confirmation, tt.confirmErr + } + + number, confirmed, err := offerRemoteStackForBranch(cfg, tt.sf, tt.remote, "feature") + + require.NoError(t, err) + assert.False(t, confirmed) + assert.Zero(t, number) + assert.Equal(t, tt.wantPrompt, prompted) + }) + } +} + +func TestOfferRemoteStackForBranch_InterruptAborts(t *testing.T) { + cfg, outR, errR := config.NewTestConfig() + cfg.ConfirmFn = func(string, bool) (bool, error) { + return false, terminal.InterruptErr + } + + number, confirmed, err := offerRemoteStackForBranch(cfg, &stack.StackFile{}, []github.RemoteStack{{ + Number: 7, + PRDetails: []github.RemoteStackPR{ + {Number: 10, State: "open", Head: github.RemoteStackPRHead{Ref: "feature"}}, + }, + }}, "feature") + output := collectOutput(cfg, outR, errR) + + assert.ErrorIs(t, err, errInterrupt) + assert.False(t, confirmed) + assert.Zero(t, number) + assert.Contains(t, output, "Received interrupt, aborting operation") +} + +func TestCheckout_NoTarget_ConfirmedRemoteMatch(t *testing.T) { + gitDir := t.TempDir() + currentBranch := "feature" + checkedOut := "" + branches := map[string]bool{"main": true, "feature": true} + restore := git.SetOps(&git.MockOps{ + GitDirFn: func() (string, error) { return gitDir, nil }, + CurrentBranchFn: func() (string, error) { return currentBranch, nil }, + BranchExistsFn: func(name string) bool { return branches[name] }, + FetchFn: func(string) error { return nil }, + ResolveRemoteFn: func(string) (string, error) { return "origin", nil }, + CreateBranchFn: func(name, _ string) error { + branches[name] = true + return nil + }, + SetUpstreamTrackingFn: func(string, string) error { return nil }, + CheckoutBranchFn: func(name string) error { + checkedOut = name + currentBranch = name + return nil + }, + RevParseFn: func(string) (string, error) { return "abc123", nil }, + RevParseMultiFn: func(refs []string) ([]string, error) { + shas := make([]string, len(refs)) + for i := range refs { + shas[i] = "abc123" + } + return shas, nil + }, + }) + defer restore() + + require.NoError(t, stack.Save(gitDir, &stack.StackFile{SchemaVersion: 1, Stacks: []stack.Stack{}})) + + listCalls := 0 + cfg, _, _ := config.NewTestConfig() + cfg.ForceInteractive = true + cfg.ConfirmFn = func(string, bool) (bool, error) { return true, nil } + cfg.GitHubClientOverride = &github.MockClient{ + ListStacksFn: func() ([]github.RemoteStack, error) { + listCalls++ + return []github.RemoteStack{{ + ID: 42, + Number: 7, + Base: github.RemoteStackBase{Ref: "main"}, + PRDetails: []github.RemoteStackPR{ + {Number: 10, State: "open", Head: github.RemoteStackPRHead{Ref: "feature"}}, + {Number: 11, State: "open", Head: github.RemoteStackPRHead{Ref: "feature-top"}}, + }, + }}, nil + }, + GetStackFn: func(int) (*github.RemoteStack, error) { + return &github.RemoteStack{ID: 42, Number: 7, PullRequests: []int{10, 11}}, nil + }, + FindPRByNumberFn: func(number int) (*github.PullRequest, error) { + prs := map[int]*github.PullRequest{ + 10: {ID: "PR_10", Number: 10, HeadRefName: "feature", BaseRefName: "main"}, + 11: {ID: "PR_11", Number: 11, HeadRefName: "feature-top", BaseRefName: "feature"}, + } + return prs[number], nil + }, + } + + err := runCheckout(cfg, &checkoutOptions{}) + + require.NoError(t, err) + assert.Equal(t, 1, listCalls, "auto-detection and picker data should share one remote list") + assert.Equal(t, "feature-top", checkedOut) + + sf, loadErr := stack.Load(gitDir) + require.NoError(t, loadErr) + require.Len(t, sf.Stacks, 1) + assert.Equal(t, 7, sf.Stacks[0].Number) + assert.Equal(t, []string{"feature", "feature-top"}, sf.Stacks[0].BranchNames()) +} + func TestResolveCheckoutSelection_Local(t *testing.T) { cfg, _, _ := config.NewTestConfig() localStack := &stack.Stack{ diff --git a/docs/src/content/docs/reference/cli.md b/docs/src/content/docs/reference/cli.md index 33084e30..043ddb65 100644 --- a/docs/src/content/docs/reference/cli.md +++ b/docs/src/content/docs/reference/cli.md @@ -135,7 +135,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 is untracked locally but belongs to exactly one active stack on GitHub 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:** diff --git a/internal/github/github.go b/internal/github/github.go index 2bd577ca..ae6a3e35 100644 --- a/internal/github/github.go +++ b/internal/github/github.go @@ -489,10 +489,19 @@ func (s *RemoteStack) PRNumbers() []int { // (descending). Returns an empty slice if no stacks exist. A 404 response // indicates stacked PRs are not enabled for this repository. func (c *Client) ListStacks() ([]RemoteStack, error) { - path := fmt.Sprintf("repos/%s/%s/stacks", c.owner, c.repo) + const perPage = 100 + var stacks []RemoteStack - if err := c.rest.Get(path, &stacks); err != nil { - return nil, err + for page := 1; ; page++ { + path := fmt.Sprintf("repos/%s/%s/stacks?per_page=%d&page=%d", c.owner, c.repo, perPage, page) + var batch []RemoteStack + if err := c.rest.Get(path, &batch); err != nil { + return nil, err + } + stacks = append(stacks, batch...) + if len(batch) < perPage { + break + } } if stacks == nil { stacks = []RemoteStack{} diff --git a/internal/github/github_test.go b/internal/github/github_test.go index 1f18edf1..baeff335 100644 --- a/internal/github/github_test.go +++ b/internal/github/github_test.go @@ -1,9 +1,14 @@ package github import ( + "bytes" "encoding/json" + "io" + "net/http" + "strconv" "testing" + "github.com/cli/go-gh/v2/pkg/api" graphql "github.com/cli/shurcooL-graphql" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -148,3 +153,49 @@ func TestRemoteStack_UnmarshalJSON_EmptyPRs(t *testing.T) { assert.Empty(t, s.PullRequests) assert.Empty(t, s.PRDetails) } + +func TestListStacks_Paginates(t *testing.T) { + firstPage := make([]RemoteStack, 100) + for i := range firstPage { + firstPage[i] = RemoteStack{ID: i + 1, Number: 101 - i} + } + secondPage := []RemoteStack{{ID: 101, Number: 1}} + firstBody, err := json.Marshal(firstPage) + require.NoError(t, err) + secondBody, err := json.Marshal(secondPage) + require.NoError(t, err) + + var requestedPages []int + transport := roundTripFunc(func(r *http.Request) (*http.Response, error) { + assert.Equal(t, "100", r.URL.Query().Get("per_page")) + page, parseErr := strconv.Atoi(r.URL.Query().Get("page")) + require.NoError(t, parseErr) + requestedPages = append(requestedPages, page) + + body := firstBody + if page == 2 { + body = secondBody + } + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader(body)), + Header: http.Header{"Content-Type": []string{"application/json"}}, + Request: r, + }, nil + }) + rest, err := api.NewRESTClient(api.ClientOptions{ + Host: "github.com", + AuthToken: "x", + Transport: transport, + }) + require.NoError(t, err) + + client := &Client{rest: rest, owner: "o", repo: "r"} + stacks, err := client.ListStacks() + + require.NoError(t, err) + require.Len(t, stacks, 101) + assert.Equal(t, []int{1, 2}, requestedPages) + assert.Equal(t, 101, stacks[0].Number) + assert.Equal(t, 1, stacks[100].Number) +} From 38a057207b7f62653100d531457b3944f0b808b1 Mon Sep 17 00:00:00 2001 From: Sameen Karim Date: Mon, 24 Aug 2026 21:55:15 -0400 Subject: [PATCH 2/2] clean up phrasing --- README.md | 2 +- cmd/checkout.go | 2 +- docs/src/content/docs/reference/cli.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9e17700a..4cdcec9b 100644 --- a/README.md +++ b/README.md @@ -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, first checks whether the current branch is untracked locally but belongs to exactly one active stack on GitHub 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. +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:** diff --git a/cmd/checkout.go b/cmd/checkout.go index f291856e..b5fbe730 100644 --- a/cmd/checkout.go +++ b/cmd/checkout.go @@ -45,7 +45,7 @@ When a branch name is provided, the command resolves it against locally tracked stacks only. When run without arguments, first checks whether the current branch belongs -to exactly one stack on GitHub that is not tracked locally and offers to check +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 diff --git a/docs/src/content/docs/reference/cli.md b/docs/src/content/docs/reference/cli.md index 043ddb65..d7e6d22c 100644 --- a/docs/src/content/docs/reference/cli.md +++ b/docs/src/content/docs/reference/cli.md @@ -135,7 +135,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, first checks whether the current branch is untracked locally but belongs to exactly one active stack on GitHub 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. +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:**