cli/api/queries_issue.go
Francisco Miamoto 9a5e69a8a4 use graphql for repo milestones on issue list
Since we can obtain the necessary ID for the query on
node ID from the GraphQL API, it makes sense to remove
the REST version.
2020-08-01 23:43:36 -03:00

456 lines
9.4 KiB
Go

package api
import (
"context"
"encoding/base64"
"fmt"
"strings"
"time"
"github.com/cli/cli/internal/ghrepo"
"github.com/shurcooL/githubv4"
)
type IssuesPayload struct {
Assigned IssuesAndTotalCount
Mentioned IssuesAndTotalCount
Authored IssuesAndTotalCount
}
type IssuesAndTotalCount struct {
Issues []Issue
TotalCount int
}
// Ref. https://developer.github.com/v4/object/issue/
type Issue struct {
ID string
Number int
Title string
URL string
State string
Closed bool
Body string
CreatedAt time.Time
UpdatedAt time.Time
Comments struct {
TotalCount int
}
Author struct {
Login string
}
Assignees struct {
Nodes []struct {
Login string
}
TotalCount int
}
Labels struct {
Nodes []struct {
Name string
}
TotalCount int
}
ProjectCards struct {
Nodes []struct {
Project struct {
Name string
}
Column struct {
Name string
}
}
TotalCount int
}
Milestone struct {
Title string
}
}
type IssuesDisabledError struct {
error
}
const fragments = `
fragment issue on Issue {
number
title
url
state
updatedAt
labels(first: 3) {
nodes {
name
}
totalCount
}
}
`
// IssueCreate creates an issue in a GitHub repository
func IssueCreate(client *Client, repo *Repository, params map[string]interface{}) (*Issue, error) {
query := `
mutation IssueCreate($input: CreateIssueInput!) {
createIssue(input: $input) {
issue {
url
}
}
}`
inputParams := map[string]interface{}{
"repositoryId": repo.ID,
}
for key, val := range params {
inputParams[key] = val
}
variables := map[string]interface{}{
"input": inputParams,
}
result := struct {
CreateIssue struct {
Issue Issue
}
}{}
err := client.GraphQL(query, variables, &result)
if err != nil {
return nil, err
}
return &result.CreateIssue.Issue, nil
}
func IssueStatus(client *Client, repo ghrepo.Interface, currentUsername string) (*IssuesPayload, error) {
type response struct {
Repository struct {
Assigned struct {
TotalCount int
Nodes []Issue
}
Mentioned struct {
TotalCount int
Nodes []Issue
}
Authored struct {
TotalCount int
Nodes []Issue
}
HasIssuesEnabled bool
}
}
query := fragments + `
query IssueStatus($owner: String!, $repo: String!, $viewer: String!, $per_page: Int = 10) {
repository(owner: $owner, name: $repo) {
hasIssuesEnabled
assigned: issues(filterBy: {assignee: $viewer, states: OPEN}, first: $per_page, orderBy: {field: UPDATED_AT, direction: DESC}) {
totalCount
nodes {
...issue
}
}
mentioned: issues(filterBy: {mentioned: $viewer, states: OPEN}, first: $per_page, orderBy: {field: UPDATED_AT, direction: DESC}) {
totalCount
nodes {
...issue
}
}
authored: issues(filterBy: {createdBy: $viewer, states: OPEN}, first: $per_page, orderBy: {field: UPDATED_AT, direction: DESC}) {
totalCount
nodes {
...issue
}
}
}
}`
variables := map[string]interface{}{
"owner": repo.RepoOwner(),
"repo": repo.RepoName(),
"viewer": currentUsername,
}
var resp response
err := client.GraphQL(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))
}
payload := IssuesPayload{
Assigned: IssuesAndTotalCount{
Issues: resp.Repository.Assigned.Nodes,
TotalCount: resp.Repository.Assigned.TotalCount,
},
Mentioned: IssuesAndTotalCount{
Issues: resp.Repository.Mentioned.Nodes,
TotalCount: resp.Repository.Mentioned.TotalCount,
},
Authored: IssuesAndTotalCount{
Issues: resp.Repository.Authored.Nodes,
TotalCount: resp.Repository.Authored.TotalCount,
},
}
return &payload, nil
}
func IssueList(client *Client, repo ghrepo.Interface, state string, labels []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, $labels: [String!], $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, labels: $labels, 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 len(labels) > 0 {
variables["labels"] = labels
}
if assigneeString != "" {
variables["assignee"] = assigneeString
}
if authorString != "" {
variables["author"] = authorString
}
if mentionString != "" {
variables["mention"] = mentionString
}
if milestoneString != "" {
milestones, err := RepoMilestones(client, repo)
if err != nil {
return nil, err
}
for i := range milestones {
if strings.EqualFold(milestones[i].Title, milestoneString) {
// The query for milestones requires the use of the milestone's database ID (see #1441)
id, err := milestoneNodeIdToDatabaseId(milestones[i].ID)
if err != nil {
return nil, err
}
variables["milestone"] = id
break
}
}
}
var response struct {
Repository struct {
Issues struct {
TotalCount int
Nodes []Issue
PageInfo struct {
HasNextPage bool
EndCursor string
}
}
HasIssuesEnabled bool
}
}
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
}
}
res := IssuesAndTotalCount{Issues: issues, TotalCount: response.Repository.Issues.TotalCount}
return &res, nil
}
func IssueByNumber(client *Client, repo ghrepo.Interface, number int) (*Issue, error) {
type response struct {
Repository struct {
Issue Issue
HasIssuesEnabled bool
}
}
query := `
query IssueByNumber($owner: String!, $repo: String!, $issue_number: Int!) {
repository(owner: $owner, name: $repo) {
hasIssuesEnabled
issue(number: $issue_number) {
id
title
state
closed
body
author {
login
}
comments {
totalCount
}
number
url
createdAt
assignees(first: 100) {
nodes {
login
}
totalCount
}
labels(first: 100) {
nodes {
name
}
totalCount
}
projectCards(first: 100) {
nodes {
project {
name
}
column {
name
}
}
totalCount
}
milestone{
title
}
}
}
}`
variables := map[string]interface{}{
"owner": repo.RepoOwner(),
"repo": repo.RepoName(),
"issue_number": number,
}
var resp response
err := client.GraphQL(query, variables, &resp)
if err != nil {
return nil, err
}
if !resp.Repository.HasIssuesEnabled {
return nil, &IssuesDisabledError{fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(repo))}
}
return &resp.Repository.Issue, nil
}
func IssueClose(client *Client, repo ghrepo.Interface, issue Issue) error {
var mutation struct {
CloseIssue struct {
Issue struct {
ID githubv4.ID
}
} `graphql:"closeIssue(input: $input)"`
}
variables := map[string]interface{}{
"input": githubv4.CloseIssueInput{
IssueID: issue.ID,
},
}
gql := graphQLClient(client.http)
err := gql.MutateNamed(context.Background(), "IssueClose", &mutation, variables)
if err != nil {
return err
}
return nil
}
func IssueReopen(client *Client, repo ghrepo.Interface, issue Issue) error {
var mutation struct {
ReopenIssue struct {
Issue struct {
ID githubv4.ID
}
} `graphql:"reopenIssue(input: $input)"`
}
variables := map[string]interface{}{
"input": githubv4.ReopenIssueInput{
IssueID: issue.ID,
},
}
gql := graphQLClient(client.http)
err := gql.MutateNamed(context.Background(), "IssueReopen", &mutation, variables)
return err
}
// milestoneNodeIdToDatabaseId extracts the REST Database ID from the GraphQL Node ID
func milestoneNodeIdToDatabaseId(id string) (string, error) {
// The node id is base64 obfuscated
decoded, err := base64.StdEncoding.DecodeString(id)
if err != nil {
return "", err
}
// The decoded node ID has a pattern like '09:Milestone12345'
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
}