226 lines
5.8 KiB
Go
226 lines
5.8 KiB
Go
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
|
|
}
|