code.gitea.io/gitea@v1.21.7/services/migrations/gitea_uploader.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  	"os"
    12  	"path/filepath"
    13  	"strconv"
    14  	"strings"
    15  	"time"
    16  
    17  	"code.gitea.io/gitea/models"
    18  	"code.gitea.io/gitea/models/db"
    19  	issues_model "code.gitea.io/gitea/models/issues"
    20  	repo_model "code.gitea.io/gitea/models/repo"
    21  	user_model "code.gitea.io/gitea/models/user"
    22  	base_module "code.gitea.io/gitea/modules/base"
    23  	"code.gitea.io/gitea/modules/git"
    24  	"code.gitea.io/gitea/modules/label"
    25  	"code.gitea.io/gitea/modules/log"
    26  	base "code.gitea.io/gitea/modules/migration"
    27  	repo_module "code.gitea.io/gitea/modules/repository"
    28  	"code.gitea.io/gitea/modules/setting"
    29  	"code.gitea.io/gitea/modules/storage"
    30  	"code.gitea.io/gitea/modules/structs"
    31  	"code.gitea.io/gitea/modules/timeutil"
    32  	"code.gitea.io/gitea/modules/uri"
    33  	"code.gitea.io/gitea/modules/util"
    34  	"code.gitea.io/gitea/services/pull"
    35  	repo_service "code.gitea.io/gitea/services/repository"
    36  
    37  	"github.com/google/uuid"
    38  )
    39  
    40  var _ base.Uploader = &GiteaLocalUploader{}
    41  
    42  // GiteaLocalUploader implements an Uploader to gitea sites
    43  type GiteaLocalUploader struct {
    44  	ctx            context.Context
    45  	doer           *user_model.User
    46  	repoOwner      string
    47  	repoName       string
    48  	repo           *repo_model.Repository
    49  	labels         map[string]*issues_model.Label
    50  	milestones     map[string]int64
    51  	issues         map[int64]*issues_model.Issue
    52  	gitRepo        *git.Repository
    53  	prHeadCache    map[string]string
    54  	sameApp        bool
    55  	userMap        map[int64]int64 // external user id mapping to user id
    56  	prCache        map[int64]*issues_model.PullRequest
    57  	gitServiceType structs.GitServiceType
    58  }
    59  
    60  // NewGiteaLocalUploader creates an gitea Uploader via gitea API v1
    61  func NewGiteaLocalUploader(ctx context.Context, doer *user_model.User, repoOwner, repoName string) *GiteaLocalUploader {
    62  	return &GiteaLocalUploader{
    63  		ctx:         ctx,
    64  		doer:        doer,
    65  		repoOwner:   repoOwner,
    66  		repoName:    repoName,
    67  		labels:      make(map[string]*issues_model.Label),
    68  		milestones:  make(map[string]int64),
    69  		issues:      make(map[int64]*issues_model.Issue),
    70  		prHeadCache: make(map[string]string),
    71  		userMap:     make(map[int64]int64),
    72  		prCache:     make(map[int64]*issues_model.PullRequest),
    73  	}
    74  }
    75  
    76  // MaxBatchInsertSize returns the table's max batch insert size
    77  func (g *GiteaLocalUploader) MaxBatchInsertSize(tp string) int {
    78  	switch tp {
    79  	case "issue":
    80  		return db.MaxBatchInsertSize(new(issues_model.Issue))
    81  	case "comment":
    82  		return db.MaxBatchInsertSize(new(issues_model.Comment))
    83  	case "milestone":
    84  		return db.MaxBatchInsertSize(new(issues_model.Milestone))
    85  	case "label":
    86  		return db.MaxBatchInsertSize(new(issues_model.Label))
    87  	case "release":
    88  		return db.MaxBatchInsertSize(new(repo_model.Release))
    89  	case "pullrequest":
    90  		return db.MaxBatchInsertSize(new(issues_model.PullRequest))
    91  	}
    92  	return 10
    93  }
    94  
    95  // CreateRepo creates a repository
    96  func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.MigrateOptions) error {
    97  	owner, err := user_model.GetUserByName(g.ctx, g.repoOwner)
    98  	if err != nil {
    99  		return err
   100  	}
   101  
   102  	var r *repo_model.Repository
   103  	if opts.MigrateToRepoID <= 0 {
   104  		r, err = repo_service.CreateRepositoryDirectly(g.ctx, g.doer, owner, repo_service.CreateRepoOptions{
   105  			Name:           g.repoName,
   106  			Description:    repo.Description,
   107  			OriginalURL:    repo.OriginalURL,
   108  			GitServiceType: opts.GitServiceType,
   109  			IsPrivate:      opts.Private,
   110  			IsMirror:       opts.Mirror,
   111  			Status:         repo_model.RepositoryBeingMigrated,
   112  		})
   113  	} else {
   114  		r, err = repo_model.GetRepositoryByID(g.ctx, opts.MigrateToRepoID)
   115  	}
   116  	if err != nil {
   117  		return err
   118  	}
   119  	r.DefaultBranch = repo.DefaultBranch
   120  	r.Description = repo.Description
   121  
   122  	r, err = repo_module.MigrateRepositoryGitData(g.ctx, owner, r, base.MigrateOptions{
   123  		RepoName:       g.repoName,
   124  		Description:    repo.Description,
   125  		OriginalURL:    repo.OriginalURL,
   126  		GitServiceType: opts.GitServiceType,
   127  		Mirror:         repo.IsMirror,
   128  		LFS:            opts.LFS,
   129  		LFSEndpoint:    opts.LFSEndpoint,
   130  		CloneAddr:      repo.CloneURL, // SECURITY: we will assume that this has already been checked
   131  		Private:        repo.IsPrivate,
   132  		Wiki:           opts.Wiki,
   133  		Releases:       opts.Releases, // if didn't get releases, then sync them from tags
   134  		MirrorInterval: opts.MirrorInterval,
   135  	}, NewMigrationHTTPTransport())
   136  
   137  	g.sameApp = strings.HasPrefix(repo.OriginalURL, setting.AppURL)
   138  	g.repo = r
   139  	if err != nil {
   140  		return err
   141  	}
   142  	g.gitRepo, err = git.OpenRepository(g.ctx, r.RepoPath())
   143  	return err
   144  }
   145  
   146  // Close closes this uploader
   147  func (g *GiteaLocalUploader) Close() {
   148  	if g.gitRepo != nil {
   149  		g.gitRepo.Close()
   150  	}
   151  }
   152  
   153  // CreateTopics creates topics
   154  func (g *GiteaLocalUploader) CreateTopics(topics ...string) error {
   155  	// Ignore topics too long for the db
   156  	c := 0
   157  	for _, topic := range topics {
   158  		if len(topic) > 50 {
   159  			continue
   160  		}
   161  
   162  		topics[c] = topic
   163  		c++
   164  	}
   165  	topics = topics[:c]
   166  	return repo_model.SaveTopics(g.ctx, g.repo.ID, topics...)
   167  }
   168  
   169  // CreateMilestones creates milestones
   170  func (g *GiteaLocalUploader) CreateMilestones(milestones ...*base.Milestone) error {
   171  	mss := make([]*issues_model.Milestone, 0, len(milestones))
   172  	for _, milestone := range milestones {
   173  		var deadline timeutil.TimeStamp
   174  		if milestone.Deadline != nil {
   175  			deadline = timeutil.TimeStamp(milestone.Deadline.Unix())
   176  		}
   177  		if deadline == 0 {
   178  			deadline = timeutil.TimeStamp(time.Date(9999, 1, 1, 0, 0, 0, 0, setting.DefaultUILocation).Unix())
   179  		}
   180  
   181  		if milestone.Created.IsZero() {
   182  			if milestone.Updated != nil {
   183  				milestone.Created = *milestone.Updated
   184  			} else if milestone.Deadline != nil {
   185  				milestone.Created = *milestone.Deadline
   186  			} else {
   187  				milestone.Created = time.Now()
   188  			}
   189  		}
   190  		if milestone.Updated == nil || milestone.Updated.IsZero() {
   191  			milestone.Updated = &milestone.Created
   192  		}
   193  
   194  		ms := issues_model.Milestone{
   195  			RepoID:       g.repo.ID,
   196  			Name:         milestone.Title,
   197  			Content:      milestone.Description,
   198  			IsClosed:     milestone.State == "closed",
   199  			CreatedUnix:  timeutil.TimeStamp(milestone.Created.Unix()),
   200  			UpdatedUnix:  timeutil.TimeStamp(milestone.Updated.Unix()),
   201  			DeadlineUnix: deadline,
   202  		}
   203  		if ms.IsClosed && milestone.Closed != nil {
   204  			ms.ClosedDateUnix = timeutil.TimeStamp(milestone.Closed.Unix())
   205  		}
   206  		mss = append(mss, &ms)
   207  	}
   208  
   209  	err := issues_model.InsertMilestones(g.ctx, mss...)
   210  	if err != nil {
   211  		return err
   212  	}
   213  
   214  	for _, ms := range mss {
   215  		g.milestones[ms.Name] = ms.ID
   216  	}
   217  	return nil
   218  }
   219  
   220  // CreateLabels creates labels
   221  func (g *GiteaLocalUploader) CreateLabels(labels ...*base.Label) error {
   222  	lbs := make([]*issues_model.Label, 0, len(labels))
   223  	for _, l := range labels {
   224  		if color, err := label.NormalizeColor(l.Color); err != nil {
   225  			log.Warn("Invalid label color: #%s for label: %s in migration to %s/%s", l.Color, l.Name, g.repoOwner, g.repoName)
   226  			l.Color = "#ffffff"
   227  		} else {
   228  			l.Color = color
   229  		}
   230  
   231  		lbs = append(lbs, &issues_model.Label{
   232  			RepoID:      g.repo.ID,
   233  			Name:        l.Name,
   234  			Exclusive:   l.Exclusive,
   235  			Description: l.Description,
   236  			Color:       l.Color,
   237  		})
   238  	}
   239  
   240  	err := issues_model.NewLabels(g.ctx, lbs...)
   241  	if err != nil {
   242  		return err
   243  	}
   244  	for _, lb := range lbs {
   245  		g.labels[lb.Name] = lb
   246  	}
   247  	return nil
   248  }
   249  
   250  // CreateReleases creates releases
   251  func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error {
   252  	rels := make([]*repo_model.Release, 0, len(releases))
   253  	for _, release := range releases {
   254  		if release.Created.IsZero() {
   255  			if !release.Published.IsZero() {
   256  				release.Created = release.Published
   257  			} else {
   258  				release.Created = time.Now()
   259  			}
   260  		}
   261  
   262  		// SECURITY: The TagName must be a valid git ref
   263  		if release.TagName != "" && !git.IsValidRefPattern(release.TagName) {
   264  			release.TagName = ""
   265  		}
   266  
   267  		// SECURITY: The TargetCommitish must be a valid git ref
   268  		if release.TargetCommitish != "" && !git.IsValidRefPattern(release.TargetCommitish) {
   269  			release.TargetCommitish = ""
   270  		}
   271  
   272  		rel := repo_model.Release{
   273  			RepoID:       g.repo.ID,
   274  			TagName:      release.TagName,
   275  			LowerTagName: strings.ToLower(release.TagName),
   276  			Target:       release.TargetCommitish,
   277  			Title:        release.Name,
   278  			Note:         release.Body,
   279  			IsDraft:      release.Draft,
   280  			IsPrerelease: release.Prerelease,
   281  			IsTag:        false,
   282  			CreatedUnix:  timeutil.TimeStamp(release.Created.Unix()),
   283  		}
   284  
   285  		if err := g.remapUser(release, &rel); err != nil {
   286  			return err
   287  		}
   288  
   289  		// calc NumCommits if possible
   290  		if rel.TagName != "" {
   291  			commit, err := g.gitRepo.GetTagCommit(rel.TagName)
   292  			if !git.IsErrNotExist(err) {
   293  				if err != nil {
   294  					return fmt.Errorf("GetTagCommit[%v]: %w", rel.TagName, err)
   295  				}
   296  				rel.Sha1 = commit.ID.String()
   297  				rel.NumCommits, err = commit.CommitsCount()
   298  				if err != nil {
   299  					return fmt.Errorf("CommitsCount: %w", err)
   300  				}
   301  			}
   302  		}
   303  
   304  		for _, asset := range release.Assets {
   305  			if asset.Created.IsZero() {
   306  				if !asset.Updated.IsZero() {
   307  					asset.Created = asset.Updated
   308  				} else {
   309  					asset.Created = release.Created
   310  				}
   311  			}
   312  			attach := repo_model.Attachment{
   313  				UUID:          uuid.New().String(),
   314  				Name:          asset.Name,
   315  				DownloadCount: int64(*asset.DownloadCount),
   316  				Size:          int64(*asset.Size),
   317  				CreatedUnix:   timeutil.TimeStamp(asset.Created.Unix()),
   318  			}
   319  
   320  			// SECURITY: We cannot check the DownloadURL and DownloadFunc are safe here
   321  			// ... we must assume that they are safe and simply download the attachment
   322  			err := func() error {
   323  				// asset.DownloadURL maybe a local file
   324  				var rc io.ReadCloser
   325  				var err error
   326  				if asset.DownloadFunc != nil {
   327  					rc, err = asset.DownloadFunc()
   328  					if err != nil {
   329  						return err
   330  					}
   331  				} else if asset.DownloadURL != nil {
   332  					rc, err = uri.Open(*asset.DownloadURL)
   333  					if err != nil {
   334  						return err
   335  					}
   336  				}
   337  				if rc == nil {
   338  					return nil
   339  				}
   340  				_, err = storage.Attachments.Save(attach.RelativePath(), rc, int64(*asset.Size))
   341  				rc.Close()
   342  				return err
   343  			}()
   344  			if err != nil {
   345  				return err
   346  			}
   347  
   348  			rel.Attachments = append(rel.Attachments, &attach)
   349  		}
   350  
   351  		rels = append(rels, &rel)
   352  	}
   353  
   354  	return repo_model.InsertReleases(g.ctx, rels...)
   355  }
   356  
   357  // SyncTags syncs releases with tags in the database
   358  func (g *GiteaLocalUploader) SyncTags() error {
   359  	return repo_module.SyncReleasesWithTags(g.ctx, g.repo, g.gitRepo)
   360  }
   361  
   362  // CreateIssues creates issues
   363  func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error {
   364  	iss := make([]*issues_model.Issue, 0, len(issues))
   365  	for _, issue := range issues {
   366  		var labels []*issues_model.Label
   367  		for _, label := range issue.Labels {
   368  			lb, ok := g.labels[label.Name]
   369  			if ok {
   370  				labels = append(labels, lb)
   371  			}
   372  		}
   373  
   374  		milestoneID := g.milestones[issue.Milestone]
   375  
   376  		if issue.Created.IsZero() {
   377  			if issue.Closed != nil {
   378  				issue.Created = *issue.Closed
   379  			} else {
   380  				issue.Created = time.Now()
   381  			}
   382  		}
   383  		if issue.Updated.IsZero() {
   384  			if issue.Closed != nil {
   385  				issue.Updated = *issue.Closed
   386  			} else {
   387  				issue.Updated = time.Now()
   388  			}
   389  		}
   390  
   391  		// SECURITY: issue.Ref needs to be a valid reference
   392  		if !git.IsValidRefPattern(issue.Ref) {
   393  			log.Warn("Invalid issue.Ref[%s] in issue #%d in %s/%s", issue.Ref, issue.Number, g.repoOwner, g.repoName)
   394  			issue.Ref = ""
   395  		}
   396  
   397  		is := issues_model.Issue{
   398  			RepoID:      g.repo.ID,
   399  			Repo:        g.repo,
   400  			Index:       issue.Number,
   401  			Title:       base_module.TruncateString(issue.Title, 255),
   402  			Content:     issue.Content,
   403  			Ref:         issue.Ref,
   404  			IsClosed:    issue.State == "closed",
   405  			IsLocked:    issue.IsLocked,
   406  			MilestoneID: milestoneID,
   407  			Labels:      labels,
   408  			CreatedUnix: timeutil.TimeStamp(issue.Created.Unix()),
   409  			UpdatedUnix: timeutil.TimeStamp(issue.Updated.Unix()),
   410  		}
   411  
   412  		if err := g.remapUser(issue, &is); err != nil {
   413  			return err
   414  		}
   415  
   416  		if issue.Closed != nil {
   417  			is.ClosedUnix = timeutil.TimeStamp(issue.Closed.Unix())
   418  		}
   419  		// add reactions
   420  		for _, reaction := range issue.Reactions {
   421  			res := issues_model.Reaction{
   422  				Type:        reaction.Content,
   423  				CreatedUnix: timeutil.TimeStampNow(),
   424  			}
   425  			if err := g.remapUser(reaction, &res); err != nil {
   426  				return err
   427  			}
   428  			is.Reactions = append(is.Reactions, &res)
   429  		}
   430  		iss = append(iss, &is)
   431  	}
   432  
   433  	if len(iss) > 0 {
   434  		if err := issues_model.InsertIssues(iss...); err != nil {
   435  			return err
   436  		}
   437  
   438  		for _, is := range iss {
   439  			g.issues[is.Index] = is
   440  		}
   441  	}
   442  
   443  	return nil
   444  }
   445  
   446  // CreateComments creates comments of issues
   447  func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error {
   448  	cms := make([]*issues_model.Comment, 0, len(comments))
   449  	for _, comment := range comments {
   450  		var issue *issues_model.Issue
   451  		issue, ok := g.issues[comment.IssueIndex]
   452  		if !ok {
   453  			return fmt.Errorf("comment references non existent IssueIndex %d", comment.IssueIndex)
   454  		}
   455  
   456  		if comment.Created.IsZero() {
   457  			comment.Created = time.Unix(int64(issue.CreatedUnix), 0)
   458  		}
   459  		if comment.Updated.IsZero() {
   460  			comment.Updated = comment.Created
   461  		}
   462  		if comment.CommentType == "" {
   463  			// if type field is missing, then assume a normal comment
   464  			comment.CommentType = issues_model.CommentTypeComment.String()
   465  		}
   466  		cm := issues_model.Comment{
   467  			IssueID:     issue.ID,
   468  			Type:        issues_model.AsCommentType(comment.CommentType),
   469  			Content:     comment.Content,
   470  			CreatedUnix: timeutil.TimeStamp(comment.Created.Unix()),
   471  			UpdatedUnix: timeutil.TimeStamp(comment.Updated.Unix()),
   472  		}
   473  
   474  		switch cm.Type {
   475  		case issues_model.CommentTypeAssignees:
   476  			if assigneeID, ok := comment.Meta["AssigneeID"].(int); ok {
   477  				cm.AssigneeID = int64(assigneeID)
   478  			}
   479  			if comment.Meta["RemovedAssigneeID"] != nil {
   480  				cm.RemovedAssignee = true
   481  			}
   482  		case issues_model.CommentTypeChangeTitle:
   483  			if comment.Meta["OldTitle"] != nil {
   484  				cm.OldTitle = fmt.Sprintf("%s", comment.Meta["OldTitle"])
   485  			}
   486  			if comment.Meta["NewTitle"] != nil {
   487  				cm.NewTitle = fmt.Sprintf("%s", comment.Meta["NewTitle"])
   488  			}
   489  		default:
   490  		}
   491  
   492  		if err := g.remapUser(comment, &cm); err != nil {
   493  			return err
   494  		}
   495  
   496  		// add reactions
   497  		for _, reaction := range comment.Reactions {
   498  			res := issues_model.Reaction{
   499  				Type:        reaction.Content,
   500  				CreatedUnix: timeutil.TimeStampNow(),
   501  			}
   502  			if err := g.remapUser(reaction, &res); err != nil {
   503  				return err
   504  			}
   505  			cm.Reactions = append(cm.Reactions, &res)
   506  		}
   507  
   508  		cms = append(cms, &cm)
   509  	}
   510  
   511  	if len(cms) == 0 {
   512  		return nil
   513  	}
   514  	return issues_model.InsertIssueComments(g.ctx, cms)
   515  }
   516  
   517  // CreatePullRequests creates pull requests
   518  func (g *GiteaLocalUploader) CreatePullRequests(prs ...*base.PullRequest) error {
   519  	gprs := make([]*issues_model.PullRequest, 0, len(prs))
   520  	for _, pr := range prs {
   521  		gpr, err := g.newPullRequest(pr)
   522  		if err != nil {
   523  			return err
   524  		}
   525  
   526  		if err := g.remapUser(pr, gpr.Issue); err != nil {
   527  			return err
   528  		}
   529  
   530  		gprs = append(gprs, gpr)
   531  	}
   532  	if err := issues_model.InsertPullRequests(g.ctx, gprs...); err != nil {
   533  		return err
   534  	}
   535  	for _, pr := range gprs {
   536  		g.issues[pr.Issue.Index] = pr.Issue
   537  		pull.AddToTaskQueue(g.ctx, pr)
   538  	}
   539  	return nil
   540  }
   541  
   542  func (g *GiteaLocalUploader) updateGitForPullRequest(pr *base.PullRequest) (head string, err error) {
   543  	// SECURITY: this pr must have been must have been ensured safe
   544  	if !pr.EnsuredSafe {
   545  		log.Error("PR #%d in %s/%s has not been checked for safety.", pr.Number, g.repoOwner, g.repoName)
   546  		return "", fmt.Errorf("the PR[%d] was not checked for safety", pr.Number)
   547  	}
   548  
   549  	// Anonymous function to download the patch file (allows us to use defer)
   550  	err = func() error {
   551  		// if the patchURL is empty there is nothing to download
   552  		if pr.PatchURL == "" {
   553  			return nil
   554  		}
   555  
   556  		// SECURITY: We will assume that the pr.PatchURL has been checked
   557  		// pr.PatchURL maybe a local file - but note EnsureSafe should be asserting that this safe
   558  		ret, err := uri.Open(pr.PatchURL) // TODO: This probably needs to use the downloader as there may be rate limiting issues here
   559  		if err != nil {
   560  			return err
   561  		}
   562  		defer ret.Close()
   563  
   564  		pullDir := filepath.Join(g.repo.RepoPath(), "pulls")
   565  		if err = os.MkdirAll(pullDir, os.ModePerm); err != nil {
   566  			return err
   567  		}
   568  
   569  		f, err := os.Create(filepath.Join(pullDir, fmt.Sprintf("%d.patch", pr.Number)))
   570  		if err != nil {
   571  			return err
   572  		}
   573  		defer f.Close()
   574  
   575  		// TODO: Should there be limits on the size of this file?
   576  		_, err = io.Copy(f, ret)
   577  
   578  		return err
   579  	}()
   580  	if err != nil {
   581  		return "", err
   582  	}
   583  
   584  	head = "unknown repository"
   585  	if pr.IsForkPullRequest() && pr.State != "closed" {
   586  		// OK we want to fetch the current head as a branch from its CloneURL
   587  
   588  		// 1. Is there a head clone URL available?
   589  		// 2. Is there a head ref available?
   590  		if pr.Head.CloneURL == "" || pr.Head.Ref == "" {
   591  			return head, nil
   592  		}
   593  
   594  		// 3. We need to create a remote for this clone url
   595  		// ... maybe we already have a name for this remote
   596  		remote, ok := g.prHeadCache[pr.Head.CloneURL+":"]
   597  		if !ok {
   598  			// ... let's try ownername as a reasonable name
   599  			remote = pr.Head.OwnerName
   600  			if !git.IsValidRefPattern(remote) {
   601  				// ... let's try something less nice
   602  				remote = "head-pr-" + strconv.FormatInt(pr.Number, 10)
   603  			}
   604  			// ... now add the remote
   605  			err := g.gitRepo.AddRemote(remote, pr.Head.CloneURL, true)
   606  			if err != nil {
   607  				log.Error("PR #%d in %s/%s AddRemote[%s] failed: %v", pr.Number, g.repoOwner, g.repoName, remote, err)
   608  			} else {
   609  				g.prHeadCache[pr.Head.CloneURL+":"] = remote
   610  				ok = true
   611  			}
   612  		}
   613  		if !ok {
   614  			return head, nil
   615  		}
   616  
   617  		// 4. Check if we already have this ref?
   618  		localRef, ok := g.prHeadCache[pr.Head.CloneURL+":"+pr.Head.Ref]
   619  		if !ok {
   620  			// ... We would normally name this migrated branch as <OwnerName>/<HeadRef> but we need to ensure that is safe
   621  			localRef = git.SanitizeRefPattern(pr.Head.OwnerName + "/" + pr.Head.Ref)
   622  
   623  			// ... Now we must assert that this does not exist
   624  			if g.gitRepo.IsBranchExist(localRef) {
   625  				localRef = "head-pr-" + strconv.FormatInt(pr.Number, 10) + "/" + localRef
   626  				i := 0
   627  				for g.gitRepo.IsBranchExist(localRef) {
   628  					if i > 5 {
   629  						// ... We tried, we really tried but this is just a seriously unfriendly repo
   630  						return head, nil
   631  					}
   632  					// OK just try some uuids!
   633  					localRef = git.SanitizeRefPattern("head-pr-" + strconv.FormatInt(pr.Number, 10) + uuid.New().String())
   634  					i++
   635  				}
   636  			}
   637  
   638  			fetchArg := pr.Head.Ref + ":" + git.BranchPrefix + localRef
   639  			if strings.HasPrefix(fetchArg, "-") {
   640  				fetchArg = git.BranchPrefix + fetchArg
   641  			}
   642  
   643  			_, _, err = git.NewCommand(g.ctx, "fetch", "--no-tags").AddDashesAndList(remote, fetchArg).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()})
   644  			if err != nil {
   645  				log.Error("Fetch branch from %s failed: %v", pr.Head.CloneURL, err)
   646  				return head, nil
   647  			}
   648  			g.prHeadCache[pr.Head.CloneURL+":"+pr.Head.Ref] = localRef
   649  			head = localRef
   650  		}
   651  
   652  		// 5. Now if pr.Head.SHA == "" we should recover this to the head of this branch
   653  		if pr.Head.SHA == "" {
   654  			headSha, err := g.gitRepo.GetBranchCommitID(localRef)
   655  			if err != nil {
   656  				log.Error("unable to get head SHA of local head for PR #%d from %s in %s/%s. Error: %v", pr.Number, pr.Head.Ref, g.repoOwner, g.repoName, err)
   657  				return head, nil
   658  			}
   659  			pr.Head.SHA = headSha
   660  		}
   661  
   662  		_, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()})
   663  		if err != nil {
   664  			return "", err
   665  		}
   666  
   667  		return head, nil
   668  	}
   669  
   670  	if pr.Head.Ref != "" {
   671  		head = pr.Head.Ref
   672  	}
   673  
   674  	// Ensure the closed PR SHA still points to an existing ref
   675  	if pr.Head.SHA == "" {
   676  		// The SHA is empty
   677  		log.Warn("Empty reference, no pull head for PR #%d in %s/%s", pr.Number, g.repoOwner, g.repoName)
   678  	} else {
   679  		_, _, err = git.NewCommand(g.ctx, "rev-list", "--quiet", "-1").AddDynamicArguments(pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()})
   680  		if err != nil {
   681  			// Git update-ref remove bad references with a relative path
   682  			log.Warn("Deprecated local head %s for PR #%d in %s/%s, removing  %s", pr.Head.SHA, pr.Number, g.repoOwner, g.repoName, pr.GetGitRefName())
   683  		} else {
   684  			// set head information
   685  			_, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.repo.RepoPath()})
   686  			if err != nil {
   687  				log.Error("unable to set %s as the local head for PR #%d from %s in %s/%s. Error: %v", pr.Head.SHA, pr.Number, pr.Head.Ref, g.repoOwner, g.repoName, err)
   688  			}
   689  		}
   690  	}
   691  
   692  	return head, nil
   693  }
   694  
   695  func (g *GiteaLocalUploader) newPullRequest(pr *base.PullRequest) (*issues_model.PullRequest, error) {
   696  	var labels []*issues_model.Label
   697  	for _, label := range pr.Labels {
   698  		lb, ok := g.labels[label.Name]
   699  		if ok {
   700  			labels = append(labels, lb)
   701  		}
   702  	}
   703  
   704  	milestoneID := g.milestones[pr.Milestone]
   705  
   706  	head, err := g.updateGitForPullRequest(pr)
   707  	if err != nil {
   708  		return nil, fmt.Errorf("updateGitForPullRequest: %w", err)
   709  	}
   710  
   711  	// Now we may need to fix the mergebase
   712  	if pr.Base.SHA == "" {
   713  		if pr.Base.Ref != "" && pr.Head.SHA != "" {
   714  			// A PR against a tag base does not make sense - therefore pr.Base.Ref must be a branch
   715  			// TODO: should we be checking for the refs/heads/ prefix on the pr.Base.Ref? (i.e. are these actually branches or refs)
   716  			pr.Base.SHA, _, err = g.gitRepo.GetMergeBase("", git.BranchPrefix+pr.Base.Ref, pr.Head.SHA)
   717  			if err != nil {
   718  				log.Error("Cannot determine the merge base for PR #%d in %s/%s. Error: %v", pr.Number, g.repoOwner, g.repoName, err)
   719  			}
   720  		} else {
   721  			log.Error("Cannot determine the merge base for PR #%d in %s/%s. Not enough information", pr.Number, g.repoOwner, g.repoName)
   722  		}
   723  	}
   724  
   725  	if pr.Created.IsZero() {
   726  		if pr.Closed != nil {
   727  			pr.Created = *pr.Closed
   728  		} else if pr.MergedTime != nil {
   729  			pr.Created = *pr.MergedTime
   730  		} else {
   731  			pr.Created = time.Now()
   732  		}
   733  	}
   734  	if pr.Updated.IsZero() {
   735  		pr.Updated = pr.Created
   736  	}
   737  
   738  	issue := issues_model.Issue{
   739  		RepoID:      g.repo.ID,
   740  		Repo:        g.repo,
   741  		Title:       pr.Title,
   742  		Index:       pr.Number,
   743  		Content:     pr.Content,
   744  		MilestoneID: milestoneID,
   745  		IsPull:      true,
   746  		IsClosed:    pr.State == "closed",
   747  		IsLocked:    pr.IsLocked,
   748  		Labels:      labels,
   749  		CreatedUnix: timeutil.TimeStamp(pr.Created.Unix()),
   750  		UpdatedUnix: timeutil.TimeStamp(pr.Updated.Unix()),
   751  	}
   752  
   753  	if err := g.remapUser(pr, &issue); err != nil {
   754  		return nil, err
   755  	}
   756  
   757  	// add reactions
   758  	for _, reaction := range pr.Reactions {
   759  		res := issues_model.Reaction{
   760  			Type:        reaction.Content,
   761  			CreatedUnix: timeutil.TimeStampNow(),
   762  		}
   763  		if err := g.remapUser(reaction, &res); err != nil {
   764  			return nil, err
   765  		}
   766  		issue.Reactions = append(issue.Reactions, &res)
   767  	}
   768  
   769  	pullRequest := issues_model.PullRequest{
   770  		HeadRepoID: g.repo.ID,
   771  		HeadBranch: head,
   772  		BaseRepoID: g.repo.ID,
   773  		BaseBranch: pr.Base.Ref,
   774  		MergeBase:  pr.Base.SHA,
   775  		Index:      pr.Number,
   776  		HasMerged:  pr.Merged,
   777  
   778  		Issue: &issue,
   779  	}
   780  
   781  	if pullRequest.Issue.IsClosed && pr.Closed != nil {
   782  		pullRequest.Issue.ClosedUnix = timeutil.TimeStamp(pr.Closed.Unix())
   783  	}
   784  	if pullRequest.HasMerged && pr.MergedTime != nil {
   785  		pullRequest.MergedUnix = timeutil.TimeStamp(pr.MergedTime.Unix())
   786  		pullRequest.MergedCommitID = pr.MergeCommitSHA
   787  		pullRequest.MergerID = g.doer.ID
   788  	}
   789  
   790  	// TODO: assignees
   791  
   792  	return &pullRequest, nil
   793  }
   794  
   795  func convertReviewState(state string) issues_model.ReviewType {
   796  	switch state {
   797  	case base.ReviewStatePending:
   798  		return issues_model.ReviewTypePending
   799  	case base.ReviewStateApproved:
   800  		return issues_model.ReviewTypeApprove
   801  	case base.ReviewStateChangesRequested:
   802  		return issues_model.ReviewTypeReject
   803  	case base.ReviewStateCommented:
   804  		return issues_model.ReviewTypeComment
   805  	case base.ReviewStateRequestReview:
   806  		return issues_model.ReviewTypeRequest
   807  	default:
   808  		return issues_model.ReviewTypePending
   809  	}
   810  }
   811  
   812  // CreateReviews create pull request reviews of currently migrated issues
   813  func (g *GiteaLocalUploader) CreateReviews(reviews ...*base.Review) error {
   814  	cms := make([]*issues_model.Review, 0, len(reviews))
   815  	for _, review := range reviews {
   816  		var issue *issues_model.Issue
   817  		issue, ok := g.issues[review.IssueIndex]
   818  		if !ok {
   819  			return fmt.Errorf("review references non existent IssueIndex %d", review.IssueIndex)
   820  		}
   821  		if review.CreatedAt.IsZero() {
   822  			review.CreatedAt = time.Unix(int64(issue.CreatedUnix), 0)
   823  		}
   824  
   825  		cm := issues_model.Review{
   826  			Type:        convertReviewState(review.State),
   827  			IssueID:     issue.ID,
   828  			Content:     review.Content,
   829  			Official:    review.Official,
   830  			CreatedUnix: timeutil.TimeStamp(review.CreatedAt.Unix()),
   831  			UpdatedUnix: timeutil.TimeStamp(review.CreatedAt.Unix()),
   832  		}
   833  
   834  		if err := g.remapUser(review, &cm); err != nil {
   835  			return err
   836  		}
   837  
   838  		cms = append(cms, &cm)
   839  
   840  		// get pr
   841  		pr, ok := g.prCache[issue.ID]
   842  		if !ok {
   843  			var err error
   844  			pr, err = issues_model.GetPullRequestByIssueIDWithNoAttributes(issue.ID)
   845  			if err != nil {
   846  				return err
   847  			}
   848  			g.prCache[issue.ID] = pr
   849  		}
   850  		if pr.MergeBase == "" {
   851  			// No mergebase -> no basis for any patches
   852  			log.Warn("PR #%d in %s/%s: does not have a merge base, all review comments will be ignored", pr.Index, g.repoOwner, g.repoName)
   853  			continue
   854  		}
   855  
   856  		headCommitID, err := g.gitRepo.GetRefCommitID(pr.GetGitRefName())
   857  		if err != nil {
   858  			log.Warn("PR #%d GetRefCommitID[%s] in %s/%s: %v, all review comments will be ignored", pr.Index, pr.GetGitRefName(), g.repoOwner, g.repoName, err)
   859  			continue
   860  		}
   861  
   862  		for _, comment := range review.Comments {
   863  			line := comment.Line
   864  			if line != 0 {
   865  				comment.Position = 1
   866  			} else if comment.DiffHunk != "" {
   867  				_, _, line, _ = git.ParseDiffHunkString(comment.DiffHunk)
   868  			}
   869  
   870  			// SECURITY: The TreePath must be cleaned! use relative path
   871  			comment.TreePath = util.PathJoinRel(comment.TreePath)
   872  
   873  			var patch string
   874  			reader, writer := io.Pipe()
   875  			defer func() {
   876  				_ = reader.Close()
   877  				_ = writer.Close()
   878  			}()
   879  			go func(comment *base.ReviewComment) {
   880  				if err := git.GetRepoRawDiffForFile(g.gitRepo, pr.MergeBase, headCommitID, git.RawDiffNormal, comment.TreePath, writer); err != nil {
   881  					// We should ignore the error since the commit maybe removed when force push to the pull request
   882  					log.Warn("GetRepoRawDiffForFile failed when migrating [%s, %s, %s, %s]: %v", g.gitRepo.Path, pr.MergeBase, headCommitID, comment.TreePath, err)
   883  				}
   884  				_ = writer.Close()
   885  			}(comment)
   886  
   887  			patch, _ = git.CutDiffAroundLine(reader, int64((&issues_model.Comment{Line: int64(line + comment.Position - 1)}).UnsignedLine()), line < 0, setting.UI.CodeCommentLines)
   888  
   889  			if comment.CreatedAt.IsZero() {
   890  				comment.CreatedAt = review.CreatedAt
   891  			}
   892  			if comment.UpdatedAt.IsZero() {
   893  				comment.UpdatedAt = comment.CreatedAt
   894  			}
   895  
   896  			if !git.IsValidSHAPattern(comment.CommitID) {
   897  				log.Warn("Invalid comment CommitID[%s] on comment[%d] in PR #%d of %s/%s replaced with %s", comment.CommitID, pr.Index, g.repoOwner, g.repoName, headCommitID)
   898  				comment.CommitID = headCommitID
   899  			}
   900  
   901  			c := issues_model.Comment{
   902  				Type:        issues_model.CommentTypeCode,
   903  				IssueID:     issue.ID,
   904  				Content:     comment.Content,
   905  				Line:        int64(line + comment.Position - 1),
   906  				TreePath:    comment.TreePath,
   907  				CommitSHA:   comment.CommitID,
   908  				Patch:       patch,
   909  				CreatedUnix: timeutil.TimeStamp(comment.CreatedAt.Unix()),
   910  				UpdatedUnix: timeutil.TimeStamp(comment.UpdatedAt.Unix()),
   911  			}
   912  
   913  			if err := g.remapUser(review, &c); err != nil {
   914  				return err
   915  			}
   916  
   917  			cm.Comments = append(cm.Comments, &c)
   918  		}
   919  	}
   920  
   921  	return issues_model.InsertReviews(g.ctx, cms)
   922  }
   923  
   924  // Rollback when migrating failed, this will rollback all the changes.
   925  func (g *GiteaLocalUploader) Rollback() error {
   926  	if g.repo != nil && g.repo.ID > 0 {
   927  		g.gitRepo.Close()
   928  
   929  		// do not delete the repository, otherwise the end users won't be able to see the last error message
   930  	}
   931  	return nil
   932  }
   933  
   934  // Finish when migrating success, this will do some status update things.
   935  func (g *GiteaLocalUploader) Finish() error {
   936  	if g.repo == nil || g.repo.ID <= 0 {
   937  		return ErrRepoNotCreated
   938  	}
   939  
   940  	// update issue_index
   941  	if err := issues_model.RecalculateIssueIndexForRepo(g.ctx, g.repo.ID); err != nil {
   942  		return err
   943  	}
   944  
   945  	if err := models.UpdateRepoStats(g.ctx, g.repo.ID); err != nil {
   946  		return err
   947  	}
   948  
   949  	g.repo.Status = repo_model.RepositoryReady
   950  	return repo_model.UpdateRepositoryCols(g.ctx, g.repo, "status")
   951  }
   952  
   953  func (g *GiteaLocalUploader) remapUser(source user_model.ExternalUserMigrated, target user_model.ExternalUserRemappable) error {
   954  	var userid int64
   955  	var err error
   956  	if g.sameApp {
   957  		userid, err = g.remapLocalUser(source, target)
   958  	} else {
   959  		userid, err = g.remapExternalUser(source, target)
   960  	}
   961  
   962  	if err != nil {
   963  		return err
   964  	}
   965  
   966  	if userid > 0 {
   967  		return target.RemapExternalUser("", 0, userid)
   968  	}
   969  	return target.RemapExternalUser(source.GetExternalName(), source.GetExternalID(), g.doer.ID)
   970  }
   971  
   972  func (g *GiteaLocalUploader) remapLocalUser(source user_model.ExternalUserMigrated, target user_model.ExternalUserRemappable) (int64, error) {
   973  	userid, ok := g.userMap[source.GetExternalID()]
   974  	if !ok {
   975  		name, err := user_model.GetUserNameByID(g.ctx, source.GetExternalID())
   976  		if err != nil {
   977  			return 0, err
   978  		}
   979  		// let's not reuse an ID when the user was deleted or has a different user name
   980  		if name != source.GetExternalName() {
   981  			userid = 0
   982  		} else {
   983  			userid = source.GetExternalID()
   984  		}
   985  		g.userMap[source.GetExternalID()] = userid
   986  	}
   987  	return userid, nil
   988  }
   989  
   990  func (g *GiteaLocalUploader) remapExternalUser(source user_model.ExternalUserMigrated, target user_model.ExternalUserRemappable) (userid int64, err error) {
   991  	userid, ok := g.userMap[source.GetExternalID()]
   992  	if !ok {
   993  		userid, err = user_model.GetUserIDByExternalUserID(g.gitServiceType.Name(), fmt.Sprintf("%d", source.GetExternalID()))
   994  		if err != nil {
   995  			log.Error("GetUserIDByExternalUserID: %v", err)
   996  			return 0, err
   997  		}
   998  		g.userMap[source.GetExternalID()] = userid
   999  	}
  1000  	return userid, nil
  1001  }