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 }