github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/pr/list/list.go (about) 1 package list 2 3 import ( 4 "fmt" 5 "net/http" 6 "strconv" 7 "strings" 8 "time" 9 10 "github.com/MakeNowJust/heredoc" 11 "github.com/ungtb10d/cli/v2/api" 12 "github.com/ungtb10d/cli/v2/internal/browser" 13 "github.com/ungtb10d/cli/v2/internal/ghrepo" 14 "github.com/ungtb10d/cli/v2/internal/text" 15 "github.com/ungtb10d/cli/v2/pkg/cmd/pr/shared" 16 "github.com/ungtb10d/cli/v2/pkg/cmdutil" 17 "github.com/ungtb10d/cli/v2/pkg/iostreams" 18 "github.com/ungtb10d/cli/v2/utils" 19 "github.com/spf13/cobra" 20 ) 21 22 type ListOptions struct { 23 HttpClient func() (*http.Client, error) 24 IO *iostreams.IOStreams 25 BaseRepo func() (ghrepo.Interface, error) 26 Browser browser.Browser 27 28 WebMode bool 29 LimitResults int 30 Exporter cmdutil.Exporter 31 32 State string 33 BaseBranch string 34 HeadBranch string 35 Labels []string 36 Author string 37 Assignee string 38 Search string 39 Draft *bool 40 41 Now func() time.Time 42 } 43 44 func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command { 45 opts := &ListOptions{ 46 IO: f.IOStreams, 47 HttpClient: f.HttpClient, 48 Browser: f.Browser, 49 Now: time.Now, 50 } 51 52 var appAuthor string 53 54 cmd := &cobra.Command{ 55 Use: "list", 56 Short: "List pull requests in a repository", 57 Long: heredoc.Doc(` 58 List pull requests in a GitHub repository. 59 60 The search query syntax is documented here: 61 <https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests> 62 `), 63 Example: heredoc.Doc(` 64 List PRs authored by you 65 $ gh pr list --author "@me" 66 67 List only PRs with all of the given labels 68 $ gh pr list --label bug --label "priority 1" 69 70 Filter PRs using search syntax 71 $ gh pr list --search "status:success review:required" 72 73 Find a PR that introduced a given commit 74 $ gh pr list --search "<SHA>" --state merged 75 `), 76 Aliases: []string{"ls"}, 77 Args: cmdutil.NoArgsQuoteReminder, 78 RunE: func(cmd *cobra.Command, args []string) error { 79 // support `-R, --repo` override 80 opts.BaseRepo = f.BaseRepo 81 82 if opts.LimitResults < 1 { 83 return cmdutil.FlagErrorf("invalid value for --limit: %v", opts.LimitResults) 84 } 85 86 if cmd.Flags().Changed("author") && cmd.Flags().Changed("app") { 87 return cmdutil.FlagErrorf("specify only `--author` or `--app`") 88 } 89 90 if cmd.Flags().Changed("app") { 91 opts.Author = fmt.Sprintf("app/%s", appAuthor) 92 } 93 94 if runF != nil { 95 return runF(opts) 96 } 97 return listRun(opts) 98 }, 99 } 100 101 cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "List pull requests in the web browser") 102 cmd.Flags().IntVarP(&opts.LimitResults, "limit", "L", 30, "Maximum number of items to fetch") 103 cmdutil.StringEnumFlag(cmd, &opts.State, "state", "s", "open", []string{"open", "closed", "merged", "all"}, "Filter by state") 104 cmd.Flags().StringVarP(&opts.BaseBranch, "base", "B", "", "Filter by base branch") 105 cmd.Flags().StringVarP(&opts.HeadBranch, "head", "H", "", "Filter by head branch") 106 cmd.Flags().StringSliceVarP(&opts.Labels, "label", "l", nil, "Filter by label") 107 cmd.Flags().StringVarP(&opts.Author, "author", "A", "", "Filter by author") 108 cmd.Flags().StringVar(&appAuthor, "app", "", "Filter by GitHub App author") 109 cmd.Flags().StringVarP(&opts.Assignee, "assignee", "a", "", "Filter by assignee") 110 cmd.Flags().StringVarP(&opts.Search, "search", "S", "", "Search pull requests with `query`") 111 cmdutil.NilBoolFlag(cmd, &opts.Draft, "draft", "d", "Filter by draft state") 112 113 cmdutil.AddJSONFlags(cmd, &opts.Exporter, api.PullRequestFields) 114 115 return cmd 116 } 117 118 var defaultFields = []string{ 119 "number", 120 "title", 121 "state", 122 "url", 123 "headRefName", 124 "headRepositoryOwner", 125 "isCrossRepository", 126 "isDraft", 127 "createdAt", 128 } 129 130 func listRun(opts *ListOptions) error { 131 httpClient, err := opts.HttpClient() 132 if err != nil { 133 return err 134 } 135 136 baseRepo, err := opts.BaseRepo() 137 if err != nil { 138 return err 139 } 140 141 prState := strings.ToLower(opts.State) 142 if prState == "open" && shared.QueryHasStateClause(opts.Search) { 143 prState = "" 144 } 145 146 filters := shared.FilterOptions{ 147 Entity: "pr", 148 State: prState, 149 Author: opts.Author, 150 Assignee: opts.Assignee, 151 Labels: opts.Labels, 152 BaseBranch: opts.BaseBranch, 153 HeadBranch: opts.HeadBranch, 154 Search: opts.Search, 155 Draft: opts.Draft, 156 Fields: defaultFields, 157 } 158 if opts.Exporter != nil { 159 filters.Fields = opts.Exporter.Fields() 160 } 161 if opts.WebMode { 162 prListURL := ghrepo.GenerateRepoURL(baseRepo, "pulls") 163 openURL, err := shared.ListURLWithQuery(prListURL, filters) 164 if err != nil { 165 return err 166 } 167 168 if opts.IO.IsStdoutTTY() { 169 fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(openURL)) 170 } 171 return opts.Browser.Browse(openURL) 172 } 173 174 listResult, err := listPullRequests(httpClient, baseRepo, filters, opts.LimitResults) 175 if err != nil { 176 return err 177 } 178 if len(listResult.PullRequests) == 0 && opts.Exporter == nil { 179 return shared.ListNoResults(ghrepo.FullName(baseRepo), "pull request", !filters.IsDefault()) 180 } 181 182 err = opts.IO.StartPager() 183 if err != nil { 184 fmt.Fprintf(opts.IO.ErrOut, "error starting pager: %v\n", err) 185 } 186 defer opts.IO.StopPager() 187 188 if opts.Exporter != nil { 189 return opts.Exporter.Write(opts.IO, listResult.PullRequests) 190 } 191 192 if listResult.SearchCapped { 193 fmt.Fprintln(opts.IO.ErrOut, "warning: this query uses the Search API which is capped at 1000 results maximum") 194 } 195 if opts.IO.IsStdoutTTY() { 196 title := shared.ListHeader(ghrepo.FullName(baseRepo), "pull request", len(listResult.PullRequests), listResult.TotalCount, !filters.IsDefault()) 197 fmt.Fprintf(opts.IO.Out, "\n%s\n\n", title) 198 } 199 200 cs := opts.IO.ColorScheme() 201 //nolint:staticcheck // SA1019: utils.NewTablePrinter is deprecated: use internal/tableprinter 202 table := utils.NewTablePrinter(opts.IO) 203 for _, pr := range listResult.PullRequests { 204 prNum := strconv.Itoa(pr.Number) 205 if table.IsTTY() { 206 prNum = "#" + prNum 207 } 208 209 table.AddField(prNum, nil, cs.ColorFromString(shared.ColorForPRState(pr))) 210 table.AddField(text.RemoveExcessiveWhitespace(pr.Title), nil, nil) 211 table.AddField(pr.HeadLabel(), nil, cs.Cyan) 212 if !table.IsTTY() { 213 table.AddField(prStateWithDraft(&pr), nil, nil) 214 } 215 if table.IsTTY() { 216 table.AddField(text.FuzzyAgo(opts.Now(), pr.CreatedAt), nil, cs.Gray) 217 } else { 218 table.AddField(pr.CreatedAt.String(), nil, nil) 219 } 220 table.EndRow() 221 } 222 err = table.Render() 223 if err != nil { 224 return err 225 } 226 227 return nil 228 } 229 230 func prStateWithDraft(pr *api.PullRequest) string { 231 if pr.IsDraft && pr.State == "OPEN" { 232 return "DRAFT" 233 } 234 235 return pr.State 236 }