code.gitea.io/gitea@v1.21.7/services/migrations/gitea_downloader.go (about)

     1  // Copyright 2020 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package migrations
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"net/http"
    12  	"net/url"
    13  	"strings"
    14  	"time"
    15  
    16  	"code.gitea.io/gitea/modules/log"
    17  	base "code.gitea.io/gitea/modules/migration"
    18  	"code.gitea.io/gitea/modules/structs"
    19  
    20  	gitea_sdk "code.gitea.io/sdk/gitea"
    21  )
    22  
    23  var (
    24  	_ base.Downloader        = &GiteaDownloader{}
    25  	_ base.DownloaderFactory = &GiteaDownloaderFactory{}
    26  )
    27  
    28  func init() {
    29  	RegisterDownloaderFactory(&GiteaDownloaderFactory{})
    30  }
    31  
    32  // GiteaDownloaderFactory defines a gitea downloader factory
    33  type GiteaDownloaderFactory struct{}
    34  
    35  // New returns a Downloader related to this factory according MigrateOptions
    36  func (f *GiteaDownloaderFactory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) {
    37  	u, err := url.Parse(opts.CloneAddr)
    38  	if err != nil {
    39  		return nil, err
    40  	}
    41  
    42  	baseURL := u.Scheme + "://" + u.Host
    43  	repoNameSpace := strings.TrimPrefix(u.Path, "/")
    44  	repoNameSpace = strings.TrimSuffix(repoNameSpace, ".git")
    45  
    46  	path := strings.Split(repoNameSpace, "/")
    47  	if len(path) < 2 {
    48  		return nil, fmt.Errorf("invalid path: %s", repoNameSpace)
    49  	}
    50  
    51  	repoPath := strings.Join(path[len(path)-2:], "/")
    52  	if len(path) > 2 {
    53  		subPath := strings.Join(path[:len(path)-2], "/")
    54  		baseURL += "/" + subPath
    55  	}
    56  
    57  	log.Trace("Create gitea downloader. BaseURL: %s RepoName: %s", baseURL, repoNameSpace)
    58  
    59  	return NewGiteaDownloader(ctx, baseURL, repoPath, opts.AuthUsername, opts.AuthPassword, opts.AuthToken)
    60  }
    61  
    62  // GitServiceType returns the type of git service
    63  func (f *GiteaDownloaderFactory) GitServiceType() structs.GitServiceType {
    64  	return structs.GiteaService
    65  }
    66  
    67  // GiteaDownloader implements a Downloader interface to get repository information's
    68  type GiteaDownloader struct {
    69  	base.NullDownloader
    70  	ctx        context.Context
    71  	client     *gitea_sdk.Client
    72  	baseURL    string
    73  	repoOwner  string
    74  	repoName   string
    75  	pagination bool
    76  	maxPerPage int
    77  }
    78  
    79  // NewGiteaDownloader creates a gitea Downloader via gitea API
    80  //
    81  //	Use either a username/password or personal token. token is preferred
    82  //	Note: Public access only allows very basic access
    83  func NewGiteaDownloader(ctx context.Context, baseURL, repoPath, username, password, token string) (*GiteaDownloader, error) {
    84  	giteaClient, err := gitea_sdk.NewClient(
    85  		baseURL,
    86  		gitea_sdk.SetToken(token),
    87  		gitea_sdk.SetBasicAuth(username, password),
    88  		gitea_sdk.SetContext(ctx),
    89  		gitea_sdk.SetHTTPClient(NewMigrationHTTPClient()),
    90  	)
    91  	if err != nil {
    92  		log.Error(fmt.Sprintf("Failed to create NewGiteaDownloader for: %s. Error: %v", baseURL, err))
    93  		return nil, err
    94  	}
    95  
    96  	path := strings.Split(repoPath, "/")
    97  
    98  	paginationSupport := true
    99  	if err = giteaClient.CheckServerVersionConstraint(">=1.12"); err != nil {
   100  		paginationSupport = false
   101  	}
   102  
   103  	// set small maxPerPage since we can only guess
   104  	// (default would be 50 but this can differ)
   105  	maxPerPage := 10
   106  	// gitea instances >=1.13 can tell us what maximum they have
   107  	apiConf, _, err := giteaClient.GetGlobalAPISettings()
   108  	if err != nil {
   109  		log.Info("Unable to get global API settings. Ignoring these.")
   110  		log.Debug("giteaClient.GetGlobalAPISettings. Error: %v", err)
   111  	}
   112  	if apiConf != nil {
   113  		maxPerPage = apiConf.MaxResponseItems
   114  	}
   115  
   116  	return &GiteaDownloader{
   117  		ctx:        ctx,
   118  		client:     giteaClient,
   119  		baseURL:    baseURL,
   120  		repoOwner:  path[0],
   121  		repoName:   path[1],
   122  		pagination: paginationSupport,
   123  		maxPerPage: maxPerPage,
   124  	}, nil
   125  }
   126  
   127  // SetContext set context
   128  func (g *GiteaDownloader) SetContext(ctx context.Context) {
   129  	g.ctx = ctx
   130  }
   131  
   132  // String implements Stringer
   133  func (g *GiteaDownloader) String() string {
   134  	return fmt.Sprintf("migration from gitea server %s %s/%s", g.baseURL, g.repoOwner, g.repoName)
   135  }
   136  
   137  func (g *GiteaDownloader) LogString() string {
   138  	if g == nil {
   139  		return "<GiteaDownloader nil>"
   140  	}
   141  	return fmt.Sprintf("<GiteaDownloader %s %s/%s>", g.baseURL, g.repoOwner, g.repoName)
   142  }
   143  
   144  // GetRepoInfo returns a repository information
   145  func (g *GiteaDownloader) GetRepoInfo() (*base.Repository, error) {
   146  	if g == nil {
   147  		return nil, errors.New("error: GiteaDownloader is nil")
   148  	}
   149  
   150  	repo, _, err := g.client.GetRepo(g.repoOwner, g.repoName)
   151  	if err != nil {
   152  		return nil, err
   153  	}
   154  
   155  	return &base.Repository{
   156  		Name:          repo.Name,
   157  		Owner:         repo.Owner.UserName,
   158  		IsPrivate:     repo.Private,
   159  		Description:   repo.Description,
   160  		CloneURL:      repo.CloneURL,
   161  		OriginalURL:   repo.HTMLURL,
   162  		DefaultBranch: repo.DefaultBranch,
   163  	}, nil
   164  }
   165  
   166  // GetTopics return gitea topics
   167  func (g *GiteaDownloader) GetTopics() ([]string, error) {
   168  	topics, _, err := g.client.ListRepoTopics(g.repoOwner, g.repoName, gitea_sdk.ListRepoTopicsOptions{})
   169  	return topics, err
   170  }
   171  
   172  // GetMilestones returns milestones
   173  func (g *GiteaDownloader) GetMilestones() ([]*base.Milestone, error) {
   174  	milestones := make([]*base.Milestone, 0, g.maxPerPage)
   175  
   176  	for i := 1; ; i++ {
   177  		// make sure gitea can shutdown gracefully
   178  		select {
   179  		case <-g.ctx.Done():
   180  			return nil, nil
   181  		default:
   182  		}
   183  
   184  		ms, _, err := g.client.ListRepoMilestones(g.repoOwner, g.repoName, gitea_sdk.ListMilestoneOption{
   185  			ListOptions: gitea_sdk.ListOptions{
   186  				PageSize: g.maxPerPage,
   187  				Page:     i,
   188  			},
   189  			State: gitea_sdk.StateAll,
   190  		})
   191  		if err != nil {
   192  			return nil, err
   193  		}
   194  
   195  		for i := range ms {
   196  			// old gitea instances dont have this information
   197  			createdAT := time.Time{}
   198  			var updatedAT *time.Time
   199  			if ms[i].Closed != nil {
   200  				createdAT = *ms[i].Closed
   201  				updatedAT = ms[i].Closed
   202  			}
   203  
   204  			// new gitea instances (>=1.13) do
   205  			if !ms[i].Created.IsZero() {
   206  				createdAT = ms[i].Created
   207  			}
   208  			if ms[i].Updated != nil && !ms[i].Updated.IsZero() {
   209  				updatedAT = ms[i].Updated
   210  			}
   211  
   212  			milestones = append(milestones, &base.Milestone{
   213  				Title:       ms[i].Title,
   214  				Description: ms[i].Description,
   215  				Deadline:    ms[i].Deadline,
   216  				Created:     createdAT,
   217  				Updated:     updatedAT,
   218  				Closed:      ms[i].Closed,
   219  				State:       string(ms[i].State),
   220  			})
   221  		}
   222  		if !g.pagination || len(ms) < g.maxPerPage {
   223  			break
   224  		}
   225  	}
   226  	return milestones, nil
   227  }
   228  
   229  func (g *GiteaDownloader) convertGiteaLabel(label *gitea_sdk.Label) *base.Label {
   230  	return &base.Label{
   231  		Name:        label.Name,
   232  		Color:       label.Color,
   233  		Description: label.Description,
   234  	}
   235  }
   236  
   237  // GetLabels returns labels
   238  func (g *GiteaDownloader) GetLabels() ([]*base.Label, error) {
   239  	labels := make([]*base.Label, 0, g.maxPerPage)
   240  
   241  	for i := 1; ; i++ {
   242  		// make sure gitea can shutdown gracefully
   243  		select {
   244  		case <-g.ctx.Done():
   245  			return nil, nil
   246  		default:
   247  		}
   248  
   249  		ls, _, err := g.client.ListRepoLabels(g.repoOwner, g.repoName, gitea_sdk.ListLabelsOptions{ListOptions: gitea_sdk.ListOptions{
   250  			PageSize: g.maxPerPage,
   251  			Page:     i,
   252  		}})
   253  		if err != nil {
   254  			return nil, err
   255  		}
   256  
   257  		for i := range ls {
   258  			labels = append(labels, g.convertGiteaLabel(ls[i]))
   259  		}
   260  		if !g.pagination || len(ls) < g.maxPerPage {
   261  			break
   262  		}
   263  	}
   264  	return labels, nil
   265  }
   266  
   267  func (g *GiteaDownloader) convertGiteaRelease(rel *gitea_sdk.Release) *base.Release {
   268  	r := &base.Release{
   269  		TagName:         rel.TagName,
   270  		TargetCommitish: rel.Target,
   271  		Name:            rel.Title,
   272  		Body:            rel.Note,
   273  		Draft:           rel.IsDraft,
   274  		Prerelease:      rel.IsPrerelease,
   275  		PublisherID:     rel.Publisher.ID,
   276  		PublisherName:   rel.Publisher.UserName,
   277  		PublisherEmail:  rel.Publisher.Email,
   278  		Published:       rel.PublishedAt,
   279  		Created:         rel.CreatedAt,
   280  	}
   281  
   282  	httpClient := NewMigrationHTTPClient()
   283  
   284  	for _, asset := range rel.Attachments {
   285  		assetID := asset.ID // Don't optimize this, for closure we need a local variable
   286  		assetDownloadURL := asset.DownloadURL
   287  		size := int(asset.Size)
   288  		dlCount := int(asset.DownloadCount)
   289  		r.Assets = append(r.Assets, &base.ReleaseAsset{
   290  			ID:            asset.ID,
   291  			Name:          asset.Name,
   292  			Size:          &size,
   293  			DownloadCount: &dlCount,
   294  			Created:       asset.Created,
   295  			DownloadURL:   &asset.DownloadURL,
   296  			DownloadFunc: func() (io.ReadCloser, error) {
   297  				asset, _, err := g.client.GetReleaseAttachment(g.repoOwner, g.repoName, rel.ID, assetID)
   298  				if err != nil {
   299  					return nil, err
   300  				}
   301  
   302  				if !hasBaseURL(assetDownloadURL, g.baseURL) {
   303  					WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", assetID, g, assetDownloadURL)
   304  					return io.NopCloser(strings.NewReader(asset.DownloadURL)), nil
   305  				}
   306  
   307  				// FIXME: for a private download?
   308  				req, err := http.NewRequest("GET", assetDownloadURL, nil)
   309  				if err != nil {
   310  					return nil, err
   311  				}
   312  				resp, err := httpClient.Do(req)
   313  				if err != nil {
   314  					return nil, err
   315  				}
   316  
   317  				// resp.Body is closed by the uploader
   318  				return resp.Body, nil
   319  			},
   320  		})
   321  	}
   322  	return r
   323  }
   324  
   325  // GetReleases returns releases
   326  func (g *GiteaDownloader) GetReleases() ([]*base.Release, error) {
   327  	releases := make([]*base.Release, 0, g.maxPerPage)
   328  
   329  	for i := 1; ; i++ {
   330  		// make sure gitea can shutdown gracefully
   331  		select {
   332  		case <-g.ctx.Done():
   333  			return nil, nil
   334  		default:
   335  		}
   336  
   337  		rl, _, err := g.client.ListReleases(g.repoOwner, g.repoName, gitea_sdk.ListReleasesOptions{ListOptions: gitea_sdk.ListOptions{
   338  			PageSize: g.maxPerPage,
   339  			Page:     i,
   340  		}})
   341  		if err != nil {
   342  			return nil, err
   343  		}
   344  
   345  		for i := range rl {
   346  			releases = append(releases, g.convertGiteaRelease(rl[i]))
   347  		}
   348  		if !g.pagination || len(rl) < g.maxPerPage {
   349  			break
   350  		}
   351  	}
   352  	return releases, nil
   353  }
   354  
   355  func (g *GiteaDownloader) getIssueReactions(index int64) ([]*base.Reaction, error) {
   356  	var reactions []*base.Reaction
   357  	if err := g.client.CheckServerVersionConstraint(">=1.11"); err != nil {
   358  		log.Info("GiteaDownloader: instance to old, skip getIssueReactions")
   359  		return reactions, nil
   360  	}
   361  	rl, _, err := g.client.GetIssueReactions(g.repoOwner, g.repoName, index)
   362  	if err != nil {
   363  		return nil, err
   364  	}
   365  
   366  	for _, reaction := range rl {
   367  		reactions = append(reactions, &base.Reaction{
   368  			UserID:   reaction.User.ID,
   369  			UserName: reaction.User.UserName,
   370  			Content:  reaction.Reaction,
   371  		})
   372  	}
   373  	return reactions, nil
   374  }
   375  
   376  func (g *GiteaDownloader) getCommentReactions(commentID int64) ([]*base.Reaction, error) {
   377  	var reactions []*base.Reaction
   378  	if err := g.client.CheckServerVersionConstraint(">=1.11"); err != nil {
   379  		log.Info("GiteaDownloader: instance to old, skip getCommentReactions")
   380  		return reactions, nil
   381  	}
   382  	rl, _, err := g.client.GetIssueCommentReactions(g.repoOwner, g.repoName, commentID)
   383  	if err != nil {
   384  		return nil, err
   385  	}
   386  
   387  	for i := range rl {
   388  		reactions = append(reactions, &base.Reaction{
   389  			UserID:   rl[i].User.ID,
   390  			UserName: rl[i].User.UserName,
   391  			Content:  rl[i].Reaction,
   392  		})
   393  	}
   394  	return reactions, nil
   395  }
   396  
   397  // GetIssues returns issues according start and limit
   398  func (g *GiteaDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) {
   399  	if perPage > g.maxPerPage {
   400  		perPage = g.maxPerPage
   401  	}
   402  	allIssues := make([]*base.Issue, 0, perPage)
   403  
   404  	issues, _, err := g.client.ListRepoIssues(g.repoOwner, g.repoName, gitea_sdk.ListIssueOption{
   405  		ListOptions: gitea_sdk.ListOptions{Page: page, PageSize: perPage},
   406  		State:       gitea_sdk.StateAll,
   407  		Type:        gitea_sdk.IssueTypeIssue,
   408  	})
   409  	if err != nil {
   410  		return nil, false, fmt.Errorf("error while listing issues: %w", err)
   411  	}
   412  	for _, issue := range issues {
   413  
   414  		labels := make([]*base.Label, 0, len(issue.Labels))
   415  		for i := range issue.Labels {
   416  			labels = append(labels, g.convertGiteaLabel(issue.Labels[i]))
   417  		}
   418  
   419  		var milestone string
   420  		if issue.Milestone != nil {
   421  			milestone = issue.Milestone.Title
   422  		}
   423  
   424  		reactions, err := g.getIssueReactions(issue.Index)
   425  		if err != nil {
   426  			WarnAndNotice("Unable to load reactions during migrating issue #%d in %s. Error: %v", issue.Index, g, err)
   427  		}
   428  
   429  		var assignees []string
   430  		for i := range issue.Assignees {
   431  			assignees = append(assignees, issue.Assignees[i].UserName)
   432  		}
   433  
   434  		allIssues = append(allIssues, &base.Issue{
   435  			Title:        issue.Title,
   436  			Number:       issue.Index,
   437  			PosterID:     issue.Poster.ID,
   438  			PosterName:   issue.Poster.UserName,
   439  			PosterEmail:  issue.Poster.Email,
   440  			Content:      issue.Body,
   441  			Milestone:    milestone,
   442  			State:        string(issue.State),
   443  			Created:      issue.Created,
   444  			Updated:      issue.Updated,
   445  			Closed:       issue.Closed,
   446  			Reactions:    reactions,
   447  			Labels:       labels,
   448  			Assignees:    assignees,
   449  			IsLocked:     issue.IsLocked,
   450  			ForeignIndex: issue.Index,
   451  		})
   452  	}
   453  
   454  	isEnd := len(issues) < perPage
   455  	if !g.pagination {
   456  		isEnd = len(issues) == 0
   457  	}
   458  	return allIssues, isEnd, nil
   459  }
   460  
   461  // GetComments returns comments according issueNumber
   462  func (g *GiteaDownloader) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) {
   463  	allComments := make([]*base.Comment, 0, g.maxPerPage)
   464  
   465  	for i := 1; ; i++ {
   466  		// make sure gitea can shutdown gracefully
   467  		select {
   468  		case <-g.ctx.Done():
   469  			return nil, false, nil
   470  		default:
   471  		}
   472  
   473  		comments, _, err := g.client.ListIssueComments(g.repoOwner, g.repoName, commentable.GetForeignIndex(), gitea_sdk.ListIssueCommentOptions{ListOptions: gitea_sdk.ListOptions{
   474  			PageSize: g.maxPerPage,
   475  			Page:     i,
   476  		}})
   477  		if err != nil {
   478  			return nil, false, fmt.Errorf("error while listing comments for issue #%d. Error: %w", commentable.GetForeignIndex(), err)
   479  		}
   480  
   481  		for _, comment := range comments {
   482  			reactions, err := g.getCommentReactions(comment.ID)
   483  			if err != nil {
   484  				WarnAndNotice("Unable to load comment reactions during migrating issue #%d for comment %d in %s. Error: %v", commentable.GetForeignIndex(), comment.ID, g, err)
   485  			}
   486  
   487  			allComments = append(allComments, &base.Comment{
   488  				IssueIndex:  commentable.GetLocalIndex(),
   489  				Index:       comment.ID,
   490  				PosterID:    comment.Poster.ID,
   491  				PosterName:  comment.Poster.UserName,
   492  				PosterEmail: comment.Poster.Email,
   493  				Content:     comment.Body,
   494  				Created:     comment.Created,
   495  				Updated:     comment.Updated,
   496  				Reactions:   reactions,
   497  			})
   498  		}
   499  
   500  		if !g.pagination || len(comments) < g.maxPerPage {
   501  			break
   502  		}
   503  	}
   504  	return allComments, true, nil
   505  }
   506  
   507  // GetPullRequests returns pull requests according page and perPage
   508  func (g *GiteaDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) {
   509  	if perPage > g.maxPerPage {
   510  		perPage = g.maxPerPage
   511  	}
   512  	allPRs := make([]*base.PullRequest, 0, perPage)
   513  
   514  	prs, _, err := g.client.ListRepoPullRequests(g.repoOwner, g.repoName, gitea_sdk.ListPullRequestsOptions{
   515  		ListOptions: gitea_sdk.ListOptions{
   516  			Page:     page,
   517  			PageSize: perPage,
   518  		},
   519  		State: gitea_sdk.StateAll,
   520  	})
   521  	if err != nil {
   522  		return nil, false, fmt.Errorf("error while listing pull requests (page: %d, pagesize: %d). Error: %w", page, perPage, err)
   523  	}
   524  	for _, pr := range prs {
   525  		var milestone string
   526  		if pr.Milestone != nil {
   527  			milestone = pr.Milestone.Title
   528  		}
   529  
   530  		labels := make([]*base.Label, 0, len(pr.Labels))
   531  		for i := range pr.Labels {
   532  			labels = append(labels, g.convertGiteaLabel(pr.Labels[i]))
   533  		}
   534  
   535  		var (
   536  			headUserName string
   537  			headRepoName string
   538  			headCloneURL string
   539  			headRef      string
   540  			headSHA      string
   541  		)
   542  		if pr.Head != nil {
   543  			if pr.Head.Repository != nil {
   544  				headUserName = pr.Head.Repository.Owner.UserName
   545  				headRepoName = pr.Head.Repository.Name
   546  				headCloneURL = pr.Head.Repository.CloneURL
   547  			}
   548  			headSHA = pr.Head.Sha
   549  			headRef = pr.Head.Ref
   550  		}
   551  
   552  		var mergeCommitSHA string
   553  		if pr.MergedCommitID != nil {
   554  			mergeCommitSHA = *pr.MergedCommitID
   555  		}
   556  
   557  		reactions, err := g.getIssueReactions(pr.Index)
   558  		if err != nil {
   559  			WarnAndNotice("Unable to load reactions during migrating pull #%d in %s. Error: %v", pr.Index, g, err)
   560  		}
   561  
   562  		var assignees []string
   563  		for i := range pr.Assignees {
   564  			assignees = append(assignees, pr.Assignees[i].UserName)
   565  		}
   566  
   567  		createdAt := time.Time{}
   568  		if pr.Created != nil {
   569  			createdAt = *pr.Created
   570  		}
   571  		updatedAt := time.Time{}
   572  		if pr.Created != nil {
   573  			updatedAt = *pr.Updated
   574  		}
   575  
   576  		closedAt := pr.Closed
   577  		if pr.Merged != nil && closedAt == nil {
   578  			closedAt = pr.Merged
   579  		}
   580  
   581  		allPRs = append(allPRs, &base.PullRequest{
   582  			Title:          pr.Title,
   583  			Number:         pr.Index,
   584  			PosterID:       pr.Poster.ID,
   585  			PosterName:     pr.Poster.UserName,
   586  			PosterEmail:    pr.Poster.Email,
   587  			Content:        pr.Body,
   588  			State:          string(pr.State),
   589  			Created:        createdAt,
   590  			Updated:        updatedAt,
   591  			Closed:         closedAt,
   592  			Labels:         labels,
   593  			Milestone:      milestone,
   594  			Reactions:      reactions,
   595  			Assignees:      assignees,
   596  			Merged:         pr.HasMerged,
   597  			MergedTime:     pr.Merged,
   598  			MergeCommitSHA: mergeCommitSHA,
   599  			IsLocked:       pr.IsLocked,
   600  			PatchURL:       pr.PatchURL,
   601  			Head: base.PullRequestBranch{
   602  				Ref:       headRef,
   603  				SHA:       headSHA,
   604  				RepoName:  headRepoName,
   605  				OwnerName: headUserName,
   606  				CloneURL:  headCloneURL,
   607  			},
   608  			Base: base.PullRequestBranch{
   609  				Ref:       pr.Base.Ref,
   610  				SHA:       pr.Base.Sha,
   611  				RepoName:  g.repoName,
   612  				OwnerName: g.repoOwner,
   613  			},
   614  			ForeignIndex: pr.Index,
   615  		})
   616  		// SECURITY: Ensure that the PR is safe
   617  		_ = CheckAndEnsureSafePR(allPRs[len(allPRs)-1], g.baseURL, g)
   618  	}
   619  
   620  	isEnd := len(prs) < perPage
   621  	if !g.pagination {
   622  		isEnd = len(prs) == 0
   623  	}
   624  	return allPRs, isEnd, nil
   625  }
   626  
   627  // GetReviews returns pull requests review
   628  func (g *GiteaDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Review, error) {
   629  	if err := g.client.CheckServerVersionConstraint(">=1.12"); err != nil {
   630  		log.Info("GiteaDownloader: instance to old, skip GetReviews")
   631  		return nil, nil
   632  	}
   633  
   634  	allReviews := make([]*base.Review, 0, g.maxPerPage)
   635  
   636  	for i := 1; ; i++ {
   637  		// make sure gitea can shutdown gracefully
   638  		select {
   639  		case <-g.ctx.Done():
   640  			return nil, nil
   641  		default:
   642  		}
   643  
   644  		prl, _, err := g.client.ListPullReviews(g.repoOwner, g.repoName, reviewable.GetForeignIndex(), gitea_sdk.ListPullReviewsOptions{ListOptions: gitea_sdk.ListOptions{
   645  			Page:     i,
   646  			PageSize: g.maxPerPage,
   647  		}})
   648  		if err != nil {
   649  			return nil, err
   650  		}
   651  
   652  		for _, pr := range prl {
   653  			if pr.Reviewer == nil {
   654  				// Presumably this is a team review which we cannot migrate at present but we have to skip this review as otherwise the review will be mapped on to an incorrect user.
   655  				// TODO: handle team reviews
   656  				continue
   657  			}
   658  
   659  			rcl, _, err := g.client.ListPullReviewComments(g.repoOwner, g.repoName, reviewable.GetForeignIndex(), pr.ID)
   660  			if err != nil {
   661  				return nil, err
   662  			}
   663  			var reviewComments []*base.ReviewComment
   664  			for i := range rcl {
   665  				line := int(rcl[i].LineNum)
   666  				if rcl[i].OldLineNum > 0 {
   667  					line = int(rcl[i].OldLineNum) * -1
   668  				}
   669  
   670  				reviewComments = append(reviewComments, &base.ReviewComment{
   671  					ID:        rcl[i].ID,
   672  					Content:   rcl[i].Body,
   673  					TreePath:  rcl[i].Path,
   674  					DiffHunk:  rcl[i].DiffHunk,
   675  					Line:      line,
   676  					CommitID:  rcl[i].CommitID,
   677  					PosterID:  rcl[i].Reviewer.ID,
   678  					CreatedAt: rcl[i].Created,
   679  					UpdatedAt: rcl[i].Updated,
   680  				})
   681  			}
   682  
   683  			review := &base.Review{
   684  				ID:           pr.ID,
   685  				IssueIndex:   reviewable.GetLocalIndex(),
   686  				ReviewerID:   pr.Reviewer.ID,
   687  				ReviewerName: pr.Reviewer.UserName,
   688  				Official:     pr.Official,
   689  				CommitID:     pr.CommitID,
   690  				Content:      pr.Body,
   691  				CreatedAt:    pr.Submitted,
   692  				State:        string(pr.State),
   693  				Comments:     reviewComments,
   694  			}
   695  
   696  			allReviews = append(allReviews, review)
   697  		}
   698  
   699  		if len(prl) < g.maxPerPage {
   700  			break
   701  		}
   702  	}
   703  	return allReviews, nil
   704  }