mirror of
https://github.com/github/gh-stack.git
synced 2026-09-14 20:26:28 +08:00
68ce60c760
* 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
173 lines
4.9 KiB
Go
173 lines
4.9 KiB
Go
package github
|
|
|
|
// MockClient is a test double for GitHub API operations.
|
|
// Each field is an optional function that, when set, handles the corresponding
|
|
// ClientOps method call. When nil, a reasonable default is returned.
|
|
type MockClient struct {
|
|
FindPRForBranchFn func(string) (*PullRequest, error)
|
|
FindPRByNumberFn func(int) (*PullRequest, error)
|
|
FindPRDetailsForBranchFn func(string) (*PRDetails, error)
|
|
CreatePRFn func(string, string, string, string, bool) (*PullRequest, error)
|
|
UpdatePRBaseFn func(int, string) error
|
|
MarkPRReadyForReviewFn func(string) error
|
|
DisableAutoMergeFn func(string) error
|
|
ListStacksFn func() ([]RemoteStack, error)
|
|
FindStackForPRFn func(int) (*RemoteStack, error)
|
|
GetStackFn func(int) (*RemoteStack, error)
|
|
CreateStackFn func([]int) (*RemoteStack, error)
|
|
AddToStackFn func(int, []int) (*RemoteStack, error)
|
|
UnstackFn func(int) (*RemoteStack, bool, error)
|
|
RepoMergeConfigFn func() (*RepoMergeConfig, error)
|
|
MergeStackAsyncFn func(int, string, string) (*AsyncMergeResult, error)
|
|
GetAsyncMergeResultFn func(int, string) (*AsyncMergeResult, error)
|
|
PRTitlesFn func([]int) (map[int]string, error)
|
|
BaseBranchUsesMergeQueueFn func(string) (bool, error)
|
|
}
|
|
|
|
// Compile-time check that MockClient satisfies ClientOps.
|
|
var _ ClientOps = (*MockClient)(nil)
|
|
|
|
func (m *MockClient) FindPRForBranch(branch string) (*PullRequest, error) {
|
|
if m.FindPRForBranchFn != nil {
|
|
return m.FindPRForBranchFn(branch)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *MockClient) FindPRByNumber(number int) (*PullRequest, error) {
|
|
if m.FindPRByNumberFn != nil {
|
|
return m.FindPRByNumberFn(number)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *MockClient) FindPRDetailsForBranch(branch string) (*PRDetails, error) {
|
|
if m.FindPRDetailsForBranchFn != nil {
|
|
return m.FindPRDetailsForBranchFn(branch)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *MockClient) CreatePR(base, head, title, body string, draft bool) (*PullRequest, error) {
|
|
if m.CreatePRFn != nil {
|
|
return m.CreatePRFn(base, head, title, body, draft)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *MockClient) UpdatePRBase(number int, base string) error {
|
|
if m.UpdatePRBaseFn != nil {
|
|
return m.UpdatePRBaseFn(number, base)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *MockClient) MarkPRReadyForReview(prID string) error {
|
|
if m.MarkPRReadyForReviewFn != nil {
|
|
return m.MarkPRReadyForReviewFn(prID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *MockClient) DisableAutoMerge(prID string) error {
|
|
if m.DisableAutoMergeFn != nil {
|
|
return m.DisableAutoMergeFn(prID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *MockClient) ListStacks() ([]RemoteStack, error) {
|
|
if m.ListStacksFn != nil {
|
|
return m.ListStacksFn()
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *MockClient) FindStackForPR(prNumber int) (*RemoteStack, error) {
|
|
if m.FindStackForPRFn != nil {
|
|
return m.FindStackForPRFn(prNumber)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *MockClient) GetStack(stackNumber int) (*RemoteStack, error) {
|
|
if m.GetStackFn != nil {
|
|
return m.GetStackFn(stackNumber)
|
|
}
|
|
return &RemoteStack{}, nil
|
|
}
|
|
|
|
func (m *MockClient) CreateStack(prNumbers []int) (*RemoteStack, error) {
|
|
if m.CreateStackFn != nil {
|
|
return m.CreateStackFn(prNumbers)
|
|
}
|
|
return &RemoteStack{}, nil
|
|
}
|
|
|
|
func (m *MockClient) AddToStack(stackNumber int, prNumbers []int) (*RemoteStack, error) {
|
|
if m.AddToStackFn != nil {
|
|
return m.AddToStackFn(stackNumber, prNumbers)
|
|
}
|
|
return &RemoteStack{}, nil
|
|
}
|
|
|
|
func (m *MockClient) Unstack(stackNumber int) (*RemoteStack, bool, error) {
|
|
if m.UnstackFn != nil {
|
|
return m.UnstackFn(stackNumber)
|
|
}
|
|
return nil, false, nil
|
|
}
|
|
|
|
func (m *MockClient) RepoMergeConfig() (*RepoMergeConfig, error) {
|
|
if m.RepoMergeConfigFn != nil {
|
|
return m.RepoMergeConfigFn()
|
|
}
|
|
return &RepoMergeConfig{
|
|
MergeAllowed: true,
|
|
SquashAllowed: true,
|
|
RebaseAllowed: true,
|
|
DefaultMethod: MergeMethodMerge,
|
|
}, nil
|
|
}
|
|
|
|
func (m *MockClient) MergeStackAsync(prNumber int, method, mergeAction string) (*AsyncMergeResult, error) {
|
|
if m.MergeStackAsyncFn != nil {
|
|
return m.MergeStackAsyncFn(prNumber, method, mergeAction)
|
|
}
|
|
return &AsyncMergeResult{
|
|
Status: AsyncMergeStatusPending,
|
|
Details: AsyncMergeDetails{
|
|
Message: "Merge request enqueued.",
|
|
UUID: "mock-uuid",
|
|
MergeMethod: method,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (m *MockClient) GetAsyncMergeResult(prNumber int, uuid string) (*AsyncMergeResult, error) {
|
|
if m.GetAsyncMergeResultFn != nil {
|
|
return m.GetAsyncMergeResultFn(prNumber, uuid)
|
|
}
|
|
return &AsyncMergeResult{
|
|
Status: AsyncMergeStatusMerged,
|
|
Details: AsyncMergeDetails{
|
|
Message: "Pull request was merged.",
|
|
SHA: "mockmergesha",
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (m *MockClient) PRTitles(numbers []int) (map[int]string, error) {
|
|
if m.PRTitlesFn != nil {
|
|
return m.PRTitlesFn(numbers)
|
|
}
|
|
return map[int]string{}, nil
|
|
}
|
|
|
|
func (m *MockClient) BaseBranchUsesMergeQueue(baseRef string) (bool, error) {
|
|
if m.BaseBranchUsesMergeQueueFn != nil {
|
|
return m.BaseBranchUsesMergeQueueFn(baseRef)
|
|
}
|
|
return false, nil
|
|
}
|