Move issue list queries to under the issue/list package

This commit is contained in:
Mislav Marohnić 2021-04-13 19:11:13 +02:00
parent 61a8049592
commit 19ea49b5a9
4 changed files with 250 additions and 218 deletions

View file

@ -2,10 +2,7 @@ package api
import (
"context"
"encoding/base64"
"fmt"
"strconv"
"strings"
"time"
"github.com/cli/cli/internal/ghrepo"
@ -233,123 +230,6 @@ func IssueStatus(client *Client, repo ghrepo.Interface, currentUsername string)
return &payload, nil
}
func IssueList(client *Client, repo ghrepo.Interface, state string, assigneeString string, limit int, authorString string, mentionString string, milestoneString string) (*IssuesAndTotalCount, error) {
var states []string
switch 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", state)
}
query := fragments + `
query IssueList($owner: String!, $repo: String!, $limit: Int, $endCursor: String, $states: [IssueState!] = OPEN, $assignee: String, $author: String, $mention: String, $milestone: 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, milestone: $milestone}) {
totalCount
nodes {
...issue
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
`
variables := map[string]interface{}{
"owner": repo.RepoOwner(),
"repo": repo.RepoName(),
"states": states,
}
if assigneeString != "" {
variables["assignee"] = assigneeString
}
if authorString != "" {
variables["author"] = authorString
}
if mentionString != "" {
variables["mention"] = mentionString
}
if milestoneString != "" {
var milestone *RepoMilestone
if milestoneNumber, err := strconv.ParseInt(milestoneString, 10, 32); err == nil {
milestone, err = MilestoneByNumber(client, repo, int32(milestoneNumber))
if err != nil {
return nil, err
}
} else {
milestone, err = MilestoneByTitle(client, repo, "all", milestoneString)
if err != nil {
return nil, err
}
}
milestoneRESTID, err := milestoneNodeIdToDatabaseId(milestone.ID)
if err != nil {
return nil, err
}
variables["milestone"] = milestoneRESTID
}
type responseData struct {
Repository struct {
Issues struct {
TotalCount int
Nodes []Issue
PageInfo struct {
HasNextPage bool
EndCursor string
}
}
HasIssuesEnabled bool
}
}
var issues []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 := IssuesAndTotalCount{Issues: issues, TotalCount: totalCount}
return &res, nil
}
func IssueByNumber(client *Client, repo ghrepo.Interface, number int) (*Issue, error) {
type response struct {
Repository struct {
@ -450,80 +330,6 @@ func IssueByNumber(client *Client, repo ghrepo.Interface, number int) (*Issue, e
return &resp.Repository.Issue, nil
}
func IssueSearch(client *Client, repo ghrepo.Interface, searchQuery string, limit int) (*IssuesAndTotalCount, error) {
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 []Issue
PageInfo struct {
HasNextPage bool
EndCursor string
}
}
}
perPage := min(limit, 100)
searchQuery = fmt.Sprintf("repo:%s/%s %s", repo.RepoOwner(), repo.RepoName(), searchQuery)
variables := map[string]interface{}{
"owner": repo.RepoOwner(),
"repo": repo.RepoName(),
"type": "ISSUE",
"limit": perPage,
"query": searchQuery,
}
ic := IssuesAndTotalCount{}
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 IssueClose(client *Client, repo ghrepo.Interface, issue Issue) error {
var mutation struct {
CloseIssue struct {
@ -605,23 +411,6 @@ func IssueUpdate(client *Client, repo ghrepo.Interface, params githubv4.UpdateIs
return err
}
// milestoneNodeIdToDatabaseId extracts the REST Database ID from the GraphQL Node ID
// This conversion is necessary since the GraphQL API requires the use of the milestone's database ID
// for querying the related issues.
func milestoneNodeIdToDatabaseId(nodeId string) (string, error) {
// The Node ID is Base64 obfuscated, with an underlying pattern:
// "09:Milestone12345", where "12345" is the database ID
decoded, err := base64.StdEncoding.DecodeString(nodeId)
if err != nil {
return "", err
}
splitted := strings.Split(string(decoded), "Milestone")
if len(splitted) != 2 {
return "", fmt.Errorf("couldn't get database id from node id")
}
return splitted[1], nil
}
func (i Issue) Link() string {
return i.URL
}

242
pkg/cmd/issue/list/http.go Normal file
View file

@ -0,0 +1,242 @@
package list
import (
"encoding/base64"
"fmt"
"strconv"
"strings"
"github.com/cli/cli/api"
"github.com/cli/cli/internal/ghrepo"
)
const fragments = `
fragment issue on Issue {
number
title
url
state
updatedAt
labels(first: 100) {
nodes {
name
}
totalCount
}
}
`
func IssueList(client *api.Client, repo ghrepo.Interface, state string, assigneeString string, limit int, authorString string, mentionString string, milestoneString string) (*api.IssuesAndTotalCount, error) {
var states []string
switch 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", state)
}
query := fragments + `
query IssueList($owner: String!, $repo: String!, $limit: Int, $endCursor: String, $states: [IssueState!] = OPEN, $assignee: String, $author: String, $mention: String, $milestone: 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, milestone: $milestone}) {
totalCount
nodes {
...issue
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
`
variables := map[string]interface{}{
"owner": repo.RepoOwner(),
"repo": repo.RepoName(),
"states": states,
}
if assigneeString != "" {
variables["assignee"] = assigneeString
}
if authorString != "" {
variables["author"] = authorString
}
if mentionString != "" {
variables["mention"] = mentionString
}
if milestoneString != "" {
var milestone *api.RepoMilestone
if milestoneNumber, err := strconv.ParseInt(milestoneString, 10, 32); err == nil {
milestone, err = api.MilestoneByNumber(client, repo, int32(milestoneNumber))
if err != nil {
return nil, err
}
} else {
milestone, err = api.MilestoneByTitle(client, repo, "all", milestoneString)
if err != nil {
return nil, err
}
}
milestoneRESTID, err := milestoneNodeIdToDatabaseId(milestone.ID)
if err != nil {
return nil, err
}
variables["milestone"] = milestoneRESTID
}
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 IssueSearch(client *api.Client, repo ghrepo.Interface, searchQuery string, limit int) (*api.IssuesAndTotalCount, error) {
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)
searchQuery = fmt.Sprintf("repo:%s/%s %s", repo.RepoOwner(), repo.RepoName(), searchQuery)
variables := map[string]interface{}{
"owner": repo.RepoOwner(),
"repo": repo.RepoName(),
"type": "ISSUE",
"limit": perPage,
"query": searchQuery,
}
ic := api.IssuesAndTotalCount{}
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
}
// milestoneNodeIdToDatabaseId extracts the REST Database ID from the GraphQL Node ID
// This conversion is necessary since the GraphQL API requires the use of the milestone's database ID
// for querying the related issues.
func milestoneNodeIdToDatabaseId(nodeId string) (string, error) {
// The Node ID is Base64 obfuscated, with an underlying pattern:
// "09:Milestone12345", where "12345" is the database ID
decoded, err := base64.StdEncoding.DecodeString(nodeId)
if err != nil {
return "", err
}
splitted := strings.Split(string(decoded), "Milestone")
if len(splitted) != 2 {
return "", fmt.Errorf("couldn't get database id from node id")
}
return splitted[1], nil
}
func min(a, b int) int {
if a < b {
return a
}
return b
}

View file

@ -1,4 +1,4 @@
package api
package list
import (
"encoding/json"
@ -7,13 +7,14 @@ import (
"github.com/stretchr/testify/assert"
"github.com/cli/cli/api"
"github.com/cli/cli/internal/ghrepo"
"github.com/cli/cli/pkg/httpmock"
)
func TestIssueList(t *testing.T) {
http := &httpmock.Registry{}
client := NewClient(ReplaceTripper(http))
client := api.NewClient(api.ReplaceTripper(http))
http.Register(
httpmock.GraphQL(`query IssueList\b`),
@ -78,7 +79,7 @@ func TestIssueList(t *testing.T) {
func TestIssueList_pagination(t *testing.T) {
http := &httpmock.Registry{}
client := NewClient(ReplaceTripper(http))
client := api.NewClient(api.ReplaceTripper(http))
http.Register(
httpmock.GraphQL(`query IssueList\b`),
@ -135,14 +136,14 @@ func TestIssueList_pagination(t *testing.T) {
assert.Equal(t, 2, res.TotalCount)
assert.Equal(t, 2, len(res.Issues))
getLabels := func(i Issue) []string {
getLabels := func(i api.Issue) []string {
var labels []string
for _, l := range i.Labels.Nodes {
labels = append(labels, l.Name)
}
return labels
}
getAssignees := func(i Issue) []string {
getAssignees := func(i api.Issue) []string {
var logins []string
for _, u := range i.Assignees.Nodes {
logins = append(logins, u.Login)

View file

@ -161,7 +161,7 @@ func issueList(client *http.Client, repo ghrepo.Interface, filters prShared.Filt
}
searchQuery := prShared.SearchQueryBuild(filters)
return api.IssueSearch(apiClient, repo, searchQuery, limit)
return IssueSearch(apiClient, repo, searchQuery, limit)
}
meReplacer := shared.NewMeReplacer(apiClient, repo.RepoHost())
@ -178,7 +178,7 @@ func issueList(client *http.Client, repo ghrepo.Interface, filters prShared.Filt
return nil, err
}
return api.IssueList(
return IssueList(
apiClient,
repo,
filters.State,