github.com/andrewhsu/cli/v2@v2.0.1-0.20210910131313-d4b4061f5b89/pkg/cmd/issue/list/http.go (about) 1 package list 2 3 import ( 4 "encoding/base64" 5 "fmt" 6 "strconv" 7 "strings" 8 9 "github.com/andrewhsu/cli/v2/api" 10 "github.com/andrewhsu/cli/v2/internal/ghrepo" 11 prShared "github.com/andrewhsu/cli/v2/pkg/cmd/pr/shared" 12 ) 13 14 func listIssues(client *api.Client, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.IssuesAndTotalCount, error) { 15 var states []string 16 switch filters.State { 17 case "open", "": 18 states = []string{"OPEN"} 19 case "closed": 20 states = []string{"CLOSED"} 21 case "all": 22 states = []string{"OPEN", "CLOSED"} 23 default: 24 return nil, fmt.Errorf("invalid state: %s", filters.State) 25 } 26 27 fragments := fmt.Sprintf("fragment issue on Issue {%s}", api.PullRequestGraphQL(filters.Fields)) 28 query := fragments + ` 29 query IssueList($owner: String!, $repo: String!, $limit: Int, $endCursor: String, $states: [IssueState!] = OPEN, $assignee: String, $author: String, $mention: String, $milestone: String) { 30 repository(owner: $owner, name: $repo) { 31 hasIssuesEnabled 32 issues(first: $limit, after: $endCursor, orderBy: {field: CREATED_AT, direction: DESC}, states: $states, filterBy: {assignee: $assignee, createdBy: $author, mentioned: $mention, milestone: $milestone}) { 33 totalCount 34 nodes { 35 ...issue 36 } 37 pageInfo { 38 hasNextPage 39 endCursor 40 } 41 } 42 } 43 } 44 ` 45 46 variables := map[string]interface{}{ 47 "owner": repo.RepoOwner(), 48 "repo": repo.RepoName(), 49 "states": states, 50 } 51 if filters.Assignee != "" { 52 variables["assignee"] = filters.Assignee 53 } 54 if filters.Author != "" { 55 variables["author"] = filters.Author 56 } 57 if filters.Mention != "" { 58 variables["mention"] = filters.Mention 59 } 60 61 if filters.Milestone != "" { 62 var milestone *api.RepoMilestone 63 if milestoneNumber, err := strconv.ParseInt(filters.Milestone, 10, 32); err == nil { 64 milestone, err = api.MilestoneByNumber(client, repo, int32(milestoneNumber)) 65 if err != nil { 66 return nil, err 67 } 68 } else { 69 milestone, err = api.MilestoneByTitle(client, repo, "all", filters.Milestone) 70 if err != nil { 71 return nil, err 72 } 73 } 74 75 milestoneRESTID, err := milestoneNodeIdToDatabaseId(milestone.ID) 76 if err != nil { 77 return nil, err 78 } 79 variables["milestone"] = milestoneRESTID 80 } 81 82 type responseData struct { 83 Repository struct { 84 Issues struct { 85 TotalCount int 86 Nodes []api.Issue 87 PageInfo struct { 88 HasNextPage bool 89 EndCursor string 90 } 91 } 92 HasIssuesEnabled bool 93 } 94 } 95 96 var issues []api.Issue 97 var totalCount int 98 pageLimit := min(limit, 100) 99 100 loop: 101 for { 102 var response responseData 103 variables["limit"] = pageLimit 104 err := client.GraphQL(repo.RepoHost(), query, variables, &response) 105 if err != nil { 106 return nil, err 107 } 108 if !response.Repository.HasIssuesEnabled { 109 return nil, fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(repo)) 110 } 111 totalCount = response.Repository.Issues.TotalCount 112 113 for _, issue := range response.Repository.Issues.Nodes { 114 issues = append(issues, issue) 115 if len(issues) == limit { 116 break loop 117 } 118 } 119 120 if response.Repository.Issues.PageInfo.HasNextPage { 121 variables["endCursor"] = response.Repository.Issues.PageInfo.EndCursor 122 pageLimit = min(pageLimit, limit-len(issues)) 123 } else { 124 break 125 } 126 } 127 128 res := api.IssuesAndTotalCount{Issues: issues, TotalCount: totalCount} 129 return &res, nil 130 } 131 132 func searchIssues(client *api.Client, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.IssuesAndTotalCount, error) { 133 fragments := fmt.Sprintf("fragment issue on Issue {%s}", api.PullRequestGraphQL(filters.Fields)) 134 query := fragments + 135 `query IssueSearch($repo: String!, $owner: String!, $type: SearchType!, $limit: Int, $after: String, $query: String!) { 136 repository(name: $repo, owner: $owner) { 137 hasIssuesEnabled 138 } 139 search(type: $type, last: $limit, after: $after, query: $query) { 140 issueCount 141 nodes { ...issue } 142 pageInfo { 143 hasNextPage 144 endCursor 145 } 146 } 147 }` 148 149 type response struct { 150 Repository struct { 151 HasIssuesEnabled bool 152 } 153 Search struct { 154 IssueCount int 155 Nodes []api.Issue 156 PageInfo struct { 157 HasNextPage bool 158 EndCursor string 159 } 160 } 161 } 162 163 perPage := min(limit, 100) 164 searchQuery := fmt.Sprintf("repo:%s/%s %s", repo.RepoOwner(), repo.RepoName(), prShared.SearchQueryBuild(filters)) 165 166 variables := map[string]interface{}{ 167 "owner": repo.RepoOwner(), 168 "repo": repo.RepoName(), 169 "type": "ISSUE", 170 "limit": perPage, 171 "query": searchQuery, 172 } 173 174 ic := api.IssuesAndTotalCount{} 175 176 loop: 177 for { 178 var resp response 179 err := client.GraphQL(repo.RepoHost(), query, variables, &resp) 180 if err != nil { 181 return nil, err 182 } 183 184 if !resp.Repository.HasIssuesEnabled { 185 return nil, fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(repo)) 186 } 187 188 ic.TotalCount = resp.Search.IssueCount 189 190 for _, issue := range resp.Search.Nodes { 191 ic.Issues = append(ic.Issues, issue) 192 if len(ic.Issues) == limit { 193 break loop 194 } 195 } 196 197 if !resp.Search.PageInfo.HasNextPage { 198 break 199 } 200 variables["after"] = resp.Search.PageInfo.EndCursor 201 variables["perPage"] = min(perPage, limit-len(ic.Issues)) 202 } 203 204 return &ic, nil 205 } 206 207 // milestoneNodeIdToDatabaseId extracts the REST Database ID from the GraphQL Node ID 208 // This conversion is necessary since the GraphQL API requires the use of the milestone's database ID 209 // for querying the related issues. 210 func milestoneNodeIdToDatabaseId(nodeId string) (string, error) { 211 // The Node ID is Base64 obfuscated, with an underlying pattern: 212 // "09:Milestone12345", where "12345" is the database ID 213 decoded, err := base64.StdEncoding.DecodeString(nodeId) 214 if err != nil { 215 return "", err 216 } 217 splitted := strings.Split(string(decoded), "Milestone") 218 if len(splitted) != 2 { 219 return "", fmt.Errorf("couldn't get database id from node id") 220 } 221 return splitted[1], nil 222 } 223 224 func min(a, b int) int { 225 if a < b { 226 return a 227 } 228 return b 229 }