diff --git a/api/queries_issue.go b/api/queries_issue.go index 934467e05..c0fca27e4 100644 --- a/api/queries_issue.go +++ b/api/queries_issue.go @@ -185,20 +185,23 @@ func IssueList(client *Client, repo ghrepo.Interface, state string, labels []str } query := fragments + ` - query($owner: String!, $repo: String!, $limit: Int, $states: [IssueState!] = OPEN, $labels: [String!], $assignee: String) { + query($owner: String!, $repo: String!, $limit: Int, $endCursor: String, $states: [IssueState!] = OPEN, $labels: [String!], $assignee: String) { repository(owner: $owner, name: $repo) { hasIssuesEnabled - issues(first: $limit, orderBy: {field: CREATED_AT, direction: DESC}, states: $states, labels: $labels, filterBy: {assignee: $assignee}) { + issues(first: $limit, after: $endCursor, orderBy: {field: CREATED_AT, direction: DESC}, states: $states, labels: $labels, filterBy: {assignee: $assignee}) { nodes { ...issue } + pageInfo { + hasNextPage + endCursor + } } } } ` variables := map[string]interface{}{ - "limit": limit, "owner": repo.RepoOwner(), "repo": repo.RepoName(), "states": states, @@ -210,25 +213,49 @@ func IssueList(client *Client, repo ghrepo.Interface, state string, labels []str variables["assignee"] = assigneeString } - var resp struct { + var response struct { Repository struct { Issues struct { - Nodes []Issue + Nodes []Issue + PageInfo struct { + HasNextPage bool + EndCursor string + } } HasIssuesEnabled bool } } - err := client.GraphQL(query, variables, &resp) - if err != nil { - return nil, err + var issues []Issue + pageLimit := min(limit, 100) + +loop: + for { + variables["limit"] = pageLimit + err := client.GraphQL(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)) + } + + 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 + } } - if !resp.Repository.HasIssuesEnabled { - return nil, fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(repo)) - } - - return resp.Repository.Issues.Nodes, nil + return issues, nil } func IssueByNumber(client *Client, repo ghrepo.Interface, number int) (*Issue, error) { diff --git a/api/queries_issue_test.go b/api/queries_issue_test.go new file mode 100644 index 000000000..bde6b8696 --- /dev/null +++ b/api/queries_issue_test.go @@ -0,0 +1,68 @@ +package api + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "testing" + + "github.com/cli/cli/internal/ghrepo" +) + +func TestIssueList(t *testing.T) { + http := &FakeHTTP{} + client := NewClient(ReplaceTripper(http)) + + http.StubResponse(200, bytes.NewBufferString(` + { "data": { "repository": { + "hasIssuesEnabled": true, + "issues": { + "nodes": [], + "pageInfo": { + "hasNextPage": true, + "endCursor": "ENDCURSOR" + } + } + } } } + `)) + http.StubResponse(200, bytes.NewBufferString(` + { "data": { "repository": { + "hasIssuesEnabled": true, + "issues": { + "nodes": [], + "pageInfo": { + "hasNextPage": false, + "endCursor": "ENDCURSOR" + } + } + } } } + `)) + + _, err := IssueList(client, ghrepo.FromFullName("OWNER/REPO"), "open", []string{}, "", 251) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(http.Requests) != 2 { + t.Fatalf("expected 2 HTTP requests, seen %d", len(http.Requests)) + } + var reqBody struct { + Query string + Variables map[string]interface{} + } + + bodyBytes, _ := ioutil.ReadAll(http.Requests[0].Body) + json.Unmarshal(bodyBytes, &reqBody) + if reqLimit := reqBody.Variables["limit"].(float64); reqLimit != 100 { + t.Errorf("expected 100, got %v", reqLimit) + } + if _, cursorPresent := reqBody.Variables["endCursor"]; cursorPresent { + t.Error("did not expect first request to pass 'endCursor'") + } + + bodyBytes, _ = ioutil.ReadAll(http.Requests[1].Body) + json.Unmarshal(bodyBytes, &reqBody) + if endCursor := reqBody.Variables["endCursor"].(string); endCursor != "ENDCURSOR" { + t.Errorf("expected %q, got %q", "ENDCURSOR", endCursor) + } +}