Move issue list queries to under the issue/list package
This commit is contained in:
parent
61a8049592
commit
19ea49b5a9
4 changed files with 250 additions and 218 deletions
|
|
@ -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
242
pkg/cmd/issue/list/http.go
Normal 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue