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

     1  // Copyright 2020 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package migrations
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"net/http"
    12  	"net/url"
    13  	"os"
    14  	"path/filepath"
    15  	"strconv"
    16  	"strings"
    17  	"time"
    18  
    19  	user_model "code.gitea.io/gitea/models/user"
    20  	"code.gitea.io/gitea/modules/git"
    21  	"code.gitea.io/gitea/modules/log"
    22  	base "code.gitea.io/gitea/modules/migration"
    23  	"code.gitea.io/gitea/modules/repository"
    24  	"code.gitea.io/gitea/modules/setting"
    25  	"code.gitea.io/gitea/modules/structs"
    26  
    27  	"github.com/google/uuid"
    28  	"gopkg.in/yaml.v3"
    29  )
    30  
    31  var _ base.Uploader = &RepositoryDumper{}
    32  
    33  // RepositoryDumper implements an Uploader to the local directory
    34  type RepositoryDumper struct {
    35  	ctx             context.Context
    36  	baseDir         string
    37  	repoOwner       string
    38  	repoName        string
    39  	opts            base.MigrateOptions
    40  	milestoneFile   *os.File
    41  	labelFile       *os.File
    42  	releaseFile     *os.File
    43  	issueFile       *os.File
    44  	commentFiles    map[int64]*os.File
    45  	pullrequestFile *os.File
    46  	reviewFiles     map[int64]*os.File
    47  
    48  	gitRepo     *git.Repository
    49  	prHeadCache map[string]string
    50  }
    51  
    52  // NewRepositoryDumper creates an gitea Uploader
    53  func NewRepositoryDumper(ctx context.Context, baseDir, repoOwner, repoName string, opts base.MigrateOptions) (*RepositoryDumper, error) {
    54  	baseDir = filepath.Join(baseDir, repoOwner, repoName)
    55  	if err := os.MkdirAll(baseDir, os.ModePerm); err != nil {
    56  		return nil, err
    57  	}
    58  	return &RepositoryDumper{
    59  		ctx:          ctx,
    60  		opts:         opts,
    61  		baseDir:      baseDir,
    62  		repoOwner:    repoOwner,
    63  		repoName:     repoName,
    64  		prHeadCache:  make(map[string]string),
    65  		commentFiles: make(map[int64]*os.File),
    66  		reviewFiles:  make(map[int64]*os.File),
    67  	}, nil
    68  }
    69  
    70  // MaxBatchInsertSize returns the table's max batch insert size
    71  func (g *RepositoryDumper) MaxBatchInsertSize(tp string) int {
    72  	return 1000
    73  }
    74  
    75  func (g *RepositoryDumper) gitPath() string {
    76  	return filepath.Join(g.baseDir, "git")
    77  }
    78  
    79  func (g *RepositoryDumper) wikiPath() string {
    80  	return filepath.Join(g.baseDir, "wiki")
    81  }
    82  
    83  func (g *RepositoryDumper) commentDir() string {
    84  	return filepath.Join(g.baseDir, "comments")
    85  }
    86  
    87  func (g *RepositoryDumper) reviewDir() string {
    88  	return filepath.Join(g.baseDir, "reviews")
    89  }
    90  
    91  func (g *RepositoryDumper) setURLToken(remoteAddr string) (string, error) {
    92  	if len(g.opts.AuthToken) > 0 || len(g.opts.AuthUsername) > 0 {
    93  		u, err := url.Parse(remoteAddr)
    94  		if err != nil {
    95  			return "", err
    96  		}
    97  		u.User = url.UserPassword(g.opts.AuthUsername, g.opts.AuthPassword)
    98  		if len(g.opts.AuthToken) > 0 {
    99  			u.User = url.UserPassword("oauth2", g.opts.AuthToken)
   100  		}
   101  		remoteAddr = u.String()
   102  	}
   103  
   104  	return remoteAddr, nil
   105  }
   106  
   107  // CreateRepo creates a repository
   108  func (g *RepositoryDumper) CreateRepo(repo *base.Repository, opts base.MigrateOptions) error {
   109  	f, err := os.Create(filepath.Join(g.baseDir, "repo.yml"))
   110  	if err != nil {
   111  		return err
   112  	}
   113  	defer f.Close()
   114  
   115  	bs, err := yaml.Marshal(map[string]any{
   116  		"name":         repo.Name,
   117  		"owner":        repo.Owner,
   118  		"description":  repo.Description,
   119  		"clone_addr":   opts.CloneAddr,
   120  		"original_url": repo.OriginalURL,
   121  		"is_private":   opts.Private,
   122  		"service_type": opts.GitServiceType,
   123  		"wiki":         opts.Wiki,
   124  		"issues":       opts.Issues,
   125  		"milestones":   opts.Milestones,
   126  		"labels":       opts.Labels,
   127  		"releases":     opts.Releases,
   128  		"comments":     opts.Comments,
   129  		"pulls":        opts.PullRequests,
   130  		"assets":       opts.ReleaseAssets,
   131  	})
   132  	if err != nil {
   133  		return err
   134  	}
   135  
   136  	if _, err := f.Write(bs); err != nil {
   137  		return err
   138  	}
   139  
   140  	repoPath := g.gitPath()
   141  	if err := os.MkdirAll(repoPath, os.ModePerm); err != nil {
   142  		return err
   143  	}
   144  
   145  	migrateTimeout := 2 * time.Hour
   146  
   147  	remoteAddr, err := g.setURLToken(repo.CloneURL)
   148  	if err != nil {
   149  		return err
   150  	}
   151  
   152  	err = git.Clone(g.ctx, remoteAddr, repoPath, git.CloneRepoOptions{
   153  		Mirror:        true,
   154  		Quiet:         true,
   155  		Timeout:       migrateTimeout,
   156  		SkipTLSVerify: setting.Migrations.SkipTLSVerify,
   157  	})
   158  	if err != nil {
   159  		return fmt.Errorf("Clone: %w", err)
   160  	}
   161  	if err := git.WriteCommitGraph(g.ctx, repoPath); err != nil {
   162  		return err
   163  	}
   164  
   165  	if opts.Wiki {
   166  		wikiPath := g.wikiPath()
   167  		wikiRemotePath := repository.WikiRemoteURL(g.ctx, remoteAddr)
   168  		if len(wikiRemotePath) > 0 {
   169  			if err := os.MkdirAll(wikiPath, os.ModePerm); err != nil {
   170  				return fmt.Errorf("Failed to remove %s: %w", wikiPath, err)
   171  			}
   172  
   173  			if err := git.Clone(g.ctx, wikiRemotePath, wikiPath, git.CloneRepoOptions{
   174  				Mirror:        true,
   175  				Quiet:         true,
   176  				Timeout:       migrateTimeout,
   177  				Branch:        "master",
   178  				SkipTLSVerify: setting.Migrations.SkipTLSVerify,
   179  			}); err != nil {
   180  				log.Warn("Clone wiki: %v", err)
   181  				if err := os.RemoveAll(wikiPath); err != nil {
   182  					return fmt.Errorf("Failed to remove %s: %w", wikiPath, err)
   183  				}
   184  			} else if err := git.WriteCommitGraph(g.ctx, wikiPath); err != nil {
   185  				return err
   186  			}
   187  		}
   188  	}
   189  
   190  	g.gitRepo, err = git.OpenRepository(g.ctx, g.gitPath())
   191  	return err
   192  }
   193  
   194  // Close closes this uploader
   195  func (g *RepositoryDumper) Close() {
   196  	if g.gitRepo != nil {
   197  		g.gitRepo.Close()
   198  	}
   199  	if g.milestoneFile != nil {
   200  		g.milestoneFile.Close()
   201  	}
   202  	if g.labelFile != nil {
   203  		g.labelFile.Close()
   204  	}
   205  	if g.releaseFile != nil {
   206  		g.releaseFile.Close()
   207  	}
   208  	if g.issueFile != nil {
   209  		g.issueFile.Close()
   210  	}
   211  	for _, f := range g.commentFiles {
   212  		f.Close()
   213  	}
   214  	if g.pullrequestFile != nil {
   215  		g.pullrequestFile.Close()
   216  	}
   217  	for _, f := range g.reviewFiles {
   218  		f.Close()
   219  	}
   220  }
   221  
   222  // CreateTopics creates topics
   223  func (g *RepositoryDumper) CreateTopics(topics ...string) error {
   224  	f, err := os.Create(filepath.Join(g.baseDir, "topic.yml"))
   225  	if err != nil {
   226  		return err
   227  	}
   228  	defer f.Close()
   229  
   230  	bs, err := yaml.Marshal(map[string]any{
   231  		"topics": topics,
   232  	})
   233  	if err != nil {
   234  		return err
   235  	}
   236  
   237  	if _, err := f.Write(bs); err != nil {
   238  		return err
   239  	}
   240  
   241  	return nil
   242  }
   243  
   244  // CreateMilestones creates milestones
   245  func (g *RepositoryDumper) CreateMilestones(milestones ...*base.Milestone) error {
   246  	var err error
   247  	if g.milestoneFile == nil {
   248  		g.milestoneFile, err = os.Create(filepath.Join(g.baseDir, "milestone.yml"))
   249  		if err != nil {
   250  			return err
   251  		}
   252  	}
   253  
   254  	bs, err := yaml.Marshal(milestones)
   255  	if err != nil {
   256  		return err
   257  	}
   258  
   259  	if _, err := g.milestoneFile.Write(bs); err != nil {
   260  		return err
   261  	}
   262  
   263  	return nil
   264  }
   265  
   266  // CreateLabels creates labels
   267  func (g *RepositoryDumper) CreateLabels(labels ...*base.Label) error {
   268  	var err error
   269  	if g.labelFile == nil {
   270  		g.labelFile, err = os.Create(filepath.Join(g.baseDir, "label.yml"))
   271  		if err != nil {
   272  			return err
   273  		}
   274  	}
   275  
   276  	bs, err := yaml.Marshal(labels)
   277  	if err != nil {
   278  		return err
   279  	}
   280  
   281  	if _, err := g.labelFile.Write(bs); err != nil {
   282  		return err
   283  	}
   284  
   285  	return nil
   286  }
   287  
   288  // CreateReleases creates releases
   289  func (g *RepositoryDumper) CreateReleases(releases ...*base.Release) error {
   290  	if g.opts.ReleaseAssets {
   291  		for _, release := range releases {
   292  			attachDir := filepath.Join("release_assets", release.TagName)
   293  			if err := os.MkdirAll(filepath.Join(g.baseDir, attachDir), os.ModePerm); err != nil {
   294  				return err
   295  			}
   296  			for _, asset := range release.Assets {
   297  				attachLocalPath := filepath.Join(attachDir, asset.Name)
   298  
   299  				// SECURITY: We cannot check the DownloadURL and DownloadFunc are safe here
   300  				// ... we must assume that they are safe and simply download the attachment
   301  				// download attachment
   302  				err := func(attachPath string) error {
   303  					var rc io.ReadCloser
   304  					var err error
   305  					if asset.DownloadURL == nil {
   306  						rc, err = asset.DownloadFunc()
   307  						if err != nil {
   308  							return err
   309  						}
   310  					} else {
   311  						resp, err := http.Get(*asset.DownloadURL)
   312  						if err != nil {
   313  							return err
   314  						}
   315  						rc = resp.Body
   316  					}
   317  					defer rc.Close()
   318  
   319  					fw, err := os.Create(attachPath)
   320  					if err != nil {
   321  						return fmt.Errorf("create: %w", err)
   322  					}
   323  					defer fw.Close()
   324  
   325  					_, err = io.Copy(fw, rc)
   326  					return err
   327  				}(filepath.Join(g.baseDir, attachLocalPath))
   328  				if err != nil {
   329  					return err
   330  				}
   331  				asset.DownloadURL = &attachLocalPath // to save the filepath on the yml file, change the source
   332  			}
   333  		}
   334  	}
   335  
   336  	var err error
   337  	if g.releaseFile == nil {
   338  		g.releaseFile, err = os.Create(filepath.Join(g.baseDir, "release.yml"))
   339  		if err != nil {
   340  			return err
   341  		}
   342  	}
   343  
   344  	bs, err := yaml.Marshal(releases)
   345  	if err != nil {
   346  		return err
   347  	}
   348  
   349  	if _, err := g.releaseFile.Write(bs); err != nil {
   350  		return err
   351  	}
   352  
   353  	return nil
   354  }
   355  
   356  // SyncTags syncs releases with tags in the database
   357  func (g *RepositoryDumper) SyncTags() error {
   358  	return nil
   359  }
   360  
   361  // CreateIssues creates issues
   362  func (g *RepositoryDumper) CreateIssues(issues ...*base.Issue) error {
   363  	var err error
   364  	if g.issueFile == nil {
   365  		g.issueFile, err = os.Create(filepath.Join(g.baseDir, "issue.yml"))
   366  		if err != nil {
   367  			return err
   368  		}
   369  	}
   370  
   371  	bs, err := yaml.Marshal(issues)
   372  	if err != nil {
   373  		return err
   374  	}
   375  
   376  	if _, err := g.issueFile.Write(bs); err != nil {
   377  		return err
   378  	}
   379  
   380  	return nil
   381  }
   382  
   383  func (g *RepositoryDumper) createItems(dir string, itemFiles map[int64]*os.File, itemsMap map[int64][]any) error {
   384  	if err := os.MkdirAll(dir, os.ModePerm); err != nil {
   385  		return err
   386  	}
   387  
   388  	for number, items := range itemsMap {
   389  		if err := g.encodeItems(number, items, dir, itemFiles); err != nil {
   390  			return err
   391  		}
   392  	}
   393  
   394  	return nil
   395  }
   396  
   397  func (g *RepositoryDumper) encodeItems(number int64, items []any, dir string, itemFiles map[int64]*os.File) error {
   398  	itemFile := itemFiles[number]
   399  	if itemFile == nil {
   400  		var err error
   401  		itemFile, err = os.Create(filepath.Join(dir, fmt.Sprintf("%d.yml", number)))
   402  		if err != nil {
   403  			return err
   404  		}
   405  		itemFiles[number] = itemFile
   406  	}
   407  
   408  	encoder := yaml.NewEncoder(itemFile)
   409  	defer encoder.Close()
   410  
   411  	return encoder.Encode(items)
   412  }
   413  
   414  // CreateComments creates comments of issues
   415  func (g *RepositoryDumper) CreateComments(comments ...*base.Comment) error {
   416  	commentsMap := make(map[int64][]any, len(comments))
   417  	for _, comment := range comments {
   418  		commentsMap[comment.IssueIndex] = append(commentsMap[comment.IssueIndex], comment)
   419  	}
   420  
   421  	return g.createItems(g.commentDir(), g.commentFiles, commentsMap)
   422  }
   423  
   424  func (g *RepositoryDumper) handlePullRequest(pr *base.PullRequest) error {
   425  	// SECURITY: this pr must have been ensured safe
   426  	if !pr.EnsuredSafe {
   427  		log.Error("PR #%d in %s/%s has not been checked for safety ... We will ignore this.", pr.Number, g.repoOwner, g.repoName)
   428  		return fmt.Errorf("unsafe PR #%d", pr.Number)
   429  	}
   430  
   431  	// First we download the patch file
   432  	err := func() error {
   433  		// if the patchURL is empty there is nothing to download
   434  		if pr.PatchURL == "" {
   435  			return nil
   436  		}
   437  
   438  		// SECURITY: We will assume that the pr.PatchURL has been checked
   439  		// pr.PatchURL maybe a local file - but note EnsureSafe should be asserting that this safe
   440  		u, err := g.setURLToken(pr.PatchURL)
   441  		if err != nil {
   442  			return err
   443  		}
   444  
   445  		// SECURITY: We will assume that the pr.PatchURL has been checked
   446  		// pr.PatchURL maybe a local file - but note EnsureSafe should be asserting that this safe
   447  		resp, err := http.Get(u) // TODO: This probably needs to use the downloader as there may be rate limiting issues here
   448  		if err != nil {
   449  			return err
   450  		}
   451  		defer resp.Body.Close()
   452  		pullDir := filepath.Join(g.gitPath(), "pulls")
   453  		if err = os.MkdirAll(pullDir, os.ModePerm); err != nil {
   454  			return err
   455  		}
   456  		fPath := filepath.Join(pullDir, fmt.Sprintf("%d.patch", pr.Number))
   457  		f, err := os.Create(fPath)
   458  		if err != nil {
   459  			return err
   460  		}
   461  		defer f.Close()
   462  
   463  		// TODO: Should there be limits on the size of this file?
   464  		if _, err = io.Copy(f, resp.Body); err != nil {
   465  			return err
   466  		}
   467  		pr.PatchURL = "git/pulls/" + fmt.Sprintf("%d.patch", pr.Number)
   468  
   469  		return nil
   470  	}()
   471  	if err != nil {
   472  		log.Error("PR #%d in %s/%s unable to download patch: %v", pr.Number, g.repoOwner, g.repoName, err)
   473  		return err
   474  	}
   475  
   476  	isFork := pr.IsForkPullRequest()
   477  
   478  	// Even if it's a forked repo PR, we have to change head info as the same as the base info
   479  	oldHeadOwnerName := pr.Head.OwnerName
   480  	pr.Head.OwnerName, pr.Head.RepoName = pr.Base.OwnerName, pr.Base.RepoName
   481  
   482  	if !isFork || pr.State == "closed" {
   483  		return nil
   484  	}
   485  
   486  	// OK we want to fetch the current head as a branch from its CloneURL
   487  
   488  	// 1. Is there a head clone URL available?
   489  	// 2. Is there a head ref available?
   490  	if pr.Head.CloneURL == "" || pr.Head.Ref == "" {
   491  		// Set head information if pr.Head.SHA is available
   492  		if pr.Head.SHA != "" {
   493  			_, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()})
   494  			if err != nil {
   495  				log.Error("PR #%d in %s/%s unable to update-ref for pr HEAD: %v", pr.Number, g.repoOwner, g.repoName, err)
   496  			}
   497  		}
   498  		return nil
   499  	}
   500  
   501  	// 3. We need to create a remote for this clone url
   502  	// ... maybe we already have a name for this remote
   503  	remote, ok := g.prHeadCache[pr.Head.CloneURL+":"]
   504  	if !ok {
   505  		// ... let's try ownername as a reasonable name
   506  		remote = oldHeadOwnerName
   507  		if !git.IsValidRefPattern(remote) {
   508  			// ... let's try something less nice
   509  			remote = "head-pr-" + strconv.FormatInt(pr.Number, 10)
   510  		}
   511  		// ... now add the remote
   512  		err := g.gitRepo.AddRemote(remote, pr.Head.CloneURL, true)
   513  		if err != nil {
   514  			log.Error("PR #%d in %s/%s AddRemote[%s] failed: %v", pr.Number, g.repoOwner, g.repoName, remote, err)
   515  		} else {
   516  			g.prHeadCache[pr.Head.CloneURL+":"] = remote
   517  			ok = true
   518  		}
   519  	}
   520  	if !ok {
   521  		// Set head information if pr.Head.SHA is available
   522  		if pr.Head.SHA != "" {
   523  			_, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()})
   524  			if err != nil {
   525  				log.Error("PR #%d in %s/%s unable to update-ref for pr HEAD: %v", pr.Number, g.repoOwner, g.repoName, err)
   526  			}
   527  		}
   528  
   529  		return nil
   530  	}
   531  
   532  	// 4. Check if we already have this ref?
   533  	localRef, ok := g.prHeadCache[pr.Head.CloneURL+":"+pr.Head.Ref]
   534  	if !ok {
   535  		// ... We would normally name this migrated branch as <OwnerName>/<HeadRef> but we need to ensure that is safe
   536  		localRef = git.SanitizeRefPattern(oldHeadOwnerName + "/" + pr.Head.Ref)
   537  
   538  		// ... Now we must assert that this does not exist
   539  		if g.gitRepo.IsBranchExist(localRef) {
   540  			localRef = "head-pr-" + strconv.FormatInt(pr.Number, 10) + "/" + localRef
   541  			i := 0
   542  			for g.gitRepo.IsBranchExist(localRef) {
   543  				if i > 5 {
   544  					// ... We tried, we really tried but this is just a seriously unfriendly repo
   545  					return fmt.Errorf("unable to create unique local reference from %s", pr.Head.Ref)
   546  				}
   547  				// OK just try some uuids!
   548  				localRef = git.SanitizeRefPattern("head-pr-" + strconv.FormatInt(pr.Number, 10) + uuid.New().String())
   549  				i++
   550  			}
   551  		}
   552  
   553  		fetchArg := pr.Head.Ref + ":" + git.BranchPrefix + localRef
   554  		if strings.HasPrefix(fetchArg, "-") {
   555  			fetchArg = git.BranchPrefix + fetchArg
   556  		}
   557  
   558  		_, _, err = git.NewCommand(g.ctx, "fetch", "--no-tags").AddDashesAndList(remote, fetchArg).RunStdString(&git.RunOpts{Dir: g.gitPath()})
   559  		if err != nil {
   560  			log.Error("Fetch branch from %s failed: %v", pr.Head.CloneURL, err)
   561  			// We need to continue here so that the Head.Ref is reset and we attempt to set the gitref for the PR
   562  			// (This last step will likely fail but we should try to do as much as we can.)
   563  		} else {
   564  			// Cache the localRef as the Head.Ref - if we've failed we can always try again.
   565  			g.prHeadCache[pr.Head.CloneURL+":"+pr.Head.Ref] = localRef
   566  		}
   567  	}
   568  
   569  	// Set the pr.Head.Ref to the localRef
   570  	pr.Head.Ref = localRef
   571  
   572  	// 5. Now if pr.Head.SHA == "" we should recover this to the head of this branch
   573  	if pr.Head.SHA == "" {
   574  		headSha, err := g.gitRepo.GetBranchCommitID(localRef)
   575  		if err != nil {
   576  			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)
   577  			return nil
   578  		}
   579  		pr.Head.SHA = headSha
   580  	}
   581  	if pr.Head.SHA != "" {
   582  		_, _, err = git.NewCommand(g.ctx, "update-ref", "--no-deref").AddDynamicArguments(pr.GetGitRefName(), pr.Head.SHA).RunStdString(&git.RunOpts{Dir: g.gitPath()})
   583  		if err != nil {
   584  			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)
   585  		}
   586  	}
   587  
   588  	return nil
   589  }
   590  
   591  // CreatePullRequests creates pull requests
   592  func (g *RepositoryDumper) CreatePullRequests(prs ...*base.PullRequest) error {
   593  	var err error
   594  	if g.pullrequestFile == nil {
   595  		if err := os.MkdirAll(g.baseDir, os.ModePerm); err != nil {
   596  			return err
   597  		}
   598  		g.pullrequestFile, err = os.Create(filepath.Join(g.baseDir, "pull_request.yml"))
   599  		if err != nil {
   600  			return err
   601  		}
   602  	}
   603  
   604  	encoder := yaml.NewEncoder(g.pullrequestFile)
   605  	defer encoder.Close()
   606  
   607  	count := 0
   608  	for i := 0; i < len(prs); i++ {
   609  		pr := prs[i]
   610  		if err := g.handlePullRequest(pr); err != nil {
   611  			log.Error("PR #%d in %s/%s failed - skipping", pr.Number, g.repoOwner, g.repoName, err)
   612  			continue
   613  		}
   614  		prs[count] = pr
   615  		count++
   616  	}
   617  	prs = prs[:count]
   618  
   619  	return encoder.Encode(prs)
   620  }
   621  
   622  // CreateReviews create pull request reviews
   623  func (g *RepositoryDumper) CreateReviews(reviews ...*base.Review) error {
   624  	reviewsMap := make(map[int64][]any, len(reviews))
   625  	for _, review := range reviews {
   626  		reviewsMap[review.IssueIndex] = append(reviewsMap[review.IssueIndex], review)
   627  	}
   628  
   629  	return g.createItems(g.reviewDir(), g.reviewFiles, reviewsMap)
   630  }
   631  
   632  // Rollback when migrating failed, this will rollback all the changes.
   633  func (g *RepositoryDumper) Rollback() error {
   634  	g.Close()
   635  	return os.RemoveAll(g.baseDir)
   636  }
   637  
   638  // Finish when migrating succeed, this will update something.
   639  func (g *RepositoryDumper) Finish() error {
   640  	return nil
   641  }
   642  
   643  // DumpRepository dump repository according MigrateOptions to a local directory
   644  func DumpRepository(ctx context.Context, baseDir, ownerName string, opts base.MigrateOptions) error {
   645  	doer, err := user_model.GetAdminUser(ctx)
   646  	if err != nil {
   647  		return err
   648  	}
   649  	downloader, err := newDownloader(ctx, ownerName, opts)
   650  	if err != nil {
   651  		return err
   652  	}
   653  	uploader, err := NewRepositoryDumper(ctx, baseDir, ownerName, opts.RepoName, opts)
   654  	if err != nil {
   655  		return err
   656  	}
   657  
   658  	if err := migrateRepository(ctx, doer, downloader, uploader, opts, nil); err != nil {
   659  		if err1 := uploader.Rollback(); err1 != nil {
   660  			log.Error("rollback failed: %v", err1)
   661  		}
   662  		return err
   663  	}
   664  	return nil
   665  }
   666  
   667  func updateOptionsUnits(opts *base.MigrateOptions, units []string) error {
   668  	if len(units) == 0 {
   669  		opts.Wiki = true
   670  		opts.Issues = true
   671  		opts.Milestones = true
   672  		opts.Labels = true
   673  		opts.Releases = true
   674  		opts.Comments = true
   675  		opts.PullRequests = true
   676  		opts.ReleaseAssets = true
   677  	} else {
   678  		for _, unit := range units {
   679  			switch strings.ToLower(strings.TrimSpace(unit)) {
   680  			case "":
   681  				continue
   682  			case "wiki":
   683  				opts.Wiki = true
   684  			case "issues":
   685  				opts.Issues = true
   686  			case "milestones":
   687  				opts.Milestones = true
   688  			case "labels":
   689  				opts.Labels = true
   690  			case "releases":
   691  				opts.Releases = true
   692  			case "release_assets":
   693  				opts.ReleaseAssets = true
   694  			case "comments":
   695  				opts.Comments = true
   696  			case "pull_requests":
   697  				opts.PullRequests = true
   698  			default:
   699  				return errors.New("invalid unit: " + unit)
   700  			}
   701  		}
   702  	}
   703  	return nil
   704  }
   705  
   706  // RestoreRepository restore a repository from the disk directory
   707  func RestoreRepository(ctx context.Context, baseDir, ownerName, repoName string, units []string, validation bool) error {
   708  	doer, err := user_model.GetAdminUser(ctx)
   709  	if err != nil {
   710  		return err
   711  	}
   712  	uploader := NewGiteaLocalUploader(ctx, doer, ownerName, repoName)
   713  	downloader, err := NewRepositoryRestorer(ctx, baseDir, ownerName, repoName, validation)
   714  	if err != nil {
   715  		return err
   716  	}
   717  	opts, err := downloader.getRepoOptions()
   718  	if err != nil {
   719  		return err
   720  	}
   721  	tp, _ := strconv.Atoi(opts["service_type"])
   722  
   723  	migrateOpts := base.MigrateOptions{
   724  		GitServiceType: structs.GitServiceType(tp),
   725  	}
   726  	if err := updateOptionsUnits(&migrateOpts, units); err != nil {
   727  		return err
   728  	}
   729  
   730  	if err = migrateRepository(ctx, doer, downloader, uploader, migrateOpts, nil); err != nil {
   731  		if err1 := uploader.Rollback(); err1 != nil {
   732  			log.Error("rollback failed: %v", err1)
   733  		}
   734  		return err
   735  	}
   736  	return updateMigrationPosterIDByGitService(ctx, structs.GitServiceType(tp))
   737  }