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