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