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  }