Files
Sameen Karim 68ce60c760 Merge Stacked PRs with merge command (#307)
* merge cmd

* Refine the merge TUI and simplify the async-merge client

Follow-up polish for `gh stack merge` (the command itself landed in the
previous commit). These changes refine the interactive wizard, enrich the
PR picker, and replace the merge client's bespoke HTTP handling with the
standard go-gh REST client.

Wizard and stepper:
- Redesign the top stepper as a segmented bar: completed steps are green,
  the active step is the brightest, and upcoming steps are dimmed. Steps
  are separated by a Powerline arrow that blends into the shading, with a
  graceful fallback to abutting segments on terminals that lack the glyph
  (e.g. Apple Terminal). Set GH_STACK_POWERLINE=1/0 to override detection.
- Show the stack number in the header ("Merge stack #123").
- Hide the header and stepper once the merge is submitted so the live
  progress view stands on its own.

PR picker:
- Render each pull request on two lines: the title (white/black, a touch
  bolder when selected) above its "#number • branch" (gray, fainter when
  deselected). Titles are fetched in one batched GraphQL query (PRTitles)
  and fall back to the branch name.
- Scroll long stacks in a fixed 10-item window with persistent "N more"
  indicators, so the list no longer jumps as those hints appear and
  disappear. Add shift+up / shift+down to jump to the top or bottom.

Progress and outcome:
- Always render a status line ("Submitting merge request...") so it does
  not pop in later and shift the view, and normalize messages to end in an
  ellipsis.
- Print the final result from the command layer rather than the TUI: a
  success line that includes the merge commit SHA
  ("Merged #1, #2 into main (abc1234)"), an atomic-rollback note on
  failure, a distinct message when the user stops watching an in-flight
  merge, and "Cancelled operation, nothing merged" on cancel.
- Clamp every rendered line to the terminal width so resizing no longer
  leaves duplicated header lines behind, and make truncation ANSI-aware.

Async-merge client:
- Use the go-gh REST client (c.rest.Put / c.rest.Get) for both the submit
  and poll endpoints, removing the bespoke http.Client, base-URL helper,
  and manual response decoding. The REST client discards non-2xx bodies,
  but that only costs the rare 400 message and 409 UUID: real merge
  failures still surface through the 200 poll body, and the in-range PRs
  are validated open, non-draft, and non-merged before submitting.
- Add classifyAsyncMergeError to map status codes to clear errors (404
  unavailable, 409 already exists, 400 no longer mergeable) and drop the
  now-unused AsyncMergeResult.StatusCode field. Rework the client tests to
  drive the REST client through a stub http.RoundTripper.

* warn merge queue unsupported

* update for new status field from api

* merge cmd docs

* more helpful error msgs

* update to support merge queue

* addressing review comments

* hide merge method step for merge queue

* set merge action explicitly

* address review comments to clarify docs on merge/api behavior
2026-07-29 13:32:07 -04:00

173 lines
4.0 KiB
Go

package cmd
import (
"errors"
"fmt"
"os"
"github.com/github/gh-stack/internal/config"
"github.com/github/gh-stack/internal/theme"
"github.com/spf13/cobra"
)
func RootCmd() *cobra.Command {
cfg := config.New()
root := &cobra.Command{
Use: "stack <command>",
Short: "Manage stacked branches and pull requests",
Long: `Stacked PRs let you break a large change into a chain of pull requests
that build on each other. Use ` + "`gh stack`" + ` to create and manage your stack
locally, then push to GitHub to create your stack of PRs.`,
Example: ` # Start a new stack targeting your default branch
$ gh stack init
# Or turn an existing set of branches into a stack
$ gh stack init branch1 branch2 branch3
# Make changes and commit, then add a branch to the stack
$ gh stack add branch4
# Push all branches and create/update PRs on GitHub
$ gh stack submit
# Keep your local in sync with remote
$ gh stack sync`,
Version: Version,
SilenceUsage: true,
SilenceErrors: true,
// Honor GH_STACK_THEME (auto|light|dark) before any command renders
PersistentPreRun: func(_ *cobra.Command, _ []string) {
theme.ApplyOverride()
},
}
root.SetVersionTemplate("gh stack version {{.Version}}\n")
root.SetOut(cfg.Out)
root.SetErr(cfg.Err)
root.AddGroup(
&cobra.Group{ID: "stack", Title: "Stack management:"},
&cobra.Group{ID: "remote", Title: "Remote operations:"},
&cobra.Group{ID: "nav", Title: "Navigation:"},
&cobra.Group{ID: "utils", Title: "Utilities:"},
)
defaultHelp := root.HelpFunc()
root.SetHelpFunc(func(cmd *cobra.Command, args []string) {
defaultHelp(cmd, args)
if cmd.Name() == "stack" {
out := cmd.OutOrStderr()
fmt.Fprintln(out)
fmt.Fprintln(out, "Learn more:")
fmt.Fprintln(out, " Documentation: https://gh.io/stacks")
fmt.Fprintln(out, " Feedback: https://gh.io/stacks-feedback")
}
})
// Stack management commands
initCmd := InitCmd(cfg)
initCmd.GroupID = "stack"
root.AddCommand(initCmd)
addCmd := AddCmd(cfg)
addCmd.GroupID = "stack"
root.AddCommand(addCmd)
viewCmd := ViewCmd(cfg)
viewCmd.GroupID = "stack"
root.AddCommand(viewCmd)
checkoutCmd := CheckoutCmd(cfg)
checkoutCmd.GroupID = "stack"
root.AddCommand(checkoutCmd)
modifyCmd := ModifyCmd(cfg)
modifyCmd.GroupID = "stack"
root.AddCommand(modifyCmd)
unstackCmd := UnstackCmd(cfg)
unstackCmd.GroupID = "stack"
root.AddCommand(unstackCmd)
// Remote operations commands
submitCmd := SubmitCmd(cfg)
submitCmd.GroupID = "remote"
root.AddCommand(submitCmd)
syncCmd := SyncCmd(cfg)
syncCmd.GroupID = "remote"
root.AddCommand(syncCmd)
rebaseCmd := RebaseCmd(cfg)
rebaseCmd.GroupID = "remote"
root.AddCommand(rebaseCmd)
pushCmd := PushCmd(cfg)
pushCmd.GroupID = "remote"
root.AddCommand(pushCmd)
linkCmd := LinkCmd(cfg)
linkCmd.GroupID = "remote"
root.AddCommand(linkCmd)
mergeCmd := MergeCmd(cfg)
mergeCmd.GroupID = "remote"
root.AddCommand(mergeCmd)
// Navigation commands
switchCmd := SwitchCmd(cfg)
switchCmd.GroupID = "nav"
root.AddCommand(switchCmd)
upCmd := UpCmd(cfg)
upCmd.GroupID = "nav"
root.AddCommand(upCmd)
downCmd := DownCmd(cfg)
downCmd.GroupID = "nav"
root.AddCommand(downCmd)
topCmd := TopCmd(cfg)
topCmd.GroupID = "nav"
root.AddCommand(topCmd)
bottomCmd := BottomCmd(cfg)
bottomCmd.GroupID = "nav"
root.AddCommand(bottomCmd)
trunkCmd := TrunkCmd(cfg)
trunkCmd.GroupID = "nav"
root.AddCommand(trunkCmd)
// Utility commands
aliasCmd := AliasCmd(cfg)
aliasCmd.GroupID = "utils"
root.AddCommand(aliasCmd)
feedbackCmd := FeedbackCmd(cfg)
feedbackCmd.GroupID = "utils"
root.AddCommand(feedbackCmd)
return root
}
func Execute() {
cmd := RootCmd()
// Wrap in a "gh" parent so help output shows "gh stack" instead of just "stack".
wrapCmd := &cobra.Command{Use: "gh", SilenceUsage: true, SilenceErrors: true}
wrapCmd.AddCommand(cmd)
wrapCmd.SetArgs(append([]string{"stack"}, os.Args[1:]...))
if err := wrapCmd.Execute(); err != nil {
var exitErr *ExitError
if errors.As(err, &exitErr) {
os.Exit(exitErr.Code)
}
fmt.Fprintln(cmd.ErrOrStderr(), err)
os.Exit(1)
}
}