code.gitea.io/gitea@v1.21.7/services/migrations/github.go (about) 1 // Copyright 2019 The Gitea Authors. All rights reserved. 2 // Copyright 2018 Jonas Franz. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package migrations 6 7 import ( 8 "context" 9 "fmt" 10 "io" 11 "net/http" 12 "net/url" 13 "strconv" 14 "strings" 15 "time" 16 17 "code.gitea.io/gitea/modules/git" 18 "code.gitea.io/gitea/modules/log" 19 base "code.gitea.io/gitea/modules/migration" 20 "code.gitea.io/gitea/modules/proxy" 21 "code.gitea.io/gitea/modules/structs" 22 23 "github.com/google/go-github/v53/github" 24 "golang.org/x/oauth2" 25 ) 26 27 var ( 28 _ base.Downloader = &GithubDownloaderV3{} 29 _ base.DownloaderFactory = &GithubDownloaderV3Factory{} 30 // GithubLimitRateRemaining limit to wait for new rate to apply 31 GithubLimitRateRemaining = 0 32 ) 33 34 func init() { 35 RegisterDownloaderFactory(&GithubDownloaderV3Factory{}) 36 } 37 38 // GithubDownloaderV3Factory defines a github downloader v3 factory 39 type GithubDownloaderV3Factory struct{} 40 41 // New returns a Downloader related to this factory according MigrateOptions 42 func (f *GithubDownloaderV3Factory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) { 43 u, err := url.Parse(opts.CloneAddr) 44 if err != nil { 45 return nil, err 46 } 47 48 baseURL := u.Scheme + "://" + u.Host 49 fields := strings.Split(u.Path, "/") 50 oldOwner := fields[1] 51 oldName := strings.TrimSuffix(fields[2], ".git") 52 53 log.Trace("Create github downloader BaseURL: %s %s/%s", baseURL, oldOwner, oldName) 54 55 return NewGithubDownloaderV3(ctx, baseURL, opts.AuthUsername, opts.AuthPassword, opts.AuthToken, oldOwner, oldName), nil 56 } 57 58 // GitServiceType returns the type of git service 59 func (f *GithubDownloaderV3Factory) GitServiceType() structs.GitServiceType { 60 return structs.GithubService 61 } 62 63 // GithubDownloaderV3 implements a Downloader interface to get repository information 64 // from github via APIv3 65 type GithubDownloaderV3 struct { 66 base.NullDownloader 67 ctx context.Context 68 clients []*github.Client 69 baseURL string 70 repoOwner string 71 repoName string 72 userName string 73 password string 74 rates []*github.Rate 75 curClientIdx int 76 maxPerPage int 77 SkipReactions bool 78 SkipReviews bool 79 } 80 81 // NewGithubDownloaderV3 creates a github Downloader via github v3 API 82 func NewGithubDownloaderV3(ctx context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GithubDownloaderV3 { 83 downloader := GithubDownloaderV3{ 84 userName: userName, 85 baseURL: baseURL, 86 password: password, 87 ctx: ctx, 88 repoOwner: repoOwner, 89 repoName: repoName, 90 maxPerPage: 100, 91 } 92 93 if token != "" { 94 tokens := strings.Split(token, ",") 95 for _, token := range tokens { 96 token = strings.TrimSpace(token) 97 ts := oauth2.StaticTokenSource( 98 &oauth2.Token{AccessToken: token}, 99 ) 100 client := &http.Client{ 101 Transport: &oauth2.Transport{ 102 Base: NewMigrationHTTPTransport(), 103 Source: oauth2.ReuseTokenSource(nil, ts), 104 }, 105 } 106 107 downloader.addClient(client, baseURL) 108 } 109 } else { 110 transport := NewMigrationHTTPTransport() 111 transport.Proxy = func(req *http.Request) (*url.URL, error) { 112 req.SetBasicAuth(userName, password) 113 return proxy.Proxy()(req) 114 } 115 client := &http.Client{ 116 Transport: transport, 117 } 118 downloader.addClient(client, baseURL) 119 } 120 return &downloader 121 } 122 123 // String implements Stringer 124 func (g *GithubDownloaderV3) String() string { 125 return fmt.Sprintf("migration from github server %s %s/%s", g.baseURL, g.repoOwner, g.repoName) 126 } 127 128 func (g *GithubDownloaderV3) LogString() string { 129 if g == nil { 130 return "<GithubDownloaderV3 nil>" 131 } 132 return fmt.Sprintf("<GithubDownloaderV3 %s %s/%s>", g.baseURL, g.repoOwner, g.repoName) 133 } 134 135 func (g *GithubDownloaderV3) addClient(client *http.Client, baseURL string) { 136 githubClient := github.NewClient(client) 137 if baseURL != "https://github.com" { 138 githubClient, _ = github.NewEnterpriseClient(baseURL, baseURL, client) 139 } 140 g.clients = append(g.clients, githubClient) 141 g.rates = append(g.rates, nil) 142 } 143 144 // SetContext set context 145 func (g *GithubDownloaderV3) SetContext(ctx context.Context) { 146 g.ctx = ctx 147 } 148 149 func (g *GithubDownloaderV3) waitAndPickClient() { 150 var recentIdx int 151 var maxRemaining int 152 for i := 0; i < len(g.clients); i++ { 153 if g.rates[i] != nil && g.rates[i].Remaining > maxRemaining { 154 maxRemaining = g.rates[i].Remaining 155 recentIdx = i 156 } 157 } 158 g.curClientIdx = recentIdx // if no max remain, it will always pick the first client. 159 160 for g.rates[g.curClientIdx] != nil && g.rates[g.curClientIdx].Remaining <= GithubLimitRateRemaining { 161 timer := time.NewTimer(time.Until(g.rates[g.curClientIdx].Reset.Time)) 162 select { 163 case <-g.ctx.Done(): 164 timer.Stop() 165 return 166 case <-timer.C: 167 } 168 169 err := g.RefreshRate() 170 if err != nil { 171 log.Error("g.getClient().RateLimits: %s", err) 172 } 173 } 174 } 175 176 // RefreshRate update the current rate (doesn't count in rate limit) 177 func (g *GithubDownloaderV3) RefreshRate() error { 178 rates, _, err := g.getClient().RateLimits(g.ctx) 179 if err != nil { 180 // if rate limit is not enabled, ignore it 181 if strings.Contains(err.Error(), "404") { 182 g.setRate(nil) 183 return nil 184 } 185 return err 186 } 187 188 g.setRate(rates.GetCore()) 189 return nil 190 } 191 192 func (g *GithubDownloaderV3) getClient() *github.Client { 193 return g.clients[g.curClientIdx] 194 } 195 196 func (g *GithubDownloaderV3) setRate(rate *github.Rate) { 197 g.rates[g.curClientIdx] = rate 198 } 199 200 // GetRepoInfo returns a repository information 201 func (g *GithubDownloaderV3) GetRepoInfo() (*base.Repository, error) { 202 g.waitAndPickClient() 203 gr, resp, err := g.getClient().Repositories.Get(g.ctx, g.repoOwner, g.repoName) 204 if err != nil { 205 return nil, err 206 } 207 g.setRate(&resp.Rate) 208 209 // convert github repo to stand Repo 210 return &base.Repository{ 211 Owner: g.repoOwner, 212 Name: gr.GetName(), 213 IsPrivate: gr.GetPrivate(), 214 Description: gr.GetDescription(), 215 OriginalURL: gr.GetHTMLURL(), 216 CloneURL: gr.GetCloneURL(), 217 DefaultBranch: gr.GetDefaultBranch(), 218 }, nil 219 } 220 221 // GetTopics return github topics 222 func (g *GithubDownloaderV3) GetTopics() ([]string, error) { 223 g.waitAndPickClient() 224 r, resp, err := g.getClient().Repositories.Get(g.ctx, g.repoOwner, g.repoName) 225 if err != nil { 226 return nil, err 227 } 228 g.setRate(&resp.Rate) 229 return r.Topics, nil 230 } 231 232 // GetMilestones returns milestones 233 func (g *GithubDownloaderV3) GetMilestones() ([]*base.Milestone, error) { 234 perPage := g.maxPerPage 235 milestones := make([]*base.Milestone, 0, perPage) 236 for i := 1; ; i++ { 237 g.waitAndPickClient() 238 ms, resp, err := g.getClient().Issues.ListMilestones(g.ctx, g.repoOwner, g.repoName, 239 &github.MilestoneListOptions{ 240 State: "all", 241 ListOptions: github.ListOptions{ 242 Page: i, 243 PerPage: perPage, 244 }, 245 }) 246 if err != nil { 247 return nil, err 248 } 249 g.setRate(&resp.Rate) 250 251 for _, m := range ms { 252 state := "open" 253 if m.State != nil { 254 state = *m.State 255 } 256 milestones = append(milestones, &base.Milestone{ 257 Title: m.GetTitle(), 258 Description: m.GetDescription(), 259 Deadline: m.DueOn.GetTime(), 260 State: state, 261 Created: m.GetCreatedAt().Time, 262 Updated: m.UpdatedAt.GetTime(), 263 Closed: m.ClosedAt.GetTime(), 264 }) 265 } 266 if len(ms) < perPage { 267 break 268 } 269 } 270 return milestones, nil 271 } 272 273 func convertGithubLabel(label *github.Label) *base.Label { 274 return &base.Label{ 275 Name: label.GetName(), 276 Color: label.GetColor(), 277 Description: label.GetDescription(), 278 } 279 } 280 281 // GetLabels returns labels 282 func (g *GithubDownloaderV3) GetLabels() ([]*base.Label, error) { 283 perPage := g.maxPerPage 284 labels := make([]*base.Label, 0, perPage) 285 for i := 1; ; i++ { 286 g.waitAndPickClient() 287 ls, resp, err := g.getClient().Issues.ListLabels(g.ctx, g.repoOwner, g.repoName, 288 &github.ListOptions{ 289 Page: i, 290 PerPage: perPage, 291 }) 292 if err != nil { 293 return nil, err 294 } 295 g.setRate(&resp.Rate) 296 297 for _, label := range ls { 298 labels = append(labels, convertGithubLabel(label)) 299 } 300 if len(ls) < perPage { 301 break 302 } 303 } 304 return labels, nil 305 } 306 307 func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease) *base.Release { 308 // GitHub allows commitish to be a reference. 309 // In this case, we need to remove the prefix, i.e. convert "refs/heads/main" to "main". 310 targetCommitish := strings.TrimPrefix(rel.GetTargetCommitish(), git.BranchPrefix) 311 312 r := &base.Release{ 313 Name: rel.GetName(), 314 TagName: rel.GetTagName(), 315 TargetCommitish: targetCommitish, 316 Draft: rel.GetDraft(), 317 Prerelease: rel.GetPrerelease(), 318 Created: rel.GetCreatedAt().Time, 319 PublisherID: rel.GetAuthor().GetID(), 320 PublisherName: rel.GetAuthor().GetLogin(), 321 PublisherEmail: rel.GetAuthor().GetEmail(), 322 Body: rel.GetBody(), 323 } 324 325 if rel.PublishedAt != nil { 326 r.Published = rel.PublishedAt.Time 327 } 328 329 httpClient := NewMigrationHTTPClient() 330 331 for _, asset := range rel.Assets { 332 assetID := *asset.ID // Don't optimize this, for closure we need a local variable 333 r.Assets = append(r.Assets, &base.ReleaseAsset{ 334 ID: asset.GetID(), 335 Name: asset.GetName(), 336 ContentType: asset.ContentType, 337 Size: asset.Size, 338 DownloadCount: asset.DownloadCount, 339 Created: asset.CreatedAt.Time, 340 Updated: asset.UpdatedAt.Time, 341 DownloadFunc: func() (io.ReadCloser, error) { 342 g.waitAndPickClient() 343 readCloser, redirectURL, err := g.getClient().Repositories.DownloadReleaseAsset(g.ctx, g.repoOwner, g.repoName, assetID, nil) 344 if err != nil { 345 return nil, err 346 } 347 if err := g.RefreshRate(); err != nil { 348 log.Error("g.getClient().RateLimits: %s", err) 349 } 350 351 if readCloser != nil { 352 return readCloser, nil 353 } 354 355 if redirectURL == "" { 356 return nil, fmt.Errorf("no release asset found for %d", assetID) 357 } 358 359 // Prevent open redirect 360 if !hasBaseURL(redirectURL, g.baseURL) && 361 !hasBaseURL(redirectURL, "https://objects.githubusercontent.com/") { 362 WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", asset.GetID(), g, redirectURL) 363 364 return io.NopCloser(strings.NewReader(redirectURL)), nil 365 } 366 367 g.waitAndPickClient() 368 req, err := http.NewRequestWithContext(g.ctx, "GET", redirectURL, nil) 369 if err != nil { 370 return nil, err 371 } 372 resp, err := httpClient.Do(req) 373 err1 := g.RefreshRate() 374 if err1 != nil { 375 log.Error("g.RefreshRate(): %s", err1) 376 } 377 if err != nil { 378 return nil, err 379 } 380 return resp.Body, nil 381 }, 382 }) 383 } 384 return r 385 } 386 387 // GetReleases returns releases 388 func (g *GithubDownloaderV3) GetReleases() ([]*base.Release, error) { 389 perPage := g.maxPerPage 390 releases := make([]*base.Release, 0, perPage) 391 for i := 1; ; i++ { 392 g.waitAndPickClient() 393 ls, resp, err := g.getClient().Repositories.ListReleases(g.ctx, g.repoOwner, g.repoName, 394 &github.ListOptions{ 395 Page: i, 396 PerPage: perPage, 397 }) 398 if err != nil { 399 return nil, err 400 } 401 g.setRate(&resp.Rate) 402 403 for _, release := range ls { 404 releases = append(releases, g.convertGithubRelease(release)) 405 } 406 if len(ls) < perPage { 407 break 408 } 409 } 410 return releases, nil 411 } 412 413 // GetIssues returns issues according start and limit 414 func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool, error) { 415 if perPage > g.maxPerPage { 416 perPage = g.maxPerPage 417 } 418 opt := &github.IssueListByRepoOptions{ 419 Sort: "created", 420 Direction: "asc", 421 State: "all", 422 ListOptions: github.ListOptions{ 423 PerPage: perPage, 424 Page: page, 425 }, 426 } 427 428 allIssues := make([]*base.Issue, 0, perPage) 429 g.waitAndPickClient() 430 issues, resp, err := g.getClient().Issues.ListByRepo(g.ctx, g.repoOwner, g.repoName, opt) 431 if err != nil { 432 return nil, false, fmt.Errorf("error while listing repos: %w", err) 433 } 434 log.Trace("Request get issues %d/%d, but in fact get %d", perPage, page, len(issues)) 435 g.setRate(&resp.Rate) 436 for _, issue := range issues { 437 if issue.IsPullRequest() { 438 continue 439 } 440 441 labels := make([]*base.Label, 0, len(issue.Labels)) 442 for _, l := range issue.Labels { 443 labels = append(labels, convertGithubLabel(l)) 444 } 445 446 // get reactions 447 var reactions []*base.Reaction 448 if !g.SkipReactions { 449 for i := 1; ; i++ { 450 g.waitAndPickClient() 451 res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, issue.GetNumber(), &github.ListOptions{ 452 Page: i, 453 PerPage: perPage, 454 }) 455 if err != nil { 456 return nil, false, err 457 } 458 g.setRate(&resp.Rate) 459 if len(res) == 0 { 460 break 461 } 462 for _, reaction := range res { 463 reactions = append(reactions, &base.Reaction{ 464 UserID: reaction.User.GetID(), 465 UserName: reaction.User.GetLogin(), 466 Content: reaction.GetContent(), 467 }) 468 } 469 } 470 } 471 472 var assignees []string 473 for i := range issue.Assignees { 474 assignees = append(assignees, issue.Assignees[i].GetLogin()) 475 } 476 477 allIssues = append(allIssues, &base.Issue{ 478 Title: *issue.Title, 479 Number: int64(*issue.Number), 480 PosterID: issue.GetUser().GetID(), 481 PosterName: issue.GetUser().GetLogin(), 482 PosterEmail: issue.GetUser().GetEmail(), 483 Content: issue.GetBody(), 484 Milestone: issue.GetMilestone().GetTitle(), 485 State: issue.GetState(), 486 Created: issue.GetCreatedAt().Time, 487 Updated: issue.GetUpdatedAt().Time, 488 Labels: labels, 489 Reactions: reactions, 490 Closed: issue.ClosedAt.GetTime(), 491 IsLocked: issue.GetLocked(), 492 Assignees: assignees, 493 ForeignIndex: int64(*issue.Number), 494 }) 495 } 496 497 return allIssues, len(issues) < perPage, nil 498 } 499 500 // SupportGetRepoComments return true if it supports get repo comments 501 func (g *GithubDownloaderV3) SupportGetRepoComments() bool { 502 return true 503 } 504 505 // GetComments returns comments according issueNumber 506 func (g *GithubDownloaderV3) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) { 507 comments, err := g.getComments(commentable) 508 return comments, false, err 509 } 510 511 func (g *GithubDownloaderV3) getComments(commentable base.Commentable) ([]*base.Comment, error) { 512 var ( 513 allComments = make([]*base.Comment, 0, g.maxPerPage) 514 created = "created" 515 asc = "asc" 516 ) 517 opt := &github.IssueListCommentsOptions{ 518 Sort: &created, 519 Direction: &asc, 520 ListOptions: github.ListOptions{ 521 PerPage: g.maxPerPage, 522 }, 523 } 524 for { 525 g.waitAndPickClient() 526 comments, resp, err := g.getClient().Issues.ListComments(g.ctx, g.repoOwner, g.repoName, int(commentable.GetForeignIndex()), opt) 527 if err != nil { 528 return nil, fmt.Errorf("error while listing repos: %w", err) 529 } 530 g.setRate(&resp.Rate) 531 for _, comment := range comments { 532 // get reactions 533 var reactions []*base.Reaction 534 if !g.SkipReactions { 535 for i := 1; ; i++ { 536 g.waitAndPickClient() 537 res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListOptions{ 538 Page: i, 539 PerPage: g.maxPerPage, 540 }) 541 if err != nil { 542 return nil, err 543 } 544 g.setRate(&resp.Rate) 545 if len(res) == 0 { 546 break 547 } 548 for _, reaction := range res { 549 reactions = append(reactions, &base.Reaction{ 550 UserID: reaction.User.GetID(), 551 UserName: reaction.User.GetLogin(), 552 Content: reaction.GetContent(), 553 }) 554 } 555 } 556 } 557 558 allComments = append(allComments, &base.Comment{ 559 IssueIndex: commentable.GetLocalIndex(), 560 Index: comment.GetID(), 561 PosterID: comment.GetUser().GetID(), 562 PosterName: comment.GetUser().GetLogin(), 563 PosterEmail: comment.GetUser().GetEmail(), 564 Content: comment.GetBody(), 565 Created: comment.GetCreatedAt().Time, 566 Updated: comment.GetUpdatedAt().Time, 567 Reactions: reactions, 568 }) 569 } 570 if resp.NextPage == 0 { 571 break 572 } 573 opt.Page = resp.NextPage 574 } 575 return allComments, nil 576 } 577 578 // GetAllComments returns repository comments according page and perPageSize 579 func (g *GithubDownloaderV3) GetAllComments(page, perPage int) ([]*base.Comment, bool, error) { 580 var ( 581 allComments = make([]*base.Comment, 0, perPage) 582 created = "created" 583 asc = "asc" 584 ) 585 if perPage > g.maxPerPage { 586 perPage = g.maxPerPage 587 } 588 opt := &github.IssueListCommentsOptions{ 589 Sort: &created, 590 Direction: &asc, 591 ListOptions: github.ListOptions{ 592 Page: page, 593 PerPage: perPage, 594 }, 595 } 596 597 g.waitAndPickClient() 598 comments, resp, err := g.getClient().Issues.ListComments(g.ctx, g.repoOwner, g.repoName, 0, opt) 599 if err != nil { 600 return nil, false, fmt.Errorf("error while listing repos: %w", err) 601 } 602 isEnd := resp.NextPage == 0 603 604 log.Trace("Request get comments %d/%d, but in fact get %d, next page is %d", perPage, page, len(comments), resp.NextPage) 605 g.setRate(&resp.Rate) 606 for _, comment := range comments { 607 // get reactions 608 var reactions []*base.Reaction 609 if !g.SkipReactions { 610 for i := 1; ; i++ { 611 g.waitAndPickClient() 612 res, resp, err := g.getClient().Reactions.ListIssueCommentReactions(g.ctx, g.repoOwner, g.repoName, comment.GetID(), &github.ListOptions{ 613 Page: i, 614 PerPage: g.maxPerPage, 615 }) 616 if err != nil { 617 return nil, false, err 618 } 619 g.setRate(&resp.Rate) 620 if len(res) == 0 { 621 break 622 } 623 for _, reaction := range res { 624 reactions = append(reactions, &base.Reaction{ 625 UserID: reaction.User.GetID(), 626 UserName: reaction.User.GetLogin(), 627 Content: reaction.GetContent(), 628 }) 629 } 630 } 631 } 632 idx := strings.LastIndex(*comment.IssueURL, "/") 633 issueIndex, _ := strconv.ParseInt((*comment.IssueURL)[idx+1:], 10, 64) 634 allComments = append(allComments, &base.Comment{ 635 IssueIndex: issueIndex, 636 Index: comment.GetID(), 637 PosterID: comment.GetUser().GetID(), 638 PosterName: comment.GetUser().GetLogin(), 639 PosterEmail: comment.GetUser().GetEmail(), 640 Content: comment.GetBody(), 641 Created: comment.GetCreatedAt().Time, 642 Updated: comment.GetUpdatedAt().Time, 643 Reactions: reactions, 644 }) 645 } 646 647 return allComments, isEnd, nil 648 } 649 650 // GetPullRequests returns pull requests according page and perPage 651 func (g *GithubDownloaderV3) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) { 652 if perPage > g.maxPerPage { 653 perPage = g.maxPerPage 654 } 655 opt := &github.PullRequestListOptions{ 656 Sort: "created", 657 Direction: "asc", 658 State: "all", 659 ListOptions: github.ListOptions{ 660 PerPage: perPage, 661 Page: page, 662 }, 663 } 664 allPRs := make([]*base.PullRequest, 0, perPage) 665 g.waitAndPickClient() 666 prs, resp, err := g.getClient().PullRequests.List(g.ctx, g.repoOwner, g.repoName, opt) 667 if err != nil { 668 return nil, false, fmt.Errorf("error while listing repos: %w", err) 669 } 670 log.Trace("Request get pull requests %d/%d, but in fact get %d", perPage, page, len(prs)) 671 g.setRate(&resp.Rate) 672 for _, pr := range prs { 673 labels := make([]*base.Label, 0, len(pr.Labels)) 674 for _, l := range pr.Labels { 675 labels = append(labels, convertGithubLabel(l)) 676 } 677 678 // get reactions 679 var reactions []*base.Reaction 680 if !g.SkipReactions { 681 for i := 1; ; i++ { 682 g.waitAndPickClient() 683 res, resp, err := g.getClient().Reactions.ListIssueReactions(g.ctx, g.repoOwner, g.repoName, pr.GetNumber(), &github.ListOptions{ 684 Page: i, 685 PerPage: perPage, 686 }) 687 if err != nil { 688 return nil, false, err 689 } 690 g.setRate(&resp.Rate) 691 if len(res) == 0 { 692 break 693 } 694 for _, reaction := range res { 695 reactions = append(reactions, &base.Reaction{ 696 UserID: reaction.User.GetID(), 697 UserName: reaction.User.GetLogin(), 698 Content: reaction.GetContent(), 699 }) 700 } 701 } 702 } 703 704 // download patch and saved as tmp file 705 g.waitAndPickClient() 706 707 allPRs = append(allPRs, &base.PullRequest{ 708 Title: pr.GetTitle(), 709 Number: int64(pr.GetNumber()), 710 PosterID: pr.GetUser().GetID(), 711 PosterName: pr.GetUser().GetLogin(), 712 PosterEmail: pr.GetUser().GetEmail(), 713 Content: pr.GetBody(), 714 Milestone: pr.GetMilestone().GetTitle(), 715 State: pr.GetState(), 716 Created: pr.GetCreatedAt().Time, 717 Updated: pr.GetUpdatedAt().Time, 718 Closed: pr.ClosedAt.GetTime(), 719 Labels: labels, 720 Merged: pr.MergedAt != nil, 721 MergeCommitSHA: pr.GetMergeCommitSHA(), 722 MergedTime: pr.MergedAt.GetTime(), 723 IsLocked: pr.ActiveLockReason != nil, 724 Head: base.PullRequestBranch{ 725 Ref: pr.GetHead().GetRef(), 726 SHA: pr.GetHead().GetSHA(), 727 OwnerName: pr.GetHead().GetUser().GetLogin(), 728 RepoName: pr.GetHead().GetRepo().GetName(), 729 CloneURL: pr.GetHead().GetRepo().GetCloneURL(), // see below for SECURITY related issues here 730 }, 731 Base: base.PullRequestBranch{ 732 Ref: pr.GetBase().GetRef(), 733 SHA: pr.GetBase().GetSHA(), 734 RepoName: pr.GetBase().GetRepo().GetName(), 735 OwnerName: pr.GetBase().GetUser().GetLogin(), 736 }, 737 PatchURL: pr.GetPatchURL(), // see below for SECURITY related issues here 738 Reactions: reactions, 739 ForeignIndex: int64(*pr.Number), 740 }) 741 742 // SECURITY: Ensure that the PR is safe 743 _ = CheckAndEnsureSafePR(allPRs[len(allPRs)-1], g.baseURL, g) 744 } 745 746 return allPRs, len(prs) < perPage, nil 747 } 748 749 func convertGithubReview(r *github.PullRequestReview) *base.Review { 750 return &base.Review{ 751 ID: r.GetID(), 752 ReviewerID: r.GetUser().GetID(), 753 ReviewerName: r.GetUser().GetLogin(), 754 CommitID: r.GetCommitID(), 755 Content: r.GetBody(), 756 CreatedAt: r.GetSubmittedAt().Time, 757 State: r.GetState(), 758 } 759 } 760 761 func (g *GithubDownloaderV3) convertGithubReviewComments(cs []*github.PullRequestComment) ([]*base.ReviewComment, error) { 762 rcs := make([]*base.ReviewComment, 0, len(cs)) 763 for _, c := range cs { 764 // get reactions 765 var reactions []*base.Reaction 766 if !g.SkipReactions { 767 for i := 1; ; i++ { 768 g.waitAndPickClient() 769 res, resp, err := g.getClient().Reactions.ListPullRequestCommentReactions(g.ctx, g.repoOwner, g.repoName, c.GetID(), &github.ListOptions{ 770 Page: i, 771 PerPage: g.maxPerPage, 772 }) 773 if err != nil { 774 return nil, err 775 } 776 g.setRate(&resp.Rate) 777 if len(res) == 0 { 778 break 779 } 780 for _, reaction := range res { 781 reactions = append(reactions, &base.Reaction{ 782 UserID: reaction.User.GetID(), 783 UserName: reaction.User.GetLogin(), 784 Content: reaction.GetContent(), 785 }) 786 } 787 } 788 } 789 790 rcs = append(rcs, &base.ReviewComment{ 791 ID: c.GetID(), 792 InReplyTo: c.GetInReplyTo(), 793 Content: c.GetBody(), 794 TreePath: c.GetPath(), 795 DiffHunk: c.GetDiffHunk(), 796 Position: c.GetPosition(), 797 CommitID: c.GetCommitID(), 798 PosterID: c.GetUser().GetID(), 799 Reactions: reactions, 800 CreatedAt: c.GetCreatedAt().Time, 801 UpdatedAt: c.GetUpdatedAt().Time, 802 }) 803 } 804 return rcs, nil 805 } 806 807 // GetReviews returns pull requests review 808 func (g *GithubDownloaderV3) GetReviews(reviewable base.Reviewable) ([]*base.Review, error) { 809 allReviews := make([]*base.Review, 0, g.maxPerPage) 810 if g.SkipReviews { 811 return allReviews, nil 812 } 813 opt := &github.ListOptions{ 814 PerPage: g.maxPerPage, 815 } 816 // Get approve/request change reviews 817 for { 818 g.waitAndPickClient() 819 reviews, resp, err := g.getClient().PullRequests.ListReviews(g.ctx, g.repoOwner, g.repoName, int(reviewable.GetForeignIndex()), opt) 820 if err != nil { 821 return nil, fmt.Errorf("error while listing repos: %w", err) 822 } 823 g.setRate(&resp.Rate) 824 for _, review := range reviews { 825 r := convertGithubReview(review) 826 r.IssueIndex = reviewable.GetLocalIndex() 827 // retrieve all review comments 828 opt2 := &github.ListOptions{ 829 PerPage: g.maxPerPage, 830 } 831 for { 832 g.waitAndPickClient() 833 reviewComments, resp, err := g.getClient().PullRequests.ListReviewComments(g.ctx, g.repoOwner, g.repoName, int(reviewable.GetForeignIndex()), review.GetID(), opt2) 834 if err != nil { 835 return nil, fmt.Errorf("error while listing repos: %w", err) 836 } 837 g.setRate(&resp.Rate) 838 839 cs, err := g.convertGithubReviewComments(reviewComments) 840 if err != nil { 841 return nil, err 842 } 843 r.Comments = append(r.Comments, cs...) 844 if resp.NextPage == 0 { 845 break 846 } 847 opt2.Page = resp.NextPage 848 } 849 allReviews = append(allReviews, r) 850 } 851 if resp.NextPage == 0 { 852 break 853 } 854 opt.Page = resp.NextPage 855 } 856 // Get requested reviews 857 for { 858 g.waitAndPickClient() 859 reviewers, resp, err := g.getClient().PullRequests.ListReviewers(g.ctx, g.repoOwner, g.repoName, int(reviewable.GetForeignIndex()), opt) 860 if err != nil { 861 return nil, fmt.Errorf("error while listing repos: %w", err) 862 } 863 g.setRate(&resp.Rate) 864 for _, user := range reviewers.Users { 865 r := &base.Review{ 866 ReviewerID: user.GetID(), 867 ReviewerName: user.GetLogin(), 868 State: base.ReviewStateRequestReview, 869 IssueIndex: reviewable.GetLocalIndex(), 870 } 871 allReviews = append(allReviews, r) 872 } 873 // TODO: Handle Team requests 874 if resp.NextPage == 0 { 875 break 876 } 877 opt.Page = resp.NextPage 878 } 879 return allReviews, nil 880 }