github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/issue/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/config" 14 fd "github.com/ungtb10d/cli/v2/internal/featuredetection" 15 "github.com/ungtb10d/cli/v2/internal/ghrepo" 16 "github.com/ungtb10d/cli/v2/internal/text" 17 issueShared "github.com/ungtb10d/cli/v2/pkg/cmd/issue/shared" 18 "github.com/ungtb10d/cli/v2/pkg/cmd/pr/shared" 19 prShared "github.com/ungtb10d/cli/v2/pkg/cmd/pr/shared" 20 "github.com/ungtb10d/cli/v2/pkg/cmdutil" 21 "github.com/ungtb10d/cli/v2/pkg/iostreams" 22 "github.com/shurcooL/githubv4" 23 "github.com/spf13/cobra" 24 ) 25 26 type ListOptions struct { 27 HttpClient func() (*http.Client, error) 28 Config func() (config.Config, error) 29 IO *iostreams.IOStreams 30 BaseRepo func() (ghrepo.Interface, error) 31 Browser browser.Browser 32 33 Assignee string 34 Labels []string 35 State string 36 LimitResults int 37 Author string 38 Mention string 39 Milestone string 40 Search string 41 WebMode bool 42 Exporter cmdutil.Exporter 43 44 Detector fd.Detector 45 Now func() time.Time 46 } 47 48 func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command { 49 opts := &ListOptions{ 50 IO: f.IOStreams, 51 HttpClient: f.HttpClient, 52 Config: f.Config, 53 Browser: f.Browser, 54 Now: time.Now, 55 } 56 57 var appAuthor string 58 59 cmd := &cobra.Command{ 60 Use: "list", 61 Short: "List issues in a repository", 62 Long: heredoc.Doc(` 63 List issues in a GitHub repository. 64 65 The search query syntax is documented here: 66 <https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests> 67 `), 68 Example: heredoc.Doc(` 69 $ gh issue list --label "bug" --label "help wanted" 70 $ gh issue list --author monalisa 71 $ gh issue list --assignee "@me" 72 $ gh issue list --milestone "The big 1.0" 73 $ gh issue list --search "error no:assignee sort:created-asc" 74 `), 75 Aliases: []string{"ls"}, 76 Args: cmdutil.NoArgsQuoteReminder, 77 RunE: func(cmd *cobra.Command, args []string) error { 78 // support `-R, --repo` override 79 opts.BaseRepo = f.BaseRepo 80 81 if opts.LimitResults < 1 { 82 return cmdutil.FlagErrorf("invalid limit: %v", opts.LimitResults) 83 } 84 85 if cmd.Flags().Changed("author") && cmd.Flags().Changed("app") { 86 return cmdutil.FlagErrorf("specify only `--author` or `--app`") 87 } 88 89 if cmd.Flags().Changed("app") { 90 opts.Author = fmt.Sprintf("app/%s", appAuthor) 91 } 92 93 if runF != nil { 94 return runF(opts) 95 } 96 return listRun(opts) 97 }, 98 } 99 100 cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "List issues in the web browser") 101 cmd.Flags().StringVarP(&opts.Assignee, "assignee", "a", "", "Filter by assignee") 102 cmd.Flags().StringSliceVarP(&opts.Labels, "label", "l", nil, "Filter by label") 103 cmdutil.StringEnumFlag(cmd, &opts.State, "state", "s", "open", []string{"open", "closed", "all"}, "Filter by state") 104 cmd.Flags().IntVarP(&opts.LimitResults, "limit", "L", 30, "Maximum number of issues to fetch") 105 cmd.Flags().StringVarP(&opts.Author, "author", "A", "", "Filter by author") 106 cmd.Flags().StringVar(&appAuthor, "app", "", "Filter by GitHub App author") 107 cmd.Flags().StringVar(&opts.Mention, "mention", "", "Filter by mention") 108 cmd.Flags().StringVarP(&opts.Milestone, "milestone", "m", "", "Filter by milestone number or title") 109 cmd.Flags().StringVarP(&opts.Search, "search", "S", "", "Search issues with `query`") 110 cmdutil.AddJSONFlags(cmd, &opts.Exporter, api.IssueFields) 111 112 return cmd 113 } 114 115 var defaultFields = []string{ 116 "number", 117 "title", 118 "url", 119 "state", 120 "updatedAt", 121 "labels", 122 } 123 124 func listRun(opts *ListOptions) error { 125 httpClient, err := opts.HttpClient() 126 if err != nil { 127 return err 128 } 129 130 baseRepo, err := opts.BaseRepo() 131 if err != nil { 132 return err 133 } 134 135 issueState := strings.ToLower(opts.State) 136 if issueState == "open" && shared.QueryHasStateClause(opts.Search) { 137 issueState = "" 138 } 139 140 if opts.Detector == nil { 141 cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24) 142 opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost()) 143 } 144 features, err := opts.Detector.IssueFeatures() 145 if err != nil { 146 return err 147 } 148 fields := defaultFields 149 if features.StateReason { 150 fields = append(defaultFields, "stateReason") 151 } 152 153 filterOptions := prShared.FilterOptions{ 154 Entity: "issue", 155 State: issueState, 156 Assignee: opts.Assignee, 157 Labels: opts.Labels, 158 Author: opts.Author, 159 Mention: opts.Mention, 160 Milestone: opts.Milestone, 161 Search: opts.Search, 162 Fields: fields, 163 } 164 165 isTerminal := opts.IO.IsStdoutTTY() 166 167 if opts.WebMode { 168 issueListURL := ghrepo.GenerateRepoURL(baseRepo, "issues") 169 openURL, err := prShared.ListURLWithQuery(issueListURL, filterOptions) 170 if err != nil { 171 return err 172 } 173 174 if isTerminal { 175 fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(openURL)) 176 } 177 return opts.Browser.Browse(openURL) 178 } 179 180 if opts.Exporter != nil { 181 filterOptions.Fields = opts.Exporter.Fields() 182 } 183 184 listResult, err := issueList(httpClient, baseRepo, filterOptions, opts.LimitResults) 185 if err != nil { 186 return err 187 } 188 if len(listResult.Issues) == 0 && opts.Exporter == nil { 189 return prShared.ListNoResults(ghrepo.FullName(baseRepo), "issue", !filterOptions.IsDefault()) 190 } 191 192 if err := opts.IO.StartPager(); err == nil { 193 defer opts.IO.StopPager() 194 } else { 195 fmt.Fprintf(opts.IO.ErrOut, "failed to start pager: %v\n", err) 196 } 197 198 if opts.Exporter != nil { 199 return opts.Exporter.Write(opts.IO, listResult.Issues) 200 } 201 202 if listResult.SearchCapped { 203 fmt.Fprintln(opts.IO.ErrOut, "warning: this query uses the Search API which is capped at 1000 results maximum") 204 } 205 if isTerminal { 206 title := prShared.ListHeader(ghrepo.FullName(baseRepo), "issue", len(listResult.Issues), listResult.TotalCount, !filterOptions.IsDefault()) 207 fmt.Fprintf(opts.IO.Out, "\n%s\n\n", title) 208 } 209 210 issueShared.PrintIssues(opts.IO, opts.Now(), "", len(listResult.Issues), listResult.Issues) 211 212 return nil 213 } 214 215 func issueList(client *http.Client, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.IssuesAndTotalCount, error) { 216 apiClient := api.NewClientFromHTTP(client) 217 218 if filters.Search != "" || len(filters.Labels) > 0 || filters.Milestone != "" { 219 if milestoneNumber, err := strconv.ParseInt(filters.Milestone, 10, 32); err == nil { 220 milestone, err := milestoneByNumber(client, repo, int32(milestoneNumber)) 221 if err != nil { 222 return nil, err 223 } 224 filters.Milestone = milestone.Title 225 } 226 227 return searchIssues(apiClient, repo, filters, limit) 228 } 229 230 var err error 231 meReplacer := shared.NewMeReplacer(apiClient, repo.RepoHost()) 232 filters.Assignee, err = meReplacer.Replace(filters.Assignee) 233 if err != nil { 234 return nil, err 235 } 236 filters.Author, err = meReplacer.Replace(filters.Author) 237 if err != nil { 238 return nil, err 239 } 240 filters.Mention, err = meReplacer.Replace(filters.Mention) 241 if err != nil { 242 return nil, err 243 } 244 245 return listIssues(apiClient, repo, filters, limit) 246 } 247 248 func milestoneByNumber(client *http.Client, repo ghrepo.Interface, number int32) (*api.RepoMilestone, error) { 249 var query struct { 250 Repository struct { 251 Milestone *api.RepoMilestone `graphql:"milestone(number: $number)"` 252 } `graphql:"repository(owner: $owner, name: $name)"` 253 } 254 255 variables := map[string]interface{}{ 256 "owner": githubv4.String(repo.RepoOwner()), 257 "name": githubv4.String(repo.RepoName()), 258 "number": githubv4.Int(number), 259 } 260 261 gql := api.NewClientFromHTTP(client) 262 if err := gql.Query(repo.RepoHost(), "RepositoryMilestoneByNumber", &query, variables); err != nil { 263 return nil, err 264 } 265 if query.Repository.Milestone == nil { 266 return nil, fmt.Errorf("no milestone found with number '%d'", number) 267 } 268 269 return query.Repository.Milestone, nil 270 }