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

     1  // Copyright 2019 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  	"path"
    14  	"strings"
    15  	"time"
    16  
    17  	"code.gitea.io/gitea/modules/container"
    18  	"code.gitea.io/gitea/modules/log"
    19  	base "code.gitea.io/gitea/modules/migration"
    20  	"code.gitea.io/gitea/modules/structs"
    21  
    22  	"github.com/xanzy/go-gitlab"
    23  )
    24  
    25  var (
    26  	_ base.Downloader        = &GitlabDownloader{}
    27  	_ base.DownloaderFactory = &GitlabDownloaderFactory{}
    28  )
    29  
    30  func init() {
    31  	RegisterDownloaderFactory(&GitlabDownloaderFactory{})
    32  }
    33  
    34  // GitlabDownloaderFactory defines a gitlab downloader factory
    35  type GitlabDownloaderFactory struct{}
    36  
    37  // New returns a Downloader related to this factory according MigrateOptions
    38  func (f *GitlabDownloaderFactory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) {
    39  	u, err := url.Parse(opts.CloneAddr)
    40  	if err != nil {
    41  		return nil, err
    42  	}
    43  
    44  	baseURL := u.Scheme + "://" + u.Host
    45  	repoNameSpace := strings.TrimPrefix(u.Path, "/")
    46  	repoNameSpace = strings.TrimSuffix(repoNameSpace, ".git")
    47  
    48  	log.Trace("Create gitlab downloader. BaseURL: %s RepoName: %s", baseURL, repoNameSpace)
    49  
    50  	return NewGitlabDownloader(ctx, baseURL, repoNameSpace, opts.AuthUsername, opts.AuthPassword, opts.AuthToken)
    51  }
    52  
    53  // GitServiceType returns the type of git service
    54  func (f *GitlabDownloaderFactory) GitServiceType() structs.GitServiceType {
    55  	return structs.GitlabService
    56  }
    57  
    58  type gitlabIIDResolver struct {
    59  	maxIssueIID int64
    60  	frozen      bool
    61  }
    62  
    63  func (r *gitlabIIDResolver) recordIssueIID(issueIID int) {
    64  	if r.frozen {
    65  		panic("cannot record issue IID after pull request IID generation has started")
    66  	}
    67  	r.maxIssueIID = max(r.maxIssueIID, int64(issueIID))
    68  }
    69  
    70  func (r *gitlabIIDResolver) generatePullRequestNumber(mrIID int) int64 {
    71  	r.frozen = true
    72  	return r.maxIssueIID + int64(mrIID)
    73  }
    74  
    75  // GitlabDownloader implements a Downloader interface to get repository information
    76  // from gitlab via go-gitlab
    77  // - issueCount is incremented in GetIssues() to ensure PR and Issue numbers do not overlap,
    78  // because Gitlab has individual Issue and Pull Request numbers.
    79  type GitlabDownloader struct {
    80  	base.NullDownloader
    81  	ctx         context.Context
    82  	client      *gitlab.Client
    83  	baseURL     string
    84  	repoID      int
    85  	repoName    string
    86  	iidResolver gitlabIIDResolver
    87  	maxPerPage  int
    88  }
    89  
    90  // NewGitlabDownloader creates a gitlab Downloader via gitlab API
    91  //
    92  //	Use either a username/password, personal token entered into the username field, or anonymous/public access
    93  //	Note: Public access only allows very basic access
    94  func NewGitlabDownloader(ctx context.Context, baseURL, repoPath, username, password, token string) (*GitlabDownloader, error) {
    95  	gitlabClient, err := gitlab.NewClient(token, gitlab.WithBaseURL(baseURL), gitlab.WithHTTPClient(NewMigrationHTTPClient()))
    96  	// Only use basic auth if token is blank and password is NOT
    97  	// Basic auth will fail with empty strings, but empty token will allow anonymous public API usage
    98  	if token == "" && password != "" {
    99  		gitlabClient, err = gitlab.NewBasicAuthClient(username, password, gitlab.WithBaseURL(baseURL), gitlab.WithHTTPClient(NewMigrationHTTPClient()))
   100  	}
   101  
   102  	if err != nil {
   103  		log.Trace("Error logging into gitlab: %v", err)
   104  		return nil, err
   105  	}
   106  
   107  	// split namespace and subdirectory
   108  	pathParts := strings.Split(strings.Trim(repoPath, "/"), "/")
   109  	var resp *gitlab.Response
   110  	u, _ := url.Parse(baseURL)
   111  	for len(pathParts) >= 2 {
   112  		_, resp, err = gitlabClient.Version.GetVersion()
   113  		if err == nil || resp != nil && resp.StatusCode == http.StatusUnauthorized {
   114  			err = nil // if no authentication given, this still should work
   115  			break
   116  		}
   117  
   118  		u.Path = path.Join(u.Path, pathParts[0])
   119  		baseURL = u.String()
   120  		pathParts = pathParts[1:]
   121  		_ = gitlab.WithBaseURL(baseURL)(gitlabClient)
   122  		repoPath = strings.Join(pathParts, "/")
   123  	}
   124  	if err != nil {
   125  		log.Trace("Error could not get gitlab version: %v", err)
   126  		return nil, err
   127  	}
   128  
   129  	log.Trace("gitlab downloader: use BaseURL: '%s' and RepoPath: '%s'", baseURL, repoPath)
   130  
   131  	// Grab and store project/repo ID here, due to issues using the URL escaped path
   132  	gr, _, err := gitlabClient.Projects.GetProject(repoPath, nil, nil, gitlab.WithContext(ctx))
   133  	if err != nil {
   134  		log.Trace("Error retrieving project: %v", err)
   135  		return nil, err
   136  	}
   137  
   138  	if gr == nil {
   139  		log.Trace("Error getting project, project is nil")
   140  		return nil, errors.New("Error getting project, project is nil")
   141  	}
   142  
   143  	return &GitlabDownloader{
   144  		ctx:        ctx,
   145  		client:     gitlabClient,
   146  		baseURL:    baseURL,
   147  		repoID:     gr.ID,
   148  		repoName:   gr.Name,
   149  		maxPerPage: 100,
   150  	}, nil
   151  }
   152  
   153  // String implements Stringer
   154  func (g *GitlabDownloader) String() string {
   155  	return fmt.Sprintf("migration from gitlab server %s [%d]/%s", g.baseURL, g.repoID, g.repoName)
   156  }
   157  
   158  func (g *GitlabDownloader) LogString() string {
   159  	if g == nil {
   160  		return "<GitlabDownloader nil>"
   161  	}
   162  	return fmt.Sprintf("<GitlabDownloader %s [%d]/%s>", g.baseURL, g.repoID, g.repoName)
   163  }
   164  
   165  // SetContext set context
   166  func (g *GitlabDownloader) SetContext(ctx context.Context) {
   167  	g.ctx = ctx
   168  }
   169  
   170  // GetRepoInfo returns a repository information
   171  func (g *GitlabDownloader) GetRepoInfo() (*base.Repository, error) {
   172  	gr, _, err := g.client.Projects.GetProject(g.repoID, nil, nil, gitlab.WithContext(g.ctx))
   173  	if err != nil {
   174  		return nil, err
   175  	}
   176  
   177  	var private bool
   178  	switch gr.Visibility {
   179  	case gitlab.InternalVisibility:
   180  		private = true
   181  	case gitlab.PrivateVisibility:
   182  		private = true
   183  	}
   184  
   185  	var owner string
   186  	if gr.Owner == nil {
   187  		log.Trace("gr.Owner is nil, trying to get owner from Namespace")
   188  		if gr.Namespace != nil && gr.Namespace.Kind == "user" {
   189  			owner = gr.Namespace.Path
   190  		}
   191  	} else {
   192  		owner = gr.Owner.Username
   193  	}
   194  
   195  	// convert gitlab repo to stand Repo
   196  	return &base.Repository{
   197  		Owner:         owner,
   198  		Name:          gr.Name,
   199  		IsPrivate:     private,
   200  		Description:   gr.Description,
   201  		OriginalURL:   gr.WebURL,
   202  		CloneURL:      gr.HTTPURLToRepo,
   203  		DefaultBranch: gr.DefaultBranch,
   204  	}, nil
   205  }
   206  
   207  // GetTopics return gitlab topics
   208  func (g *GitlabDownloader) GetTopics() ([]string, error) {
   209  	gr, _, err := g.client.Projects.GetProject(g.repoID, nil, nil, gitlab.WithContext(g.ctx))
   210  	if err != nil {
   211  		return nil, err
   212  	}
   213  	return gr.TagList, err
   214  }
   215  
   216  // GetMilestones returns milestones
   217  func (g *GitlabDownloader) GetMilestones() ([]*base.Milestone, error) {
   218  	perPage := g.maxPerPage
   219  	state := "all"
   220  	milestones := make([]*base.Milestone, 0, perPage)
   221  	for i := 1; ; i++ {
   222  		ms, _, err := g.client.Milestones.ListMilestones(g.repoID, &gitlab.ListMilestonesOptions{
   223  			State: &state,
   224  			ListOptions: gitlab.ListOptions{
   225  				Page:    i,
   226  				PerPage: perPage,
   227  			},
   228  		}, nil, gitlab.WithContext(g.ctx))
   229  		if err != nil {
   230  			return nil, err
   231  		}
   232  
   233  		for _, m := range ms {
   234  			var desc string
   235  			if m.Description != "" {
   236  				desc = m.Description
   237  			}
   238  			state := "open"
   239  			var closedAt *time.Time
   240  			if m.State != "" {
   241  				state = m.State
   242  				if state == "closed" {
   243  					closedAt = m.UpdatedAt
   244  				}
   245  			}
   246  
   247  			var deadline *time.Time
   248  			if m.DueDate != nil {
   249  				deadlineParsed, err := time.Parse("2006-01-02", m.DueDate.String())
   250  				if err != nil {
   251  					log.Trace("Error parsing Milestone DueDate time")
   252  					deadline = nil
   253  				} else {
   254  					deadline = &deadlineParsed
   255  				}
   256  			}
   257  
   258  			milestones = append(milestones, &base.Milestone{
   259  				Title:       m.Title,
   260  				Description: desc,
   261  				Deadline:    deadline,
   262  				State:       state,
   263  				Created:     *m.CreatedAt,
   264  				Updated:     m.UpdatedAt,
   265  				Closed:      closedAt,
   266  			})
   267  		}
   268  		if len(ms) < perPage {
   269  			break
   270  		}
   271  	}
   272  	return milestones, nil
   273  }
   274  
   275  func (g *GitlabDownloader) normalizeColor(val string) string {
   276  	val = strings.TrimLeft(val, "#")
   277  	val = strings.ToLower(val)
   278  	if len(val) == 3 {
   279  		c := []rune(val)
   280  		val = fmt.Sprintf("%c%c%c%c%c%c", c[0], c[0], c[1], c[1], c[2], c[2])
   281  	}
   282  	if len(val) != 6 {
   283  		return ""
   284  	}
   285  	return val
   286  }
   287  
   288  // GetLabels returns labels
   289  func (g *GitlabDownloader) GetLabels() ([]*base.Label, error) {
   290  	perPage := g.maxPerPage
   291  	labels := make([]*base.Label, 0, perPage)
   292  	for i := 1; ; i++ {
   293  		ls, _, err := g.client.Labels.ListLabels(g.repoID, &gitlab.ListLabelsOptions{ListOptions: gitlab.ListOptions{
   294  			Page:    i,
   295  			PerPage: perPage,
   296  		}}, nil, gitlab.WithContext(g.ctx))
   297  		if err != nil {
   298  			return nil, err
   299  		}
   300  		for _, label := range ls {
   301  			baseLabel := &base.Label{
   302  				Name:        label.Name,
   303  				Color:       g.normalizeColor(label.Color),
   304  				Description: label.Description,
   305  			}
   306  			labels = append(labels, baseLabel)
   307  		}
   308  		if len(ls) < perPage {
   309  			break
   310  		}
   311  	}
   312  	return labels, nil
   313  }
   314  
   315  func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Release {
   316  	var zero int
   317  	r := &base.Release{
   318  		TagName:         rel.TagName,
   319  		TargetCommitish: rel.Commit.ID,
   320  		Name:            rel.Name,
   321  		Body:            rel.Description,
   322  		Created:         *rel.CreatedAt,
   323  		PublisherID:     int64(rel.Author.ID),
   324  		PublisherName:   rel.Author.Username,
   325  	}
   326  
   327  	httpClient := NewMigrationHTTPClient()
   328  
   329  	for k, asset := range rel.Assets.Links {
   330  		assetID := asset.ID // Don't optimize this, for closure we need a local variable
   331  		r.Assets = append(r.Assets, &base.ReleaseAsset{
   332  			ID:            int64(asset.ID),
   333  			Name:          asset.Name,
   334  			ContentType:   &rel.Assets.Sources[k].Format,
   335  			Size:          &zero,
   336  			DownloadCount: &zero,
   337  			DownloadFunc: func() (io.ReadCloser, error) {
   338  				link, _, err := g.client.ReleaseLinks.GetReleaseLink(g.repoID, rel.TagName, assetID, gitlab.WithContext(g.ctx))
   339  				if err != nil {
   340  					return nil, err
   341  				}
   342  
   343  				if !hasBaseURL(link.URL, g.baseURL) {
   344  					WarnAndNotice("Unexpected AssetURL for assetID[%d] in %s: %s", assetID, g, link.URL)
   345  					return io.NopCloser(strings.NewReader(link.URL)), nil
   346  				}
   347  
   348  				req, err := http.NewRequest("GET", link.URL, nil)
   349  				if err != nil {
   350  					return nil, err
   351  				}
   352  				req = req.WithContext(g.ctx)
   353  				resp, err := httpClient.Do(req)
   354  				if err != nil {
   355  					return nil, err
   356  				}
   357  
   358  				// resp.Body is closed by the uploader
   359  				return resp.Body, nil
   360  			},
   361  		})
   362  	}
   363  	return r
   364  }
   365  
   366  // GetReleases returns releases
   367  func (g *GitlabDownloader) GetReleases() ([]*base.Release, error) {
   368  	perPage := g.maxPerPage
   369  	releases := make([]*base.Release, 0, perPage)
   370  	for i := 1; ; i++ {
   371  		ls, _, err := g.client.Releases.ListReleases(g.repoID, &gitlab.ListReleasesOptions{
   372  			ListOptions: gitlab.ListOptions{
   373  				Page:    i,
   374  				PerPage: perPage,
   375  			},
   376  		}, nil, gitlab.WithContext(g.ctx))
   377  		if err != nil {
   378  			return nil, err
   379  		}
   380  
   381  		for _, release := range ls {
   382  			releases = append(releases, g.convertGitlabRelease(release))
   383  		}
   384  		if len(ls) < perPage {
   385  			break
   386  		}
   387  	}
   388  	return releases, nil
   389  }
   390  
   391  type gitlabIssueContext struct {
   392  	IsMergeRequest bool
   393  }
   394  
   395  // GetIssues returns issues according start and limit
   396  //
   397  //	Note: issue label description and colors are not supported by the go-gitlab library at this time
   398  func (g *GitlabDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) {
   399  	state := "all"
   400  	sort := "asc"
   401  
   402  	if perPage > g.maxPerPage {
   403  		perPage = g.maxPerPage
   404  	}
   405  
   406  	opt := &gitlab.ListProjectIssuesOptions{
   407  		State: &state,
   408  		Sort:  &sort,
   409  		ListOptions: gitlab.ListOptions{
   410  			PerPage: perPage,
   411  			Page:    page,
   412  		},
   413  	}
   414  
   415  	allIssues := make([]*base.Issue, 0, perPage)
   416  
   417  	issues, _, err := g.client.Issues.ListProjectIssues(g.repoID, opt, nil, gitlab.WithContext(g.ctx))
   418  	if err != nil {
   419  		return nil, false, fmt.Errorf("error while listing issues: %w", err)
   420  	}
   421  	for _, issue := range issues {
   422  
   423  		labels := make([]*base.Label, 0, len(issue.Labels))
   424  		for _, l := range issue.Labels {
   425  			labels = append(labels, &base.Label{
   426  				Name: l,
   427  			})
   428  		}
   429  
   430  		var milestone string
   431  		if issue.Milestone != nil {
   432  			milestone = issue.Milestone.Title
   433  		}
   434  
   435  		var reactions []*gitlab.AwardEmoji
   436  		awardPage := 1
   437  		for {
   438  			awards, _, err := g.client.AwardEmoji.ListIssueAwardEmoji(g.repoID, issue.IID, &gitlab.ListAwardEmojiOptions{Page: awardPage, PerPage: perPage}, gitlab.WithContext(g.ctx))
   439  			if err != nil {
   440  				return nil, false, fmt.Errorf("error while listing issue awards: %w", err)
   441  			}
   442  
   443  			reactions = append(reactions, awards...)
   444  
   445  			if len(awards) < perPage {
   446  				break
   447  			}
   448  
   449  			awardPage++
   450  		}
   451  
   452  		allIssues = append(allIssues, &base.Issue{
   453  			Title:        issue.Title,
   454  			Number:       int64(issue.IID),
   455  			PosterID:     int64(issue.Author.ID),
   456  			PosterName:   issue.Author.Username,
   457  			Content:      issue.Description,
   458  			Milestone:    milestone,
   459  			State:        issue.State,
   460  			Created:      *issue.CreatedAt,
   461  			Labels:       labels,
   462  			Reactions:    g.awardsToReactions(reactions),
   463  			Closed:       issue.ClosedAt,
   464  			IsLocked:     issue.DiscussionLocked,
   465  			Updated:      *issue.UpdatedAt,
   466  			ForeignIndex: int64(issue.IID),
   467  			Context:      gitlabIssueContext{IsMergeRequest: false},
   468  		})
   469  
   470  		// record the issue IID, to be used in GetPullRequests()
   471  		g.iidResolver.recordIssueIID(issue.IID)
   472  	}
   473  
   474  	return allIssues, len(issues) < perPage, nil
   475  }
   476  
   477  // GetComments returns comments according issueNumber
   478  // TODO: figure out how to transfer comment reactions
   479  func (g *GitlabDownloader) GetComments(commentable base.Commentable) ([]*base.Comment, bool, error) {
   480  	context, ok := commentable.GetContext().(gitlabIssueContext)
   481  	if !ok {
   482  		return nil, false, fmt.Errorf("unexpected context: %+v", commentable.GetContext())
   483  	}
   484  
   485  	allComments := make([]*base.Comment, 0, g.maxPerPage)
   486  
   487  	page := 1
   488  
   489  	for {
   490  		var comments []*gitlab.Discussion
   491  		var resp *gitlab.Response
   492  		var err error
   493  		if !context.IsMergeRequest {
   494  			comments, resp, err = g.client.Discussions.ListIssueDiscussions(g.repoID, int(commentable.GetForeignIndex()), &gitlab.ListIssueDiscussionsOptions{
   495  				Page:    page,
   496  				PerPage: g.maxPerPage,
   497  			}, nil, gitlab.WithContext(g.ctx))
   498  		} else {
   499  			comments, resp, err = g.client.Discussions.ListMergeRequestDiscussions(g.repoID, int(commentable.GetForeignIndex()), &gitlab.ListMergeRequestDiscussionsOptions{
   500  				Page:    page,
   501  				PerPage: g.maxPerPage,
   502  			}, nil, gitlab.WithContext(g.ctx))
   503  		}
   504  
   505  		if err != nil {
   506  			return nil, false, fmt.Errorf("error while listing comments: %v %w", g.repoID, err)
   507  		}
   508  		for _, comment := range comments {
   509  			// Flatten comment threads
   510  			if !comment.IndividualNote {
   511  				for _, note := range comment.Notes {
   512  					allComments = append(allComments, &base.Comment{
   513  						IssueIndex:  commentable.GetLocalIndex(),
   514  						Index:       int64(note.ID),
   515  						PosterID:    int64(note.Author.ID),
   516  						PosterName:  note.Author.Username,
   517  						PosterEmail: note.Author.Email,
   518  						Content:     note.Body,
   519  						Created:     *note.CreatedAt,
   520  					})
   521  				}
   522  			} else {
   523  				c := comment.Notes[0]
   524  				allComments = append(allComments, &base.Comment{
   525  					IssueIndex:  commentable.GetLocalIndex(),
   526  					Index:       int64(c.ID),
   527  					PosterID:    int64(c.Author.ID),
   528  					PosterName:  c.Author.Username,
   529  					PosterEmail: c.Author.Email,
   530  					Content:     c.Body,
   531  					Created:     *c.CreatedAt,
   532  				})
   533  			}
   534  		}
   535  		if resp.NextPage == 0 {
   536  			break
   537  		}
   538  		page = resp.NextPage
   539  	}
   540  	return allComments, true, nil
   541  }
   542  
   543  // GetPullRequests returns pull requests according page and perPage
   544  func (g *GitlabDownloader) GetPullRequests(page, perPage int) ([]*base.PullRequest, bool, error) {
   545  	if perPage > g.maxPerPage {
   546  		perPage = g.maxPerPage
   547  	}
   548  
   549  	opt := &gitlab.ListProjectMergeRequestsOptions{
   550  		ListOptions: gitlab.ListOptions{
   551  			PerPage: perPage,
   552  			Page:    page,
   553  		},
   554  	}
   555  
   556  	allPRs := make([]*base.PullRequest, 0, perPage)
   557  
   558  	prs, _, err := g.client.MergeRequests.ListProjectMergeRequests(g.repoID, opt, nil, gitlab.WithContext(g.ctx))
   559  	if err != nil {
   560  		return nil, false, fmt.Errorf("error while listing merge requests: %w", err)
   561  	}
   562  	for _, pr := range prs {
   563  
   564  		labels := make([]*base.Label, 0, len(pr.Labels))
   565  		for _, l := range pr.Labels {
   566  			labels = append(labels, &base.Label{
   567  				Name: l,
   568  			})
   569  		}
   570  
   571  		var merged bool
   572  		if pr.State == "merged" {
   573  			merged = true
   574  			pr.State = "closed"
   575  		}
   576  
   577  		mergeTime := pr.MergedAt
   578  		if merged && pr.MergedAt == nil {
   579  			mergeTime = pr.UpdatedAt
   580  		}
   581  
   582  		closeTime := pr.ClosedAt
   583  		if merged && pr.ClosedAt == nil {
   584  			closeTime = pr.UpdatedAt
   585  		}
   586  
   587  		var locked bool
   588  		if pr.State == "locked" {
   589  			locked = true
   590  		}
   591  
   592  		var milestone string
   593  		if pr.Milestone != nil {
   594  			milestone = pr.Milestone.Title
   595  		}
   596  
   597  		var reactions []*gitlab.AwardEmoji
   598  		awardPage := 1
   599  		for {
   600  			awards, _, err := g.client.AwardEmoji.ListMergeRequestAwardEmoji(g.repoID, pr.IID, &gitlab.ListAwardEmojiOptions{Page: awardPage, PerPage: perPage}, gitlab.WithContext(g.ctx))
   601  			if err != nil {
   602  				return nil, false, fmt.Errorf("error while listing merge requests awards: %w", err)
   603  			}
   604  
   605  			reactions = append(reactions, awards...)
   606  
   607  			if len(awards) < perPage {
   608  				break
   609  			}
   610  
   611  			awardPage++
   612  		}
   613  
   614  		// Generate new PR Numbers by the known Issue Numbers, because they share the same number space in Gitea, but they are independent in Gitlab
   615  		newPRNumber := g.iidResolver.generatePullRequestNumber(pr.IID)
   616  
   617  		allPRs = append(allPRs, &base.PullRequest{
   618  			Title:          pr.Title,
   619  			Number:         newPRNumber,
   620  			PosterName:     pr.Author.Username,
   621  			PosterID:       int64(pr.Author.ID),
   622  			Content:        pr.Description,
   623  			Milestone:      milestone,
   624  			State:          pr.State,
   625  			Created:        *pr.CreatedAt,
   626  			Closed:         closeTime,
   627  			Labels:         labels,
   628  			Merged:         merged,
   629  			MergeCommitSHA: pr.MergeCommitSHA,
   630  			MergedTime:     mergeTime,
   631  			IsLocked:       locked,
   632  			Reactions:      g.awardsToReactions(reactions),
   633  			Head: base.PullRequestBranch{
   634  				Ref:       pr.SourceBranch,
   635  				SHA:       pr.SHA,
   636  				RepoName:  g.repoName,
   637  				OwnerName: pr.Author.Username,
   638  				CloneURL:  pr.WebURL,
   639  			},
   640  			Base: base.PullRequestBranch{
   641  				Ref:       pr.TargetBranch,
   642  				SHA:       pr.DiffRefs.BaseSha,
   643  				RepoName:  g.repoName,
   644  				OwnerName: pr.Author.Username,
   645  			},
   646  			PatchURL:     pr.WebURL + ".patch",
   647  			ForeignIndex: int64(pr.IID),
   648  			Context:      gitlabIssueContext{IsMergeRequest: true},
   649  		})
   650  
   651  		// SECURITY: Ensure that the PR is safe
   652  		_ = CheckAndEnsureSafePR(allPRs[len(allPRs)-1], g.baseURL, g)
   653  	}
   654  
   655  	return allPRs, len(prs) < perPage, nil
   656  }
   657  
   658  // GetReviews returns pull requests review
   659  func (g *GitlabDownloader) GetReviews(reviewable base.Reviewable) ([]*base.Review, error) {
   660  	approvals, resp, err := g.client.MergeRequestApprovals.GetConfiguration(g.repoID, int(reviewable.GetForeignIndex()), gitlab.WithContext(g.ctx))
   661  	if err != nil {
   662  		if resp != nil && resp.StatusCode == http.StatusNotFound {
   663  			log.Error(fmt.Sprintf("GitlabDownloader: while migrating a error occurred: '%s'", err.Error()))
   664  			return []*base.Review{}, nil
   665  		}
   666  		return nil, err
   667  	}
   668  
   669  	var createdAt time.Time
   670  	if approvals.CreatedAt != nil {
   671  		createdAt = *approvals.CreatedAt
   672  	} else if approvals.UpdatedAt != nil {
   673  		createdAt = *approvals.UpdatedAt
   674  	} else {
   675  		createdAt = time.Now()
   676  	}
   677  
   678  	reviews := make([]*base.Review, 0, len(approvals.ApprovedBy))
   679  	for _, user := range approvals.ApprovedBy {
   680  		reviews = append(reviews, &base.Review{
   681  			IssueIndex:   reviewable.GetLocalIndex(),
   682  			ReviewerID:   int64(user.User.ID),
   683  			ReviewerName: user.User.Username,
   684  			CreatedAt:    createdAt,
   685  			// All we get are approvals
   686  			State: base.ReviewStateApproved,
   687  		})
   688  	}
   689  
   690  	return reviews, nil
   691  }
   692  
   693  func (g *GitlabDownloader) awardsToReactions(awards []*gitlab.AwardEmoji) []*base.Reaction {
   694  	result := make([]*base.Reaction, 0, len(awards))
   695  	uniqCheck := make(container.Set[string])
   696  	for _, award := range awards {
   697  		uid := fmt.Sprintf("%s%d", award.Name, award.User.ID)
   698  		if uniqCheck.Add(uid) {
   699  			result = append(result, &base.Reaction{
   700  				UserID:   int64(award.User.ID),
   701  				UserName: award.User.Username,
   702  				Content:  award.Name,
   703  			})
   704  		}
   705  	}
   706  	return result
   707  }