github.com/abdfnx/gh-api@v0.0.0-20210414084727-f5432eec23b8/api/queries_repo.go (about) 1 package api 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "net/http" 9 "sort" 10 "strings" 11 "time" 12 13 "github.com/abdfnx/gh-api/internal/ghrepo" 14 "github.com/shurcooL/githubv4" 15 ) 16 17 // Repository contains information about a GitHub repo 18 type Repository struct { 19 ID string 20 Name string 21 Description string 22 URL string 23 CloneURL string 24 CreatedAt time.Time 25 Owner RepositoryOwner 26 27 IsPrivate bool 28 HasIssuesEnabled bool 29 HasWikiEnabled bool 30 ViewerPermission string 31 DefaultBranchRef BranchRef 32 33 Parent *Repository 34 35 MergeCommitAllowed bool 36 RebaseMergeAllowed bool 37 SquashMergeAllowed bool 38 39 // pseudo-field that keeps track of host name of this repo 40 hostname string 41 } 42 43 // RepositoryOwner is the owner of a GitHub repository 44 type RepositoryOwner struct { 45 Login string 46 } 47 48 // BranchRef is the branch name in a GitHub repository 49 type BranchRef struct { 50 Name string 51 } 52 53 // RepoOwner is the login name of the owner 54 func (r Repository) RepoOwner() string { 55 return r.Owner.Login 56 } 57 58 // RepoName is the name of the repository 59 func (r Repository) RepoName() string { 60 return r.Name 61 } 62 63 // RepoHost is the GitHub hostname of the repository 64 func (r Repository) RepoHost() string { 65 return r.hostname 66 } 67 68 // IsFork is true when this repository has a parent repository 69 func (r Repository) IsFork() bool { 70 return r.Parent != nil 71 } 72 73 // ViewerCanPush is true when the requesting user has push access 74 func (r Repository) ViewerCanPush() bool { 75 switch r.ViewerPermission { 76 case "ADMIN", "MAINTAIN", "WRITE": 77 return true 78 default: 79 return false 80 } 81 } 82 83 // ViewerCanTriage is true when the requesting user can triage issues and pull requests 84 func (r Repository) ViewerCanTriage() bool { 85 switch r.ViewerPermission { 86 case "ADMIN", "MAINTAIN", "WRITE", "TRIAGE": 87 return true 88 default: 89 return false 90 } 91 } 92 93 func GitHubRepo(client *Client, repo ghrepo.Interface) (*Repository, error) { 94 query := ` 95 fragment repo on Repository { 96 id 97 name 98 owner { login } 99 hasIssuesEnabled 100 description 101 hasWikiEnabled 102 viewerPermission 103 defaultBranchRef { 104 name 105 } 106 } 107 108 query RepositoryInfo($owner: String!, $name: String!) { 109 repository(owner: $owner, name: $name) { 110 ...repo 111 parent { 112 ...repo 113 } 114 mergeCommitAllowed 115 rebaseMergeAllowed 116 squashMergeAllowed 117 } 118 }` 119 variables := map[string]interface{}{ 120 "owner": repo.RepoOwner(), 121 "name": repo.RepoName(), 122 } 123 124 result := struct { 125 Repository Repository 126 }{} 127 err := client.GraphQL(repo.RepoHost(), query, variables, &result) 128 129 if err != nil { 130 return nil, err 131 } 132 133 return InitRepoHostname(&result.Repository, repo.RepoHost()), nil 134 } 135 136 func RepoDefaultBranch(client *Client, repo ghrepo.Interface) (string, error) { 137 if r, ok := repo.(*Repository); ok && r.DefaultBranchRef.Name != "" { 138 return r.DefaultBranchRef.Name, nil 139 } 140 141 r, err := GitHubRepo(client, repo) 142 if err != nil { 143 return "", err 144 } 145 return r.DefaultBranchRef.Name, nil 146 } 147 148 func CanPushToRepo(httpClient *http.Client, repo ghrepo.Interface) (bool, error) { 149 if r, ok := repo.(*Repository); ok && r.ViewerPermission != "" { 150 return r.ViewerCanPush(), nil 151 } 152 153 apiClient := NewClientFromHTTP(httpClient) 154 r, err := GitHubRepo(apiClient, repo) 155 if err != nil { 156 return false, err 157 } 158 return r.ViewerCanPush(), nil 159 } 160 161 // RepoParent finds out the parent repository of a fork 162 func RepoParent(client *Client, repo ghrepo.Interface) (ghrepo.Interface, error) { 163 var query struct { 164 Repository struct { 165 Parent *struct { 166 Name string 167 Owner struct { 168 Login string 169 } 170 } 171 } `graphql:"repository(owner: $owner, name: $name)"` 172 } 173 174 variables := map[string]interface{}{ 175 "owner": githubv4.String(repo.RepoOwner()), 176 "name": githubv4.String(repo.RepoName()), 177 } 178 179 gql := graphQLClient(client.http, repo.RepoHost()) 180 err := gql.QueryNamed(context.Background(), "RepositoryFindParent", &query, variables) 181 if err != nil { 182 return nil, err 183 } 184 if query.Repository.Parent == nil { 185 return nil, nil 186 } 187 188 parent := ghrepo.NewWithHost(query.Repository.Parent.Owner.Login, query.Repository.Parent.Name, repo.RepoHost()) 189 return parent, nil 190 } 191 192 // RepoNetworkResult describes the relationship between related repositories 193 type RepoNetworkResult struct { 194 ViewerLogin string 195 Repositories []*Repository 196 } 197 198 // RepoNetwork inspects the relationship between multiple GitHub repositories 199 func RepoNetwork(client *Client, repos []ghrepo.Interface) (RepoNetworkResult, error) { 200 var hostname string 201 if len(repos) > 0 { 202 hostname = repos[0].RepoHost() 203 } 204 205 queries := make([]string, 0, len(repos)) 206 for i, repo := range repos { 207 queries = append(queries, fmt.Sprintf(` 208 repo_%03d: repository(owner: %q, name: %q) { 209 ...repo 210 parent { 211 ...repo 212 } 213 } 214 `, i, repo.RepoOwner(), repo.RepoName())) 215 } 216 217 // Since the query is constructed dynamically, we can't parse a response 218 // format using a static struct. Instead, hold the raw JSON data until we 219 // decide how to parse it manually. 220 graphqlResult := make(map[string]*json.RawMessage) 221 var result RepoNetworkResult 222 223 err := client.GraphQL(hostname, fmt.Sprintf(` 224 fragment repo on Repository { 225 id 226 name 227 owner { login } 228 viewerPermission 229 defaultBranchRef { 230 name 231 } 232 isPrivate 233 } 234 query RepositoryNetwork { 235 viewer { login } 236 %s 237 } 238 `, strings.Join(queries, "")), nil, &graphqlResult) 239 graphqlError, isGraphQLError := err.(*GraphQLErrorResponse) 240 if isGraphQLError { 241 // If the only errors are that certain repositories are not found, 242 // continue processing this response instead of returning an error 243 tolerated := true 244 for _, ge := range graphqlError.Errors { 245 if ge.Type != "NOT_FOUND" { 246 tolerated = false 247 } 248 } 249 if tolerated { 250 err = nil 251 } 252 } 253 if err != nil { 254 return result, err 255 } 256 257 keys := make([]string, 0, len(graphqlResult)) 258 for key := range graphqlResult { 259 keys = append(keys, key) 260 } 261 // sort keys to ensure `repo_{N}` entries are processed in order 262 sort.Strings(keys) 263 264 // Iterate over keys of GraphQL response data and, based on its name, 265 // dynamically allocate the target struct an individual message gets decoded to. 266 for _, name := range keys { 267 jsonMessage := graphqlResult[name] 268 if name == "viewer" { 269 viewerResult := struct { 270 Login string 271 }{} 272 decoder := json.NewDecoder(bytes.NewReader([]byte(*jsonMessage))) 273 if err := decoder.Decode(&viewerResult); err != nil { 274 return result, err 275 } 276 result.ViewerLogin = viewerResult.Login 277 } else if strings.HasPrefix(name, "repo_") { 278 if jsonMessage == nil { 279 result.Repositories = append(result.Repositories, nil) 280 continue 281 } 282 var repo Repository 283 decoder := json.NewDecoder(bytes.NewReader(*jsonMessage)) 284 if err := decoder.Decode(&repo); err != nil { 285 return result, err 286 } 287 result.Repositories = append(result.Repositories, InitRepoHostname(&repo, hostname)) 288 } else { 289 return result, fmt.Errorf("unknown GraphQL result key %q", name) 290 } 291 } 292 return result, nil 293 } 294 295 func InitRepoHostname(repo *Repository, hostname string) *Repository { 296 repo.hostname = hostname 297 if repo.Parent != nil { 298 repo.Parent.hostname = hostname 299 } 300 return repo 301 } 302 303 // repositoryV3 is the repository result from GitHub API v3 304 type repositoryV3 struct { 305 NodeID string 306 Name string 307 CreatedAt time.Time `json:"created_at"` 308 CloneURL string `json:"clone_url"` 309 Owner struct { 310 Login string 311 } 312 } 313 314 // ForkRepo forks the repository on GitHub and returns the new repository 315 func ForkRepo(client *Client, repo ghrepo.Interface) (*Repository, error) { 316 path := fmt.Sprintf("repos/%s/forks", ghrepo.FullName(repo)) 317 body := bytes.NewBufferString(`{}`) 318 result := repositoryV3{} 319 err := client.REST(repo.RepoHost(), "POST", path, body, &result) 320 if err != nil { 321 return nil, err 322 } 323 324 return &Repository{ 325 ID: result.NodeID, 326 Name: result.Name, 327 CloneURL: result.CloneURL, 328 CreatedAt: result.CreatedAt, 329 Owner: RepositoryOwner{ 330 Login: result.Owner.Login, 331 }, 332 ViewerPermission: "WRITE", 333 hostname: repo.RepoHost(), 334 }, nil 335 } 336 337 // RepoFindForks finds forks of the repo that are affiliated with the viewer 338 func RepoFindForks(client *Client, repo ghrepo.Interface, limit int) ([]*Repository, error) { 339 result := struct { 340 Repository struct { 341 Forks struct { 342 Nodes []Repository 343 } 344 } 345 }{} 346 347 variables := map[string]interface{}{ 348 "owner": repo.RepoOwner(), 349 "repo": repo.RepoName(), 350 "limit": limit, 351 } 352 353 if err := client.GraphQL(repo.RepoHost(), ` 354 query RepositoryFindFork($owner: String!, $repo: String!, $limit: Int!) { 355 repository(owner: $owner, name: $repo) { 356 forks(first: $limit, affiliations: [OWNER, COLLABORATOR]) { 357 nodes { 358 id 359 name 360 owner { login } 361 url 362 viewerPermission 363 } 364 } 365 } 366 } 367 `, variables, &result); err != nil { 368 return nil, err 369 } 370 371 var results []*Repository 372 for _, r := range result.Repository.Forks.Nodes { 373 // we check ViewerCanPush, even though we expect it to always be true per 374 // `affiliations` condition, to guard against versions of GitHub with a 375 // faulty `affiliations` implementation 376 if !r.ViewerCanPush() { 377 continue 378 } 379 results = append(results, InitRepoHostname(&r, repo.RepoHost())) 380 } 381 382 return results, nil 383 } 384 385 type RepoMetadataResult struct { 386 AssignableUsers []RepoAssignee 387 Labels []RepoLabel 388 Projects []RepoProject 389 Milestones []RepoMilestone 390 Teams []OrgTeam 391 } 392 393 func (m *RepoMetadataResult) MembersToIDs(names []string) ([]string, error) { 394 var ids []string 395 for _, assigneeLogin := range names { 396 found := false 397 for _, u := range m.AssignableUsers { 398 if strings.EqualFold(assigneeLogin, u.Login) { 399 ids = append(ids, u.ID) 400 found = true 401 break 402 } 403 } 404 if !found { 405 return nil, fmt.Errorf("'%s' not found", assigneeLogin) 406 } 407 } 408 return ids, nil 409 } 410 411 func (m *RepoMetadataResult) TeamsToIDs(names []string) ([]string, error) { 412 var ids []string 413 for _, teamSlug := range names { 414 found := false 415 slug := teamSlug[strings.IndexRune(teamSlug, '/')+1:] 416 for _, t := range m.Teams { 417 if strings.EqualFold(slug, t.Slug) { 418 ids = append(ids, t.ID) 419 found = true 420 break 421 } 422 } 423 if !found { 424 return nil, fmt.Errorf("'%s' not found", teamSlug) 425 } 426 } 427 return ids, nil 428 } 429 430 func (m *RepoMetadataResult) LabelsToIDs(names []string) ([]string, error) { 431 var ids []string 432 for _, labelName := range names { 433 found := false 434 for _, l := range m.Labels { 435 if strings.EqualFold(labelName, l.Name) { 436 ids = append(ids, l.ID) 437 found = true 438 break 439 } 440 } 441 if !found { 442 return nil, fmt.Errorf("'%s' not found", labelName) 443 } 444 } 445 return ids, nil 446 } 447 448 func (m *RepoMetadataResult) ProjectsToIDs(names []string) ([]string, error) { 449 var ids []string 450 for _, projectName := range names { 451 found := false 452 for _, p := range m.Projects { 453 if strings.EqualFold(projectName, p.Name) { 454 ids = append(ids, p.ID) 455 found = true 456 break 457 } 458 } 459 if !found { 460 return nil, fmt.Errorf("'%s' not found", projectName) 461 } 462 } 463 return ids, nil 464 } 465 466 func ProjectsToPaths(projects []RepoProject, names []string) ([]string, error) { 467 var paths []string 468 for _, projectName := range names { 469 found := false 470 for _, p := range projects { 471 if strings.EqualFold(projectName, p.Name) { 472 // format of ResourcePath: /OWNER/REPO/projects/PROJECT_NUMBER or /orgs/ORG/projects/PROJECT_NUMBER 473 // required format of path: OWNER/REPO/PROJECT_NUMBER or ORG/PROJECT_NUMBER 474 var path string 475 pathParts := strings.Split(p.ResourcePath, "/") 476 if pathParts[1] == "orgs" { 477 path = fmt.Sprintf("%s/%s", pathParts[2], pathParts[4]) 478 } else { 479 path = fmt.Sprintf("%s/%s/%s", pathParts[1], pathParts[2], pathParts[4]) 480 } 481 paths = append(paths, path) 482 found = true 483 break 484 } 485 } 486 if !found { 487 return nil, fmt.Errorf("'%s' not found", projectName) 488 } 489 } 490 return paths, nil 491 } 492 493 func (m *RepoMetadataResult) MilestoneToID(title string) (string, error) { 494 for _, m := range m.Milestones { 495 if strings.EqualFold(title, m.Title) { 496 return m.ID, nil 497 } 498 } 499 return "", fmt.Errorf("'%s' not found", title) 500 } 501 502 func (m *RepoMetadataResult) Merge(m2 *RepoMetadataResult) { 503 if len(m2.AssignableUsers) > 0 || len(m.AssignableUsers) == 0 { 504 m.AssignableUsers = m2.AssignableUsers 505 } 506 507 if len(m2.Teams) > 0 || len(m.Teams) == 0 { 508 m.Teams = m2.Teams 509 } 510 511 if len(m2.Labels) > 0 || len(m.Labels) == 0 { 512 m.Labels = m2.Labels 513 } 514 515 if len(m2.Projects) > 0 || len(m.Projects) == 0 { 516 m.Projects = m2.Projects 517 } 518 519 if len(m2.Milestones) > 0 || len(m.Milestones) == 0 { 520 m.Milestones = m2.Milestones 521 } 522 } 523 524 type RepoMetadataInput struct { 525 Assignees bool 526 Reviewers bool 527 Labels bool 528 Projects bool 529 Milestones bool 530 } 531 532 // RepoMetadata pre-fetches the metadata for attaching to issues and pull requests 533 func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput) (*RepoMetadataResult, error) { 534 result := RepoMetadataResult{} 535 errc := make(chan error) 536 count := 0 537 538 if input.Assignees || input.Reviewers { 539 count++ 540 go func() { 541 users, err := RepoAssignableUsers(client, repo) 542 if err != nil { 543 err = fmt.Errorf("error fetching assignees: %w", err) 544 } 545 result.AssignableUsers = users 546 errc <- err 547 }() 548 } 549 if input.Reviewers { 550 count++ 551 go func() { 552 teams, err := OrganizationTeams(client, repo) 553 // TODO: better detection of non-org repos 554 if err != nil && !strings.HasPrefix(err.Error(), "Could not resolve to an Organization") { 555 errc <- fmt.Errorf("error fetching organization teams: %w", err) 556 return 557 } 558 result.Teams = teams 559 errc <- nil 560 }() 561 } 562 if input.Labels { 563 count++ 564 go func() { 565 labels, err := RepoLabels(client, repo) 566 if err != nil { 567 err = fmt.Errorf("error fetching labels: %w", err) 568 } 569 result.Labels = labels 570 errc <- err 571 }() 572 } 573 if input.Projects { 574 count++ 575 go func() { 576 projects, err := RepoAndOrgProjects(client, repo) 577 if err != nil { 578 errc <- err 579 return 580 } 581 result.Projects = projects 582 errc <- nil 583 }() 584 } 585 if input.Milestones { 586 count++ 587 go func() { 588 milestones, err := RepoMilestones(client, repo, "open") 589 if err != nil { 590 err = fmt.Errorf("error fetching milestones: %w", err) 591 } 592 result.Milestones = milestones 593 errc <- err 594 }() 595 } 596 597 var err error 598 for i := 0; i < count; i++ { 599 if e := <-errc; e != nil { 600 err = e 601 } 602 } 603 604 return &result, err 605 } 606 607 type RepoResolveInput struct { 608 Assignees []string 609 Reviewers []string 610 Labels []string 611 Projects []string 612 Milestones []string 613 } 614 615 // RepoResolveMetadataIDs looks up GraphQL node IDs in bulk 616 func RepoResolveMetadataIDs(client *Client, repo ghrepo.Interface, input RepoResolveInput) (*RepoMetadataResult, error) { 617 users := input.Assignees 618 hasUser := func(target string) bool { 619 for _, u := range users { 620 if strings.EqualFold(u, target) { 621 return true 622 } 623 } 624 return false 625 } 626 627 var teams []string 628 for _, r := range input.Reviewers { 629 if i := strings.IndexRune(r, '/'); i > -1 { 630 teams = append(teams, r[i+1:]) 631 } else if !hasUser(r) { 632 users = append(users, r) 633 } 634 } 635 636 // there is no way to look up projects nor milestones by name, so preload them all 637 mi := RepoMetadataInput{ 638 Projects: len(input.Projects) > 0, 639 Milestones: len(input.Milestones) > 0, 640 } 641 result, err := RepoMetadata(client, repo, mi) 642 if err != nil { 643 return result, err 644 } 645 if len(users) == 0 && len(teams) == 0 && len(input.Labels) == 0 { 646 return result, nil 647 } 648 649 query := &bytes.Buffer{} 650 fmt.Fprint(query, "query RepositoryResolveMetadataIDs {\n") 651 for i, u := range users { 652 fmt.Fprintf(query, "u%03d: user(login:%q){id,login}\n", i, u) 653 } 654 if len(input.Labels) > 0 { 655 fmt.Fprintf(query, "repository(owner:%q,name:%q){\n", repo.RepoOwner(), repo.RepoName()) 656 for i, l := range input.Labels { 657 fmt.Fprintf(query, "l%03d: label(name:%q){id,name}\n", i, l) 658 } 659 fmt.Fprint(query, "}\n") 660 } 661 if len(teams) > 0 { 662 fmt.Fprintf(query, "organization(login:%q){\n", repo.RepoOwner()) 663 for i, t := range teams { 664 fmt.Fprintf(query, "t%03d: team(slug:%q){id,slug}\n", i, t) 665 } 666 fmt.Fprint(query, "}\n") 667 } 668 fmt.Fprint(query, "}\n") 669 670 response := make(map[string]json.RawMessage) 671 err = client.GraphQL(repo.RepoHost(), query.String(), nil, &response) 672 if err != nil { 673 return result, err 674 } 675 676 for key, v := range response { 677 switch key { 678 case "repository": 679 repoResponse := make(map[string]RepoLabel) 680 err := json.Unmarshal(v, &repoResponse) 681 if err != nil { 682 return result, err 683 } 684 for _, l := range repoResponse { 685 result.Labels = append(result.Labels, l) 686 } 687 case "organization": 688 orgResponse := make(map[string]OrgTeam) 689 err := json.Unmarshal(v, &orgResponse) 690 if err != nil { 691 return result, err 692 } 693 for _, t := range orgResponse { 694 result.Teams = append(result.Teams, t) 695 } 696 default: 697 user := RepoAssignee{} 698 err := json.Unmarshal(v, &user) 699 if err != nil { 700 return result, err 701 } 702 result.AssignableUsers = append(result.AssignableUsers, user) 703 } 704 } 705 706 return result, nil 707 } 708 709 type RepoProject struct { 710 ID string 711 Name string 712 ResourcePath string 713 } 714 715 // RepoProjects fetches all open projects for a repository 716 func RepoProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, error) { 717 type responseData struct { 718 Repository struct { 719 Projects struct { 720 Nodes []RepoProject 721 PageInfo struct { 722 HasNextPage bool 723 EndCursor string 724 } 725 } `graphql:"projects(states: [OPEN], first: 100, orderBy: {field: NAME, direction: ASC}, after: $endCursor)"` 726 } `graphql:"repository(owner: $owner, name: $name)"` 727 } 728 729 variables := map[string]interface{}{ 730 "owner": githubv4.String(repo.RepoOwner()), 731 "name": githubv4.String(repo.RepoName()), 732 "endCursor": (*githubv4.String)(nil), 733 } 734 735 gql := graphQLClient(client.http, repo.RepoHost()) 736 737 var projects []RepoProject 738 for { 739 var query responseData 740 err := gql.QueryNamed(context.Background(), "RepositoryProjectList", &query, variables) 741 if err != nil { 742 return nil, err 743 } 744 745 projects = append(projects, query.Repository.Projects.Nodes...) 746 if !query.Repository.Projects.PageInfo.HasNextPage { 747 break 748 } 749 variables["endCursor"] = githubv4.String(query.Repository.Projects.PageInfo.EndCursor) 750 } 751 752 return projects, nil 753 } 754 755 // RepoAndOrgProjects fetches all open projects for a repository and its org 756 func RepoAndOrgProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, error) { 757 projects, err := RepoProjects(client, repo) 758 if err != nil { 759 return projects, fmt.Errorf("error fetching projects: %w", err) 760 } 761 762 orgProjects, err := OrganizationProjects(client, repo) 763 // TODO: better detection of non-org repos 764 if err != nil && !strings.HasPrefix(err.Error(), "Could not resolve to an Organization") { 765 return projects, fmt.Errorf("error fetching organization projects: %w", err) 766 } 767 projects = append(projects, orgProjects...) 768 769 return projects, nil 770 } 771 772 type RepoAssignee struct { 773 ID string 774 Login string 775 } 776 777 // RepoAssignableUsers fetches all the assignable users for a repository 778 func RepoAssignableUsers(client *Client, repo ghrepo.Interface) ([]RepoAssignee, error) { 779 type responseData struct { 780 Repository struct { 781 AssignableUsers struct { 782 Nodes []RepoAssignee 783 PageInfo struct { 784 HasNextPage bool 785 EndCursor string 786 } 787 } `graphql:"assignableUsers(first: 100, after: $endCursor)"` 788 } `graphql:"repository(owner: $owner, name: $name)"` 789 } 790 791 variables := map[string]interface{}{ 792 "owner": githubv4.String(repo.RepoOwner()), 793 "name": githubv4.String(repo.RepoName()), 794 "endCursor": (*githubv4.String)(nil), 795 } 796 797 gql := graphQLClient(client.http, repo.RepoHost()) 798 799 var users []RepoAssignee 800 for { 801 var query responseData 802 err := gql.QueryNamed(context.Background(), "RepositoryAssignableUsers", &query, variables) 803 if err != nil { 804 return nil, err 805 } 806 807 users = append(users, query.Repository.AssignableUsers.Nodes...) 808 if !query.Repository.AssignableUsers.PageInfo.HasNextPage { 809 break 810 } 811 variables["endCursor"] = githubv4.String(query.Repository.AssignableUsers.PageInfo.EndCursor) 812 } 813 814 return users, nil 815 } 816 817 type RepoLabel struct { 818 ID string 819 Name string 820 } 821 822 // RepoLabels fetches all the labels in a repository 823 func RepoLabels(client *Client, repo ghrepo.Interface) ([]RepoLabel, error) { 824 type responseData struct { 825 Repository struct { 826 Labels struct { 827 Nodes []RepoLabel 828 PageInfo struct { 829 HasNextPage bool 830 EndCursor string 831 } 832 } `graphql:"labels(first: 100, orderBy: {field: NAME, direction: ASC}, after: $endCursor)"` 833 } `graphql:"repository(owner: $owner, name: $name)"` 834 } 835 836 variables := map[string]interface{}{ 837 "owner": githubv4.String(repo.RepoOwner()), 838 "name": githubv4.String(repo.RepoName()), 839 "endCursor": (*githubv4.String)(nil), 840 } 841 842 gql := graphQLClient(client.http, repo.RepoHost()) 843 844 var labels []RepoLabel 845 for { 846 var query responseData 847 err := gql.QueryNamed(context.Background(), "RepositoryLabelList", &query, variables) 848 if err != nil { 849 return nil, err 850 } 851 852 labels = append(labels, query.Repository.Labels.Nodes...) 853 if !query.Repository.Labels.PageInfo.HasNextPage { 854 break 855 } 856 variables["endCursor"] = githubv4.String(query.Repository.Labels.PageInfo.EndCursor) 857 } 858 859 return labels, nil 860 } 861 862 type RepoMilestone struct { 863 ID string 864 Title string 865 } 866 867 // RepoMilestones fetches milestones in a repository 868 func RepoMilestones(client *Client, repo ghrepo.Interface, state string) ([]RepoMilestone, error) { 869 type responseData struct { 870 Repository struct { 871 Milestones struct { 872 Nodes []RepoMilestone 873 PageInfo struct { 874 HasNextPage bool 875 EndCursor string 876 } 877 } `graphql:"milestones(states: $states, first: 100, after: $endCursor)"` 878 } `graphql:"repository(owner: $owner, name: $name)"` 879 } 880 881 var states []githubv4.MilestoneState 882 switch state { 883 case "open": 884 states = []githubv4.MilestoneState{"OPEN"} 885 case "closed": 886 states = []githubv4.MilestoneState{"CLOSED"} 887 case "all": 888 states = []githubv4.MilestoneState{"OPEN", "CLOSED"} 889 default: 890 return nil, fmt.Errorf("invalid state: %s", state) 891 } 892 893 variables := map[string]interface{}{ 894 "owner": githubv4.String(repo.RepoOwner()), 895 "name": githubv4.String(repo.RepoName()), 896 "states": states, 897 "endCursor": (*githubv4.String)(nil), 898 } 899 900 gql := graphQLClient(client.http, repo.RepoHost()) 901 902 var milestones []RepoMilestone 903 for { 904 var query responseData 905 err := gql.QueryNamed(context.Background(), "RepositoryMilestoneList", &query, variables) 906 if err != nil { 907 return nil, err 908 } 909 910 milestones = append(milestones, query.Repository.Milestones.Nodes...) 911 if !query.Repository.Milestones.PageInfo.HasNextPage { 912 break 913 } 914 variables["endCursor"] = githubv4.String(query.Repository.Milestones.PageInfo.EndCursor) 915 } 916 917 return milestones, nil 918 } 919 920 func MilestoneByTitle(client *Client, repo ghrepo.Interface, state, title string) (*RepoMilestone, error) { 921 milestones, err := RepoMilestones(client, repo, state) 922 if err != nil { 923 return nil, err 924 } 925 926 for i := range milestones { 927 if strings.EqualFold(milestones[i].Title, title) { 928 return &milestones[i], nil 929 } 930 } 931 return nil, fmt.Errorf("no milestone found with title %q", title) 932 } 933 934 func MilestoneByNumber(client *Client, repo ghrepo.Interface, number int32) (*RepoMilestone, error) { 935 var query struct { 936 Repository struct { 937 Milestone *RepoMilestone `graphql:"milestone(number: $number)"` 938 } `graphql:"repository(owner: $owner, name: $name)"` 939 } 940 941 variables := map[string]interface{}{ 942 "owner": githubv4.String(repo.RepoOwner()), 943 "name": githubv4.String(repo.RepoName()), 944 "number": githubv4.Int(number), 945 } 946 947 gql := graphQLClient(client.http, repo.RepoHost()) 948 949 err := gql.QueryNamed(context.Background(), "RepositoryMilestoneByNumber", &query, variables) 950 if err != nil { 951 return nil, err 952 } 953 if query.Repository.Milestone == nil { 954 return nil, fmt.Errorf("no milestone found with number '%d'", number) 955 } 956 957 return query.Repository.Milestone, nil 958 } 959 960 func ProjectNamesToPaths(client *Client, repo ghrepo.Interface, projectNames []string) ([]string, error) { 961 var paths []string 962 projects, err := RepoAndOrgProjects(client, repo) 963 if err != nil { 964 return paths, err 965 } 966 return ProjectsToPaths(projects, projectNames) 967 }