From 7d7c240f4b0d1c2ef31fe73fc021a0802aec6454 Mon Sep 17 00:00:00 2001 From: nilvng Date: Sun, 3 Nov 2024 11:36:14 +1100 Subject: [PATCH] feat: let user select pr to checkout --- pkg/cmd/pr/checkout/checkout.go | 84 +++++++++++++++++++++++++++++--- pkg/cmd/pr/list/http.go | 2 +- pkg/cmd/pr/list/list.go | 2 +- pkg/cmd/pr/shared/commentable.go | 3 ++ pkg/cmd/pr/shared/display.go | 9 ++++ 5 files changed, 90 insertions(+), 10 deletions(-) diff --git a/pkg/cmd/pr/checkout/checkout.go b/pkg/cmd/pr/checkout/checkout.go index f566cbe1d..5bb0cc113 100644 --- a/pkg/cmd/pr/checkout/checkout.go +++ b/pkg/cmd/pr/checkout/checkout.go @@ -11,6 +11,7 @@ import ( "github.com/cli/cli/v2/git" "github.com/cli/cli/v2/internal/gh" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/pkg/cmd/pr/list" "github.com/cli/cli/v2/pkg/cmd/pr/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" @@ -25,8 +26,10 @@ type CheckoutOptions struct { Remotes func() (cliContext.Remotes, error) Branch func() (string, error) - Finder shared.PRFinder + Finder shared.PRFinder + Prompter shared.Prompter + BaseRepo func() (ghrepo.Interface, error) SelectorArg string RecurseSubmodules bool Force bool @@ -42,12 +45,14 @@ func NewCmdCheckout(f *cmdutil.Factory, runF func(*CheckoutOptions) error) *cobr Config: f.Config, Remotes: f.Remotes, Branch: f.Branch, + Prompter: f.Prompter, + BaseRepo: f.BaseRepo, } cmd := &cobra.Command{ - Use: "checkout { | | }", + Use: "checkout [ | | ]", Short: "Check out a pull request in git", - Args: cmdutil.ExactArgs(1, "argument required"), + Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.Finder = shared.NewFinder(f) @@ -71,15 +76,41 @@ func NewCmdCheckout(f *cmdutil.Factory, runF func(*CheckoutOptions) error) *cobr } func checkoutRun(opts *CheckoutOptions) error { - findOptions := shared.FindOptions{ - Selector: opts.SelectorArg, - Fields: []string{"number", "headRefName", "headRepository", "headRepositoryOwner", "isCrossRepository", "maintainerCanModify"}, - } - pr, baseRepo, err := opts.Finder.Find(findOptions) + baseRepo, err := opts.BaseRepo() if err != nil { return err } + var pr *api.PullRequest + + if len(opts.SelectorArg) > 0 { + + findOptions := shared.FindOptions{ + Selector: opts.SelectorArg, + Fields: []string{"number", "headRefName", "headRepository", "headRepositoryOwner", "isCrossRepository", "maintainerCanModify"}, + } + pr0, _, err := opts.Finder.Find(findOptions) + if err != nil { + return err + } + + pr = pr0 + + } else { + + httpClient, err := opts.HttpClient() + if err != nil { + return err + } + + pr0, err := selectPR(httpClient, baseRepo, opts.Prompter, opts.IO.ColorScheme()) + if err != nil { + return err + } + + pr = pr0 + + } cfg, err := opts.Config() if err != nil { return err @@ -258,3 +289,40 @@ func executeCmds(client *git.Client, cmdQueue [][]string) error { } return nil } + +func selectPR(httpClient *http.Client, baseRepo ghrepo.Interface, prompter shared.Prompter, cs *iostreams.ColorScheme) (*api.PullRequest, error) { + listResult, err := list.ListPullRequests(httpClient, baseRepo, shared.FilterOptions{Entity: "pr", State: "open", Fields: []string{ + "number", + "title", + "state", + "url", + "headRefName", + "headRepositoryOwner", + "isCrossRepository", + "isDraft", + "createdAt", + }}, 30) + if err != nil { + return nil, err + } + pr, err := promptForPR(prompter, cs, *listResult) + return pr, err +} + +func promptForPR(prompter shared.Prompter, cs *iostreams.ColorScheme, jobs api.PullRequestAndTotalCount) (*api.PullRequest, error) { + candidates := []string{} + for _, pr := range jobs.PullRequests { + candidates = append(candidates, fmt.Sprintf("%s %s", shared.PRNumberTitleWithColor(cs, pr), pr.Title)) + } + + selected, err := prompter.Select("Check out a specific PR?", "", candidates) + if err != nil { + return nil, err + } + + if selected >= 0 { + return &jobs.PullRequests[selected], nil + } + + return nil, nil +} diff --git a/pkg/cmd/pr/list/http.go b/pkg/cmd/pr/list/http.go index 5887551cf..cb79b3b87 100644 --- a/pkg/cmd/pr/list/http.go +++ b/pkg/cmd/pr/list/http.go @@ -13,7 +13,7 @@ func shouldUseSearch(filters prShared.FilterOptions) bool { return filters.Draft != nil || filters.Author != "" || filters.Assignee != "" || filters.Search != "" || len(filters.Labels) > 0 } -func listPullRequests(httpClient *http.Client, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.PullRequestAndTotalCount, error) { +func ListPullRequests(httpClient *http.Client, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.PullRequestAndTotalCount, error) { if shouldUseSearch(filters) { return searchPullRequests(httpClient, repo, filters, limit) } diff --git a/pkg/cmd/pr/list/list.go b/pkg/cmd/pr/list/list.go index 93a9b8b4b..401e168e3 100644 --- a/pkg/cmd/pr/list/list.go +++ b/pkg/cmd/pr/list/list.go @@ -172,7 +172,7 @@ func listRun(opts *ListOptions) error { return opts.Browser.Browse(openURL) } - listResult, err := listPullRequests(httpClient, baseRepo, filters, opts.LimitResults) + listResult, err := ListPullRequests(httpClient, baseRepo, filters, opts.LimitResults) if err != nil { return err } diff --git a/pkg/cmd/pr/shared/commentable.go b/pkg/cmd/pr/shared/commentable.go index 7a38286d2..46a71a2b4 100644 --- a/pkg/cmd/pr/shared/commentable.go +++ b/pkg/cmd/pr/shared/commentable.go @@ -19,6 +19,9 @@ import ( type InputType int +type Prompter interface { + Select(string, string, []string) (int, error) +} const ( InputTypeEditor InputType = iota InputTypeInline diff --git a/pkg/cmd/pr/shared/display.go b/pkg/cmd/pr/shared/display.go index 7bd306104..7920eac4e 100644 --- a/pkg/cmd/pr/shared/display.go +++ b/pkg/cmd/pr/shared/display.go @@ -17,6 +17,15 @@ func StateTitleWithColor(cs *iostreams.ColorScheme, pr api.PullRequest) string { return prStateColorFunc(text.Title(pr.State)) } +func PRNumberTitleWithColor(cs *iostreams.ColorScheme, pr api.PullRequest) string { + prStateColorFunc := cs.ColorFromString((ColorForPRState(pr))) + prNumber := fmt.Sprintf("%d", pr.Number) + if pr.State == "OPEN" && pr.IsDraft { + return prStateColorFunc(text.Title(prNumber)) + } + return prStateColorFunc(text.Title(prNumber)) +} + func ColorForPRState(pr api.PullRequest) string { switch pr.State { case "OPEN":