package list import ( "fmt" "regexp" "github.com/cli/cli/v2/api" fd "github.com/cli/cli/v2/internal/featuredetection" "github.com/cli/cli/v2/internal/ghrepo" prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared" ) var pullRequestSearchQualifierRE = regexp.MustCompile(`(?i)\b(?:is|type):(?:pr|pull-?request)\b`) func listIssues(client *api.Client, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.IssuesAndTotalCount, error) { var states []string switch filters.State { case "open", "": states = []string{"OPEN"} case "closed": states = []string{"CLOSED"} case "all": states = []string{"OPEN", "CLOSED"} default: return nil, fmt.Errorf("invalid state: %s", filters.State) } fragments := fmt.Sprintf("fragment issue on Issue {%s}", api.IssueGraphQL(filters.Fields)) query := fragments + ` query IssueList($owner: String!, $repo: String!, $limit: Int, $endCursor: String, $states: [IssueState!] = OPEN, $assignee: String, $author: String, $mention: String) { repository(owner: $owner, name: $repo) { hasIssuesEnabled issues(first: $limit, after: $endCursor, orderBy: {field: CREATED_AT, direction: DESC}, states: $states, filterBy: {assignee: $assignee, createdBy: $author, mentioned: $mention}) { totalCount nodes { ...issue } pageInfo { hasNextPage endCursor } } } } ` variables := map[string]interface{}{ "owner": repo.RepoOwner(), "repo": repo.RepoName(), "states": states, } if filters.Assignee != "" { variables["assignee"] = filters.Assignee } if filters.Author != "" { variables["author"] = filters.Author } if filters.Mention != "" { variables["mention"] = filters.Mention } if filters.Milestone != "" { // The "milestone" filter in the GraphQL connection doesn't work as documented and accepts neither a // milestone number nor a title. It does accept a numeric database ID, but we cannot obtain one // using the GraphQL API. return nil, fmt.Errorf("cannot filter by milestone using the `Repository.issues` GraphQL connection") } type responseData struct { Repository struct { Issues struct { TotalCount int Nodes []api.Issue PageInfo struct { HasNextPage bool EndCursor string } } HasIssuesEnabled bool } } var issues []api.Issue var totalCount int pageLimit := min(limit, 100) loop: for { var response responseData variables["limit"] = pageLimit err := client.GraphQL(repo.RepoHost(), query, variables, &response) if err != nil { return nil, err } if !response.Repository.HasIssuesEnabled { return nil, fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(repo)) } totalCount = response.Repository.Issues.TotalCount for _, issue := range response.Repository.Issues.Nodes { issues = append(issues, issue) if len(issues) == limit { break loop } } if response.Repository.Issues.PageInfo.HasNextPage { variables["endCursor"] = response.Repository.Issues.PageInfo.EndCursor pageLimit = min(pageLimit, limit-len(issues)) } else { break } } res := api.IssuesAndTotalCount{Issues: issues, TotalCount: totalCount} return &res, nil } func searchIssues(client *api.Client, detector fd.Detector, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.IssuesAndTotalCount, error) { if pullRequestSearchQualifierRE.MatchString(filters.Search) { return nil, fmt.Errorf("cannot use pull request search qualifiers with `gh issue list`; use `gh pr list` instead") } // TODO advancedIssueSearchCleanup // We won't need feature detection when GHES 3.17 support ends, since // the advanced issue search is the only available search backend for // issues. features, err := detector.SearchFeatures() if err != nil { return nil, err } fragments := fmt.Sprintf("fragment issue on Issue {%s}", api.IssueGraphQL(filters.Fields)) query := fragments + `query IssueSearch($repo: String!, $owner: String!, $type: SearchType!, $limit: Int, $after: String, $query: String!) { repository(name: $repo, owner: $owner) { hasIssuesEnabled } search(type: $type, last: $limit, after: $after, query: $query) { issueCount nodes { ...issue } pageInfo { hasNextPage endCursor } } }` type response struct { Repository struct { HasIssuesEnabled bool } Search struct { IssueCount int Nodes []api.Issue PageInfo struct { HasNextPage bool EndCursor string } } } perPage := min(limit, 100) variables := map[string]interface{}{ "owner": repo.RepoOwner(), "repo": repo.RepoName(), "limit": perPage, } filters.Repo = ghrepo.FullName(repo) filters.Entity = "issue" // TODO advancedIssueSearchCleanup if features.AdvancedIssueSearchAPI { variables["query"] = prShared.SearchQueryBuild(filters, true) // TODO advancedIssueSearchCleanup if features.AdvancedIssueSearchAPIOptIn { variables["type"] = "ISSUE_ADVANCED" } else { variables["type"] = "ISSUE" } } else { variables["query"] = prShared.SearchQueryBuild(filters, false) variables["type"] = "ISSUE" } ic := api.IssuesAndTotalCount{SearchCapped: limit > 1000} loop: for { var resp response err := client.GraphQL(repo.RepoHost(), query, variables, &resp) if err != nil { return nil, err } if !resp.Repository.HasIssuesEnabled { return nil, fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(repo)) } ic.TotalCount = resp.Search.IssueCount for _, issue := range resp.Search.Nodes { ic.Issues = append(ic.Issues, issue) if len(ic.Issues) == limit { break loop } } if !resp.Search.PageInfo.HasNextPage { break } variables["after"] = resp.Search.PageInfo.EndCursor variables["perPage"] = min(perPage, limit-len(ic.Issues)) } return &ic, nil } func min(a, b int) int { if a < b { return a } return b }