Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
383 lines
9.3 KiB
383 lines
9.3 KiB
// Copyright 2019 The Gitea Authors. All rights reserved. |
|
// Copyright 2018 Jonas Franz. All rights reserved. |
|
// Use of this source code is governed by a MIT-style |
|
// license that can be found in the LICENSE file. |
|
|
|
package migrations |
|
|
|
import ( |
|
"context" |
|
"fmt" |
|
"net" |
|
"net/url" |
|
"strings" |
|
|
|
"code.gitea.io/gitea/models" |
|
"code.gitea.io/gitea/modules/log" |
|
"code.gitea.io/gitea/modules/matchlist" |
|
"code.gitea.io/gitea/modules/migrations/base" |
|
"code.gitea.io/gitea/modules/setting" |
|
) |
|
|
|
// MigrateOptions is equal to base.MigrateOptions |
|
type MigrateOptions = base.MigrateOptions |
|
|
|
var ( |
|
factories []base.DownloaderFactory |
|
|
|
allowList *matchlist.Matchlist |
|
blockList *matchlist.Matchlist |
|
) |
|
|
|
// RegisterDownloaderFactory registers a downloader factory |
|
func RegisterDownloaderFactory(factory base.DownloaderFactory) { |
|
factories = append(factories, factory) |
|
} |
|
|
|
func isMigrateURLAllowed(remoteURL string) error { |
|
u, err := url.Parse(strings.ToLower(remoteURL)) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
if strings.EqualFold(u.Scheme, "http") || strings.EqualFold(u.Scheme, "https") { |
|
if len(setting.Migrations.AllowedDomains) > 0 { |
|
if !allowList.Match(u.Host) { |
|
return &models.ErrMigrationNotAllowed{Host: u.Host} |
|
} |
|
} else { |
|
if blockList.Match(u.Host) { |
|
return &models.ErrMigrationNotAllowed{Host: u.Host} |
|
} |
|
} |
|
} |
|
|
|
if !setting.Migrations.AllowLocalNetworks { |
|
addrList, err := net.LookupIP(strings.Split(u.Host, ":")[0]) |
|
if err != nil { |
|
return &models.ErrMigrationNotAllowed{Host: u.Host, NotResolvedIP: true} |
|
} |
|
for _, addr := range addrList { |
|
if isIPPrivate(addr) || !addr.IsGlobalUnicast() { |
|
return &models.ErrMigrationNotAllowed{Host: u.Host, PrivateNet: addr.String()} |
|
} |
|
} |
|
} |
|
|
|
return nil |
|
} |
|
|
|
// MigrateRepository migrate repository according MigrateOptions |
|
func MigrateRepository(ctx context.Context, doer *models.User, ownerName string, opts base.MigrateOptions) (*models.Repository, error) { |
|
err := isMigrateURLAllowed(opts.CloneAddr) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
var ( |
|
downloader base.Downloader |
|
uploader = NewGiteaLocalUploader(ctx, doer, ownerName, opts.RepoName) |
|
) |
|
|
|
for _, factory := range factories { |
|
if factory.GitServiceType() == opts.GitServiceType { |
|
downloader, err = factory.New(ctx, opts) |
|
if err != nil { |
|
return nil, err |
|
} |
|
break |
|
} |
|
} |
|
|
|
if downloader == nil { |
|
opts.Wiki = true |
|
opts.Milestones = false |
|
opts.Labels = false |
|
opts.Releases = false |
|
opts.Comments = false |
|
opts.Issues = false |
|
opts.PullRequests = false |
|
downloader = NewPlainGitDownloader(ownerName, opts.RepoName, opts.CloneAddr) |
|
log.Trace("Will migrate from git: %s", opts.OriginalURL) |
|
} |
|
|
|
uploader.gitServiceType = opts.GitServiceType |
|
|
|
if setting.Migrations.MaxAttempts > 1 { |
|
downloader = base.NewRetryDownloader(ctx, downloader, setting.Migrations.MaxAttempts, setting.Migrations.RetryBackoff) |
|
} |
|
|
|
if err := migrateRepository(downloader, uploader, opts); err != nil { |
|
if err1 := uploader.Rollback(); err1 != nil { |
|
log.Error("rollback failed: %v", err1) |
|
} |
|
|
|
if err2 := models.CreateRepositoryNotice(fmt.Sprintf("Migrate repository from %s failed: %v", opts.OriginalURL, err)); err2 != nil { |
|
log.Error("create repository notice failed: ", err2) |
|
} |
|
return nil, err |
|
} |
|
|
|
return uploader.repo, nil |
|
} |
|
|
|
// migrateRepository will download information and then upload it to Uploader, this is a simple |
|
// process for small repository. For a big repository, save all the data to disk |
|
// before upload is better |
|
func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts base.MigrateOptions) error { |
|
repo, err := downloader.GetRepoInfo() |
|
if err != nil { |
|
return err |
|
} |
|
repo.IsPrivate = opts.Private |
|
repo.IsMirror = opts.Mirror |
|
if opts.Description != "" { |
|
repo.Description = opts.Description |
|
} |
|
log.Trace("migrating git data") |
|
if err := uploader.CreateRepo(repo, opts); err != nil { |
|
return err |
|
} |
|
defer uploader.Close() |
|
|
|
log.Trace("migrating topics") |
|
topics, err := downloader.GetTopics() |
|
if err != nil { |
|
return err |
|
} |
|
if len(topics) > 0 { |
|
if err := uploader.CreateTopics(topics...); err != nil { |
|
return err |
|
} |
|
} |
|
|
|
if opts.Milestones { |
|
log.Trace("migrating milestones") |
|
milestones, err := downloader.GetMilestones() |
|
if err != nil { |
|
return err |
|
} |
|
|
|
msBatchSize := uploader.MaxBatchInsertSize("milestone") |
|
for len(milestones) > 0 { |
|
if len(milestones) < msBatchSize { |
|
msBatchSize = len(milestones) |
|
} |
|
|
|
if err := uploader.CreateMilestones(milestones...); err != nil { |
|
return err |
|
} |
|
milestones = milestones[msBatchSize:] |
|
} |
|
} |
|
|
|
if opts.Labels { |
|
log.Trace("migrating labels") |
|
labels, err := downloader.GetLabels() |
|
if err != nil { |
|
return err |
|
} |
|
|
|
lbBatchSize := uploader.MaxBatchInsertSize("label") |
|
for len(labels) > 0 { |
|
if len(labels) < lbBatchSize { |
|
lbBatchSize = len(labels) |
|
} |
|
|
|
if err := uploader.CreateLabels(labels...); err != nil { |
|
return err |
|
} |
|
labels = labels[lbBatchSize:] |
|
} |
|
} |
|
|
|
if opts.Releases { |
|
log.Trace("migrating releases") |
|
releases, err := downloader.GetReleases() |
|
if err != nil { |
|
return err |
|
} |
|
|
|
relBatchSize := uploader.MaxBatchInsertSize("release") |
|
for len(releases) > 0 { |
|
if len(releases) < relBatchSize { |
|
relBatchSize = len(releases) |
|
} |
|
|
|
if err := uploader.CreateReleases(downloader, releases[:relBatchSize]...); err != nil { |
|
return err |
|
} |
|
releases = releases[relBatchSize:] |
|
} |
|
|
|
// Once all releases (if any) are inserted, sync any remaining non-release tags |
|
if err := uploader.SyncTags(); err != nil { |
|
return err |
|
} |
|
} |
|
|
|
var ( |
|
commentBatchSize = uploader.MaxBatchInsertSize("comment") |
|
reviewBatchSize = uploader.MaxBatchInsertSize("review") |
|
) |
|
|
|
if opts.Issues { |
|
log.Trace("migrating issues and comments") |
|
var issueBatchSize = uploader.MaxBatchInsertSize("issue") |
|
|
|
for i := 1; ; i++ { |
|
issues, isEnd, err := downloader.GetIssues(i, issueBatchSize) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
if err := uploader.CreateIssues(issues...); err != nil { |
|
return err |
|
} |
|
|
|
if !opts.Comments { |
|
continue |
|
} |
|
|
|
var allComments = make([]*base.Comment, 0, commentBatchSize) |
|
for _, issue := range issues { |
|
comments, err := downloader.GetComments(issue.Number) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
allComments = append(allComments, comments...) |
|
|
|
if len(allComments) >= commentBatchSize { |
|
if err := uploader.CreateComments(allComments[:commentBatchSize]...); err != nil { |
|
return err |
|
} |
|
|
|
allComments = allComments[commentBatchSize:] |
|
} |
|
} |
|
|
|
if len(allComments) > 0 { |
|
if err := uploader.CreateComments(allComments...); err != nil { |
|
return err |
|
} |
|
} |
|
|
|
if isEnd { |
|
break |
|
} |
|
} |
|
} |
|
|
|
if opts.PullRequests { |
|
log.Trace("migrating pull requests and comments") |
|
var prBatchSize = uploader.MaxBatchInsertSize("pullrequest") |
|
for i := 1; ; i++ { |
|
prs, isEnd, err := downloader.GetPullRequests(i, prBatchSize) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
if err := uploader.CreatePullRequests(prs...); err != nil { |
|
return err |
|
} |
|
|
|
if !opts.Comments { |
|
continue |
|
} |
|
|
|
// plain comments |
|
var allComments = make([]*base.Comment, 0, commentBatchSize) |
|
for _, pr := range prs { |
|
comments, err := downloader.GetComments(pr.Number) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
allComments = append(allComments, comments...) |
|
|
|
if len(allComments) >= commentBatchSize { |
|
if err := uploader.CreateComments(allComments[:commentBatchSize]...); err != nil { |
|
return err |
|
} |
|
allComments = allComments[commentBatchSize:] |
|
} |
|
} |
|
if len(allComments) > 0 { |
|
if err := uploader.CreateComments(allComments...); err != nil { |
|
return err |
|
} |
|
} |
|
|
|
// migrate reviews |
|
var allReviews = make([]*base.Review, 0, reviewBatchSize) |
|
for _, pr := range prs { |
|
number := pr.Number |
|
|
|
// on gitlab migrations pull number change |
|
if pr.OriginalNumber > 0 { |
|
number = pr.OriginalNumber |
|
} |
|
|
|
reviews, err := downloader.GetReviews(number) |
|
if pr.OriginalNumber > 0 { |
|
for i := range reviews { |
|
reviews[i].IssueIndex = pr.Number |
|
} |
|
} |
|
if err != nil { |
|
return err |
|
} |
|
|
|
allReviews = append(allReviews, reviews...) |
|
|
|
if len(allReviews) >= reviewBatchSize { |
|
if err := uploader.CreateReviews(allReviews[:reviewBatchSize]...); err != nil { |
|
return err |
|
} |
|
allReviews = allReviews[reviewBatchSize:] |
|
} |
|
} |
|
if len(allReviews) > 0 { |
|
if err := uploader.CreateReviews(allReviews...); err != nil { |
|
return err |
|
} |
|
} |
|
|
|
if isEnd { |
|
break |
|
} |
|
} |
|
} |
|
|
|
return nil |
|
} |
|
|
|
// Init migrations service |
|
func Init() error { |
|
var err error |
|
allowList, err = matchlist.NewMatchlist(setting.Migrations.AllowedDomains...) |
|
if err != nil { |
|
return fmt.Errorf("init migration allowList domains failed: %v", err) |
|
} |
|
|
|
blockList, err = matchlist.NewMatchlist(setting.Migrations.BlockedDomains...) |
|
if err != nil { |
|
return fmt.Errorf("init migration blockList domains failed: %v", err) |
|
} |
|
|
|
return nil |
|
} |
|
|
|
// isIPPrivate reports whether ip is a private address, according to |
|
// RFC 1918 (IPv4 addresses) and RFC 4193 (IPv6 addresses). |
|
// from https://github.com/golang/go/pull/42793 |
|
// TODO remove if https://github.com/golang/go/issues/29146 got resolved |
|
func isIPPrivate(ip net.IP) bool { |
|
if ip4 := ip.To4(); ip4 != nil { |
|
return ip4[0] == 10 || |
|
(ip4[0] == 172 && ip4[1]&0xf0 == 16) || |
|
(ip4[0] == 192 && ip4[1] == 168) |
|
} |
|
return len(ip) == net.IPv6len && ip[0]&0xfe == 0xfc |
|
}
|
|
|