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  }