github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/api/queries_issue.go (about)

     1  package api
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"time"
     7  
     8  	"github.com/cli/cli/internal/ghrepo"
     9  	"github.com/shurcooL/githubv4"
    10  )
    11  
    12  type IssuesPayload struct {
    13  	Assigned  IssuesAndTotalCount
    14  	Mentioned IssuesAndTotalCount
    15  	Authored  IssuesAndTotalCount
    16  }
    17  
    18  type IssuesAndTotalCount struct {
    19  	Issues     []Issue
    20  	TotalCount int
    21  }
    22  
    23  type Issue struct {
    24  	ID             string
    25  	Number         int
    26  	Title          string
    27  	URL            string
    28  	State          string
    29  	Closed         bool
    30  	Body           string
    31  	CreatedAt      time.Time
    32  	UpdatedAt      time.Time
    33  	ClosedAt       *time.Time
    34  	Comments       Comments
    35  	Author         Author
    36  	Assignees      Assignees
    37  	Labels         Labels
    38  	ProjectCards   ProjectCards
    39  	Milestone      *Milestone
    40  	ReactionGroups ReactionGroups
    41  }
    42  
    43  type Assignees struct {
    44  	Nodes      []GitHubUser
    45  	TotalCount int
    46  }
    47  
    48  func (a Assignees) Logins() []string {
    49  	logins := make([]string, len(a.Nodes))
    50  	for i, a := range a.Nodes {
    51  		logins[i] = a.Login
    52  	}
    53  	return logins
    54  }
    55  
    56  type Labels struct {
    57  	Nodes      []IssueLabel
    58  	TotalCount int
    59  }
    60  
    61  func (l Labels) Names() []string {
    62  	names := make([]string, len(l.Nodes))
    63  	for i, l := range l.Nodes {
    64  		names[i] = l.Name
    65  	}
    66  	return names
    67  }
    68  
    69  type ProjectCards struct {
    70  	Nodes []struct {
    71  		Project struct {
    72  			Name string `json:"name"`
    73  		} `json:"project"`
    74  		Column struct {
    75  			Name string `json:"name"`
    76  		} `json:"column"`
    77  	}
    78  	TotalCount int
    79  }
    80  
    81  func (p ProjectCards) ProjectNames() []string {
    82  	names := make([]string, len(p.Nodes))
    83  	for i, c := range p.Nodes {
    84  		names[i] = c.Project.Name
    85  	}
    86  	return names
    87  }
    88  
    89  type Milestone struct {
    90  	Number      int        `json:"number"`
    91  	Title       string     `json:"title"`
    92  	Description string     `json:"description"`
    93  	DueOn       *time.Time `json:"dueOn"`
    94  }
    95  
    96  type IssuesDisabledError struct {
    97  	error
    98  }
    99  
   100  type Owner struct {
   101  	ID    string `json:"id,omitempty"`
   102  	Name  string `json:"name,omitempty"`
   103  	Login string `json:"login"`
   104  }
   105  
   106  type Author struct {
   107  	// adding these breaks generated GraphQL requests
   108  	//ID    string `json:"id,omitempty"`
   109  	//Name  string `json:"name,omitempty"`
   110  	Login string `json:"login"`
   111  }
   112  
   113  // IssueCreate creates an issue in a GitHub repository
   114  func IssueCreate(client *Client, repo *Repository, params map[string]interface{}) (*Issue, error) {
   115  	query := `
   116  	mutation IssueCreate($input: CreateIssueInput!) {
   117  		createIssue(input: $input) {
   118  			issue {
   119  				url
   120  			}
   121  		}
   122  	}`
   123  
   124  	inputParams := map[string]interface{}{
   125  		"repositoryId": repo.ID,
   126  	}
   127  	for key, val := range params {
   128  		inputParams[key] = val
   129  	}
   130  	variables := map[string]interface{}{
   131  		"input": inputParams,
   132  	}
   133  
   134  	result := struct {
   135  		CreateIssue struct {
   136  			Issue Issue
   137  		}
   138  	}{}
   139  
   140  	err := client.GraphQL(repo.RepoHost(), query, variables, &result)
   141  	if err != nil {
   142  		return nil, err
   143  	}
   144  
   145  	return &result.CreateIssue.Issue, nil
   146  }
   147  
   148  type IssueStatusOptions struct {
   149  	Username string
   150  	Fields   []string
   151  }
   152  
   153  func IssueStatus(client *Client, repo ghrepo.Interface, options IssueStatusOptions) (*IssuesPayload, error) {
   154  	type response struct {
   155  		Repository struct {
   156  			Assigned struct {
   157  				TotalCount int
   158  				Nodes      []Issue
   159  			}
   160  			Mentioned struct {
   161  				TotalCount int
   162  				Nodes      []Issue
   163  			}
   164  			Authored struct {
   165  				TotalCount int
   166  				Nodes      []Issue
   167  			}
   168  			HasIssuesEnabled bool
   169  		}
   170  	}
   171  
   172  	fragments := fmt.Sprintf("fragment issue on Issue{%s}", PullRequestGraphQL(options.Fields))
   173  	query := fragments + `
   174  	query IssueStatus($owner: String!, $repo: String!, $viewer: String!, $per_page: Int = 10) {
   175  		repository(owner: $owner, name: $repo) {
   176  			hasIssuesEnabled
   177  			assigned: issues(filterBy: {assignee: $viewer, states: OPEN}, first: $per_page, orderBy: {field: UPDATED_AT, direction: DESC}) {
   178  				totalCount
   179  				nodes {
   180  					...issue
   181  				}
   182  			}
   183  			mentioned: issues(filterBy: {mentioned: $viewer, states: OPEN}, first: $per_page, orderBy: {field: UPDATED_AT, direction: DESC}) {
   184  				totalCount
   185  				nodes {
   186  					...issue
   187  				}
   188  			}
   189  			authored: issues(filterBy: {createdBy: $viewer, states: OPEN}, first: $per_page, orderBy: {field: UPDATED_AT, direction: DESC}) {
   190  				totalCount
   191  				nodes {
   192  					...issue
   193  				}
   194  			}
   195  		}
   196      }`
   197  
   198  	variables := map[string]interface{}{
   199  		"owner":  repo.RepoOwner(),
   200  		"repo":   repo.RepoName(),
   201  		"viewer": options.Username,
   202  	}
   203  
   204  	var resp response
   205  	err := client.GraphQL(repo.RepoHost(), query, variables, &resp)
   206  	if err != nil {
   207  		return nil, err
   208  	}
   209  
   210  	if !resp.Repository.HasIssuesEnabled {
   211  		return nil, fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(repo))
   212  	}
   213  
   214  	payload := IssuesPayload{
   215  		Assigned: IssuesAndTotalCount{
   216  			Issues:     resp.Repository.Assigned.Nodes,
   217  			TotalCount: resp.Repository.Assigned.TotalCount,
   218  		},
   219  		Mentioned: IssuesAndTotalCount{
   220  			Issues:     resp.Repository.Mentioned.Nodes,
   221  			TotalCount: resp.Repository.Mentioned.TotalCount,
   222  		},
   223  		Authored: IssuesAndTotalCount{
   224  			Issues:     resp.Repository.Authored.Nodes,
   225  			TotalCount: resp.Repository.Authored.TotalCount,
   226  		},
   227  	}
   228  
   229  	return &payload, nil
   230  }
   231  
   232  func IssueByNumber(client *Client, repo ghrepo.Interface, number int) (*Issue, error) {
   233  	type response struct {
   234  		Repository struct {
   235  			Issue            Issue
   236  			HasIssuesEnabled bool
   237  		}
   238  	}
   239  
   240  	query := `
   241  	query IssueByNumber($owner: String!, $repo: String!, $issue_number: Int!) {
   242  		repository(owner: $owner, name: $repo) {
   243  			hasIssuesEnabled
   244  			issue(number: $issue_number) {
   245  				id
   246  				title
   247  				state
   248  				body
   249  				author {
   250  					login
   251  				}
   252  				comments(last: 1) {
   253  					nodes {
   254  						author {
   255  							login
   256  						}
   257  						authorAssociation
   258  						body
   259  						createdAt
   260  						includesCreatedEdit
   261  						isMinimized
   262  						minimizedReason
   263  						reactionGroups {
   264  							content
   265  							users {
   266  								totalCount
   267  							}
   268  						}
   269  					}
   270  					totalCount
   271  				}
   272  				number
   273  				url
   274  				createdAt
   275  				assignees(first: 100) {
   276  					nodes {
   277  						id
   278  						name
   279  						login
   280  					}
   281  					totalCount
   282  				}
   283  				labels(first: 100) {
   284  					nodes {
   285  						id
   286  						name
   287  						description
   288  						color
   289  					}
   290  					totalCount
   291  				}
   292  				projectCards(first: 100) {
   293  					nodes {
   294  						project {
   295  							name
   296  						}
   297  						column {
   298  							name
   299  						}
   300  					}
   301  					totalCount
   302  				}
   303  				milestone {
   304  					number
   305  					title
   306  					description
   307  					dueOn
   308  				}
   309  				reactionGroups {
   310  					content
   311  					users {
   312  						totalCount
   313  					}
   314  				}
   315  			}
   316  		}
   317  	}`
   318  
   319  	variables := map[string]interface{}{
   320  		"owner":        repo.RepoOwner(),
   321  		"repo":         repo.RepoName(),
   322  		"issue_number": number,
   323  	}
   324  
   325  	var resp response
   326  	err := client.GraphQL(repo.RepoHost(), query, variables, &resp)
   327  	if err != nil {
   328  		return nil, err
   329  	}
   330  
   331  	if !resp.Repository.HasIssuesEnabled {
   332  
   333  		return nil, &IssuesDisabledError{fmt.Errorf("the '%s' repository has disabled issues", ghrepo.FullName(repo))}
   334  	}
   335  
   336  	return &resp.Repository.Issue, nil
   337  }
   338  
   339  func IssueClose(client *Client, repo ghrepo.Interface, issue Issue) error {
   340  	var mutation struct {
   341  		CloseIssue struct {
   342  			Issue struct {
   343  				ID githubv4.ID
   344  			}
   345  		} `graphql:"closeIssue(input: $input)"`
   346  	}
   347  
   348  	variables := map[string]interface{}{
   349  		"input": githubv4.CloseIssueInput{
   350  			IssueID: issue.ID,
   351  		},
   352  	}
   353  
   354  	gql := graphQLClient(client.http, repo.RepoHost())
   355  	err := gql.MutateNamed(context.Background(), "IssueClose", &mutation, variables)
   356  
   357  	if err != nil {
   358  		return err
   359  	}
   360  
   361  	return nil
   362  }
   363  
   364  func IssueReopen(client *Client, repo ghrepo.Interface, issue Issue) error {
   365  	var mutation struct {
   366  		ReopenIssue struct {
   367  			Issue struct {
   368  				ID githubv4.ID
   369  			}
   370  		} `graphql:"reopenIssue(input: $input)"`
   371  	}
   372  
   373  	variables := map[string]interface{}{
   374  		"input": githubv4.ReopenIssueInput{
   375  			IssueID: issue.ID,
   376  		},
   377  	}
   378  
   379  	gql := graphQLClient(client.http, repo.RepoHost())
   380  	err := gql.MutateNamed(context.Background(), "IssueReopen", &mutation, variables)
   381  
   382  	return err
   383  }
   384  
   385  func IssueDelete(client *Client, repo ghrepo.Interface, issue Issue) error {
   386  	var mutation struct {
   387  		DeleteIssue struct {
   388  			Repository struct {
   389  				ID githubv4.ID
   390  			}
   391  		} `graphql:"deleteIssue(input: $input)"`
   392  	}
   393  
   394  	variables := map[string]interface{}{
   395  		"input": githubv4.DeleteIssueInput{
   396  			IssueID: issue.ID,
   397  		},
   398  	}
   399  
   400  	gql := graphQLClient(client.http, repo.RepoHost())
   401  	err := gql.MutateNamed(context.Background(), "IssueDelete", &mutation, variables)
   402  
   403  	return err
   404  }
   405  
   406  func IssueUpdate(client *Client, repo ghrepo.Interface, params githubv4.UpdateIssueInput) error {
   407  	var mutation struct {
   408  		UpdateIssue struct {
   409  			Issue struct {
   410  				ID string
   411  			}
   412  		} `graphql:"updateIssue(input: $input)"`
   413  	}
   414  	variables := map[string]interface{}{"input": params}
   415  	gql := graphQLClient(client.http, repo.RepoHost())
   416  	err := gql.MutateNamed(context.Background(), "IssueUpdate", &mutation, variables)
   417  	return err
   418  }
   419  
   420  func (i Issue) Link() string {
   421  	return i.URL
   422  }
   423  
   424  func (i Issue) Identifier() string {
   425  	return i.ID
   426  }