diff options
author | 6543 <6543@obermui.de> | 2021-01-21 20:33:58 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-01-21 20:33:58 +0100 |
commit | 81c833d92d04e0a5579e7168aba548dad7e17451 (patch) | |
tree | c928f1b43fc6e2f27603193f0eed657f0760c96d /modules/migrations | |
parent | b5570d3e680570343c1552bfc972b19b161209cd (diff) | |
download | gitea-81c833d92d04e0a5579e7168aba548dad7e17451.tar.gz gitea-81c833d92d04e0a5579e7168aba548dad7e17451.zip |
Add support to migrate from gogs (#14342)
Add support to migrate gogs:
* issues
* comments
* labels
* milestones
* wiki
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Andrew Thornton <art27@cantab.net>
Diffstat (limited to 'modules/migrations')
-rw-r--r-- | modules/migrations/base/downloader.go | 212 | ||||
-rw-r--r-- | modules/migrations/base/error.go | 26 | ||||
-rw-r--r-- | modules/migrations/base/milestone.go | 2 | ||||
-rw-r--r-- | modules/migrations/base/null_downloader.go | 82 | ||||
-rw-r--r-- | modules/migrations/base/retry_downloader.go | 247 | ||||
-rw-r--r-- | modules/migrations/error.go | 3 | ||||
-rw-r--r-- | modules/migrations/git.go | 40 | ||||
-rw-r--r-- | modules/migrations/gitea_downloader.go | 3 | ||||
-rw-r--r-- | modules/migrations/gitea_uploader.go | 23 | ||||
-rw-r--r-- | modules/migrations/github.go | 1 | ||||
-rw-r--r-- | modules/migrations/gitlab.go | 1 | ||||
-rw-r--r-- | modules/migrations/gogs.go | 312 | ||||
-rw-r--r-- | modules/migrations/gogs_test.go | 122 | ||||
-rw-r--r-- | modules/migrations/migrate.go | 83 | ||||
-rw-r--r-- | modules/migrations/restore.go | 1 |
15 files changed, 860 insertions, 298 deletions
diff --git a/modules/migrations/base/downloader.go b/modules/migrations/base/downloader.go index afa99105c9..919f4b52a0 100644 --- a/modules/migrations/base/downloader.go +++ b/modules/migrations/base/downloader.go @@ -7,7 +7,6 @@ package base import ( "context" - "time" "code.gitea.io/gitea/modules/structs" ) @@ -24,6 +23,7 @@ type Downloader interface { GetComments(issueNumber int64) ([]*Comment, error) GetPullRequests(page, perPage int) ([]*PullRequest, bool, error) GetReviews(pullRequestNumber int64) ([]*Review, error) + FormatCloneURL(opts MigrateOptions, remoteAddr string) (string, error) } // DownloaderFactory defines an interface to match a downloader implementation and create a downloader @@ -31,213 +31,3 @@ type DownloaderFactory interface { New(ctx context.Context, opts MigrateOptions) (Downloader, error) GitServiceType() structs.GitServiceType } - -var ( - _ Downloader = &RetryDownloader{} -) - -// RetryDownloader retry the downloads -type RetryDownloader struct { - Downloader - ctx context.Context - RetryTimes int // the total execute times - RetryDelay int // time to delay seconds -} - -// NewRetryDownloader creates a retry downloader -func NewRetryDownloader(ctx context.Context, downloader Downloader, retryTimes, retryDelay int) *RetryDownloader { - return &RetryDownloader{ - Downloader: downloader, - ctx: ctx, - RetryTimes: retryTimes, - RetryDelay: retryDelay, - } -} - -// SetContext set context -func (d *RetryDownloader) SetContext(ctx context.Context) { - d.ctx = ctx - d.Downloader.SetContext(ctx) -} - -// GetRepoInfo returns a repository information with retry -func (d *RetryDownloader) GetRepoInfo() (*Repository, error) { - var ( - times = d.RetryTimes - repo *Repository - err error - ) - for ; times > 0; times-- { - if repo, err = d.Downloader.GetRepoInfo(); err == nil { - return repo, nil - } - select { - case <-d.ctx.Done(): - return nil, d.ctx.Err() - case <-time.After(time.Second * time.Duration(d.RetryDelay)): - } - } - return nil, err -} - -// GetTopics returns a repository's topics with retry -func (d *RetryDownloader) GetTopics() ([]string, error) { - var ( - times = d.RetryTimes - topics []string - err error - ) - for ; times > 0; times-- { - if topics, err = d.Downloader.GetTopics(); err == nil { - return topics, nil - } - select { - case <-d.ctx.Done(): - return nil, d.ctx.Err() - case <-time.After(time.Second * time.Duration(d.RetryDelay)): - } - } - return nil, err -} - -// GetMilestones returns a repository's milestones with retry -func (d *RetryDownloader) GetMilestones() ([]*Milestone, error) { - var ( - times = d.RetryTimes - milestones []*Milestone - err error - ) - for ; times > 0; times-- { - if milestones, err = d.Downloader.GetMilestones(); err == nil { - return milestones, nil - } - select { - case <-d.ctx.Done(): - return nil, d.ctx.Err() - case <-time.After(time.Second * time.Duration(d.RetryDelay)): - } - } - return nil, err -} - -// GetReleases returns a repository's releases with retry -func (d *RetryDownloader) GetReleases() ([]*Release, error) { - var ( - times = d.RetryTimes - releases []*Release - err error - ) - for ; times > 0; times-- { - if releases, err = d.Downloader.GetReleases(); err == nil { - return releases, nil - } - select { - case <-d.ctx.Done(): - return nil, d.ctx.Err() - case <-time.After(time.Second * time.Duration(d.RetryDelay)): - } - } - return nil, err -} - -// GetLabels returns a repository's labels with retry -func (d *RetryDownloader) GetLabels() ([]*Label, error) { - var ( - times = d.RetryTimes - labels []*Label - err error - ) - for ; times > 0; times-- { - if labels, err = d.Downloader.GetLabels(); err == nil { - return labels, nil - } - select { - case <-d.ctx.Done(): - return nil, d.ctx.Err() - case <-time.After(time.Second * time.Duration(d.RetryDelay)): - } - } - return nil, err -} - -// GetIssues returns a repository's issues with retry -func (d *RetryDownloader) GetIssues(page, perPage int) ([]*Issue, bool, error) { - var ( - times = d.RetryTimes - issues []*Issue - isEnd bool - err error - ) - for ; times > 0; times-- { - if issues, isEnd, err = d.Downloader.GetIssues(page, perPage); err == nil { - return issues, isEnd, nil - } - select { - case <-d.ctx.Done(): - return nil, false, d.ctx.Err() - case <-time.After(time.Second * time.Duration(d.RetryDelay)): - } - } - return nil, false, err -} - -// GetComments returns a repository's comments with retry -func (d *RetryDownloader) GetComments(issueNumber int64) ([]*Comment, error) { - var ( - times = d.RetryTimes - comments []*Comment - err error - ) - for ; times > 0; times-- { - if comments, err = d.Downloader.GetComments(issueNumber); err == nil { - return comments, nil - } - select { - case <-d.ctx.Done(): - return nil, d.ctx.Err() - case <-time.After(time.Second * time.Duration(d.RetryDelay)): - } - } - return nil, err -} - -// GetPullRequests returns a repository's pull requests with retry -func (d *RetryDownloader) GetPullRequests(page, perPage int) ([]*PullRequest, bool, error) { - var ( - times = d.RetryTimes - prs []*PullRequest - err error - isEnd bool - ) - for ; times > 0; times-- { - if prs, isEnd, err = d.Downloader.GetPullRequests(page, perPage); err == nil { - return prs, isEnd, nil - } - select { - case <-d.ctx.Done(): - return nil, false, d.ctx.Err() - case <-time.After(time.Second * time.Duration(d.RetryDelay)): - } - } - return nil, false, err -} - -// GetReviews returns pull requests reviews -func (d *RetryDownloader) GetReviews(pullRequestNumber int64) ([]*Review, error) { - var ( - times = d.RetryTimes - reviews []*Review - err error - ) - for ; times > 0; times-- { - if reviews, err = d.Downloader.GetReviews(pullRequestNumber); err == nil { - return reviews, nil - } - select { - case <-d.ctx.Done(): - return nil, d.ctx.Err() - case <-time.After(time.Second * time.Duration(d.RetryDelay)): - } - } - return nil, err -} diff --git a/modules/migrations/base/error.go b/modules/migrations/base/error.go new file mode 100644 index 0000000000..40ddcf4b75 --- /dev/null +++ b/modules/migrations/base/error.go @@ -0,0 +1,26 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package base + +import "fmt" + +// ErrNotSupported represents status if a downloader do not supported something. +type ErrNotSupported struct { + Entity string +} + +// IsErrNotSupported checks if an error is an ErrNotSupported +func IsErrNotSupported(err error) bool { + _, ok := err.(ErrNotSupported) + return ok +} + +// Error return error message +func (err ErrNotSupported) Error() string { + if len(err.Entity) != 0 { + return fmt.Sprintf("'%s' not supported", err.Entity) + } + return "not supported" +} diff --git a/modules/migrations/base/milestone.go b/modules/migrations/base/milestone.go index 8736aa6cfd..921968fcb5 100644 --- a/modules/migrations/base/milestone.go +++ b/modules/migrations/base/milestone.go @@ -15,5 +15,5 @@ type Milestone struct { Created time.Time Updated *time.Time Closed *time.Time - State string + State string // open, closed } diff --git a/modules/migrations/base/null_downloader.go b/modules/migrations/base/null_downloader.go new file mode 100644 index 0000000000..a93c20339b --- /dev/null +++ b/modules/migrations/base/null_downloader.go @@ -0,0 +1,82 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package base + +import ( + "context" + "net/url" +) + +// NullDownloader implements a blank downloader +type NullDownloader struct { +} + +var ( + _ Downloader = &NullDownloader{} +) + +// SetContext set context +func (n NullDownloader) SetContext(_ context.Context) {} + +// GetRepoInfo returns a repository information +func (n NullDownloader) GetRepoInfo() (*Repository, error) { + return nil, &ErrNotSupported{Entity: "RepoInfo"} +} + +// GetTopics return repository topics +func (n NullDownloader) GetTopics() ([]string, error) { + return nil, &ErrNotSupported{Entity: "Topics"} +} + +// GetMilestones returns milestones +func (n NullDownloader) GetMilestones() ([]*Milestone, error) { + return nil, &ErrNotSupported{Entity: "Milestones"} +} + +// GetReleases returns releases +func (n NullDownloader) GetReleases() ([]*Release, error) { + return nil, &ErrNotSupported{Entity: "Releases"} +} + +// GetLabels returns labels +func (n NullDownloader) GetLabels() ([]*Label, error) { + return nil, &ErrNotSupported{Entity: "Labels"} +} + +// GetIssues returns issues according start and limit +func (n NullDownloader) GetIssues(page, perPage int) ([]*Issue, bool, error) { + return nil, false, &ErrNotSupported{Entity: "Issues"} +} + +// GetComments returns comments according issueNumber +func (n NullDownloader) GetComments(issueNumber int64) ([]*Comment, error) { + return nil, &ErrNotSupported{Entity: "Comments"} +} + +// GetPullRequests returns pull requests according page and perPage +func (n NullDownloader) GetPullRequests(page, perPage int) ([]*PullRequest, bool, error) { + return nil, false, &ErrNotSupported{Entity: "PullRequests"} +} + +// GetReviews returns pull requests review +func (n NullDownloader) GetReviews(pullRequestNumber int64) ([]*Review, error) { + return nil, &ErrNotSupported{Entity: "Reviews"} +} + +// FormatCloneURL add authentification into remote URLs +func (n NullDownloader) FormatCloneURL(opts MigrateOptions, remoteAddr string) (string, error) { + if len(opts.AuthToken) > 0 || len(opts.AuthUsername) > 0 { + u, err := url.Parse(remoteAddr) + if err != nil { + return "", err + } + u.User = url.UserPassword(opts.AuthUsername, opts.AuthPassword) + if len(opts.AuthToken) > 0 { + u.User = url.UserPassword("oauth2", opts.AuthToken) + } + return u.String(), nil + } + return remoteAddr, nil +} diff --git a/modules/migrations/base/retry_downloader.go b/modules/migrations/base/retry_downloader.go new file mode 100644 index 0000000000..eeb3cabbc1 --- /dev/null +++ b/modules/migrations/base/retry_downloader.go @@ -0,0 +1,247 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package base + +import ( + "context" + "time" +) + +var ( + _ Downloader = &RetryDownloader{} +) + +// RetryDownloader retry the downloads +type RetryDownloader struct { + Downloader + ctx context.Context + RetryTimes int // the total execute times + RetryDelay int // time to delay seconds +} + +// NewRetryDownloader creates a retry downloader +func NewRetryDownloader(ctx context.Context, downloader Downloader, retryTimes, retryDelay int) *RetryDownloader { + return &RetryDownloader{ + Downloader: downloader, + ctx: ctx, + RetryTimes: retryTimes, + RetryDelay: retryDelay, + } +} + +// SetContext set context +func (d *RetryDownloader) SetContext(ctx context.Context) { + d.ctx = ctx + d.Downloader.SetContext(ctx) +} + +// GetRepoInfo returns a repository information with retry +func (d *RetryDownloader) GetRepoInfo() (*Repository, error) { + var ( + times = d.RetryTimes + repo *Repository + err error + ) + for ; times > 0; times-- { + if repo, err = d.Downloader.GetRepoInfo(); err == nil { + return repo, nil + } + if IsErrNotSupported(err) { + return nil, err + } + select { + case <-d.ctx.Done(): + return nil, d.ctx.Err() + case <-time.After(time.Second * time.Duration(d.RetryDelay)): + } + } + return nil, err +} + +// GetTopics returns a repository's topics with retry +func (d *RetryDownloader) GetTopics() ([]string, error) { + var ( + times = d.RetryTimes + topics []string + err error + ) + for ; times > 0; times-- { + if topics, err = d.Downloader.GetTopics(); err == nil { + return topics, nil + } + if IsErrNotSupported(err) { + return nil, err + } + select { + case <-d.ctx.Done(): + return nil, d.ctx.Err() + case <-time.After(time.Second * time.Duration(d.RetryDelay)): + } + } + return nil, err +} + +// GetMilestones returns a repository's milestones with retry +func (d *RetryDownloader) GetMilestones() ([]*Milestone, error) { + var ( + times = d.RetryTimes + milestones []*Milestone + err error + ) + for ; times > 0; times-- { + if milestones, err = d.Downloader.GetMilestones(); err == nil { + return milestones, nil + } + if IsErrNotSupported(err) { + return nil, err + } + select { + case <-d.ctx.Done(): + return nil, d.ctx.Err() + case <-time.After(time.Second * time.Duration(d.RetryDelay)): + } + } + return nil, err +} + +// GetReleases returns a repository's releases with retry +func (d *RetryDownloader) GetReleases() ([]*Release, error) { + var ( + times = d.RetryTimes + releases []*Release + err error + ) + for ; times > 0; times-- { + if releases, err = d.Downloader.GetReleases(); err == nil { + return releases, nil + } + if IsErrNotSupported(err) { + return nil, err + } + select { + case <-d.ctx.Done(): + return nil, d.ctx.Err() + case <-time.After(time.Second * time.Duration(d.RetryDelay)): + } + } + return nil, err +} + +// GetLabels returns a repository's labels with retry +func (d *RetryDownloader) GetLabels() ([]*Label, error) { + var ( + times = d.RetryTimes + labels []*Label + err error + ) + for ; times > 0; times-- { + if labels, err = d.Downloader.GetLabels(); err == nil { + return labels, nil + } + if IsErrNotSupported(err) { + return nil, err + } + select { + case <-d.ctx.Done(): + return nil, d.ctx.Err() + case <-time.After(time.Second * time.Duration(d.RetryDelay)): + } + } + return nil, err +} + +// GetIssues returns a repository's issues with retry +func (d *RetryDownloader) GetIssues(page, perPage int) ([]*Issue, bool, error) { + var ( + times = d.RetryTimes + issues []*Issue + isEnd bool + err error + ) + for ; times > 0; times-- { + if issues, isEnd, err = d.Downloader.GetIssues(page, perPage); err == nil { + return issues, isEnd, nil + } + if IsErrNotSupported(err) { + return nil, false, err + } + select { + case <-d.ctx.Done(): + return nil, false, d.ctx.Err() + case <-time.After(time.Second * time.Duration(d.RetryDelay)): + } + } + return nil, false, err +} + +// GetComments returns a repository's comments with retry +func (d *RetryDownloader) GetComments(issueNumber int64) ([]*Comment, error) { + var ( + times = d.RetryTimes + comments []*Comment + err error + ) + for ; times > 0; times-- { + if comments, err = d.Downloader.GetComments(issueNumber); err == nil { + return comments, nil + } + if IsErrNotSupported(err) { + return nil, err + } + select { + case <-d.ctx.Done(): + return nil, d.ctx.Err() + case <-time.After(time.Second * time.Duration(d.RetryDelay)): + } + } + return nil, err +} + +// GetPullRequests returns a repository's pull requests with retry +func (d *RetryDownloader) GetPullRequests(page, perPage int) ([]*PullRequest, bool, error) { + var ( + times = d.RetryTimes + prs []*PullRequest + err error + isEnd bool + ) + for ; times > 0; times-- { + if prs, isEnd, err = d.Downloader.GetPullRequests(page, perPage); err == nil { + return prs, isEnd, nil + } + if IsErrNotSupported(err) { + return nil, false, err + } + select { + case <-d.ctx.Done(): + return nil, false, d.ctx.Err() + case <-time.After(time.Second * time.Duration(d.RetryDelay)): + } + } + return nil, false, err +} + +// GetReviews returns pull requests reviews +func (d *RetryDownloader) GetReviews(pullRequestNumber int64) ([]*Review, error) { + var ( + times = d.RetryTimes + reviews []*Review + err error + ) + for ; times > 0; times-- { + if reviews, err = d.Downloader.GetReviews(pullRequestNumber); err == nil { + return reviews, nil + } + if IsErrNotSupported(err) { + return nil, err + } + select { + case <-d.ctx.Done(): + return nil, d.ctx.Err() + case <-time.After(time.Second * time.Duration(d.RetryDelay)): + } + } + return nil, err +} diff --git a/modules/migrations/error.go b/modules/migrations/error.go index 462ba29026..1c77fa9f2f 100644 --- a/modules/migrations/error.go +++ b/modules/migrations/error.go @@ -12,9 +12,6 @@ import ( ) var ( - // ErrNotSupported returns the error not supported - ErrNotSupported = errors.New("not supported") - // ErrRepoNotCreated returns the error that repository not created ErrRepoNotCreated = errors.New("repository is not created yet") ) diff --git a/modules/migrations/git.go b/modules/migrations/git.go index 88222086e4..7e41945474 100644 --- a/modules/migrations/git.go +++ b/modules/migrations/git.go @@ -16,6 +16,7 @@ var ( // PlainGitDownloader implements a Downloader interface to clone git from a http/https URL type PlainGitDownloader struct { + base.NullDownloader ownerName string repoName string remoteURL string @@ -44,42 +45,7 @@ func (g *PlainGitDownloader) GetRepoInfo() (*base.Repository, error) { }, nil } -// GetTopics returns empty list for plain git repo -func (g *PlainGitDownloader) GetTopics() ([]string, error) { +// GetTopics return empty string slice +func (g PlainGitDownloader) GetTopics() ([]string, error) { return []string{}, nil } - -// GetMilestones returns milestones -func (g *PlainGitDownloader) GetMilestones() ([]*base.Milestone, error) { - return nil, ErrNotSupported -} - -// GetLabels returns labels -func (g *PlainGitDownloader) GetLabels() ([]*base.Label, error) { - return nil, ErrNotSupported -} - -// GetReleases returns releases -func (g *PlainGitDownloader) GetReleases() ([]*base.Release, error) { - return nil, ErrNotSupported -} - -// GetIssues returns issues according page and perPage -func (g *PlainGitDownloader) GetIssues(page, perPage int) ([]*base.Issue, bool, error) { - return nil, false, ErrNotSupported -} - -// GetComments returns comments according issueNumber -func (g *PlainGitDownloader) GetComments(issueNumber int64) ([]*base.Comment, error) { - return nil, ErrNotSupported -} - -// GetPullRequests returns pull requests according page and perPage -func (g *PlainGitDownloader) GetPullRequests(start, limit int) ([]*base.PullRequest, bool, error) { - return nil, false, ErrNotSupported -} - -// GetReviews returns reviews according issue number -func (g *PlainGitDownloader) GetReviews(issueNumber int64) ([]*base.Review, error) { - return nil, ErrNotSupported -} diff --git a/modules/migrations/gitea_downloader.go b/modules/migrations/gitea_downloader.go index 0c690464fa..70daf8d5a3 100644 --- a/modules/migrations/gitea_downloader.go +++ b/modules/migrations/gitea_downloader.go @@ -69,6 +69,7 @@ func (f *GiteaDownloaderFactory) GitServiceType() structs.GitServiceType { // GiteaDownloader implements a Downloader interface to get repository information's type GiteaDownloader struct { + base.NullDownloader ctx context.Context client *gitea_sdk.Client repoOwner string @@ -95,7 +96,7 @@ func NewGiteaDownloader(ctx context.Context, baseURL, repoPath, username, passwo path := strings.Split(repoPath, "/") paginationSupport := true - if err := giteaClient.CheckServerVersionConstraint(">=1.12"); err != nil { + if err = giteaClient.CheckServerVersionConstraint(">=1.12"); err != nil { paginationSupport = false } diff --git a/modules/migrations/gitea_uploader.go b/modules/migrations/gitea_uploader.go index 2c79bd4b0f..3be49b5c6c 100644 --- a/modules/migrations/gitea_uploader.go +++ b/modules/migrations/gitea_uploader.go @@ -10,7 +10,6 @@ import ( "context" "fmt" "io" - "net/url" "os" "path/filepath" "strings" @@ -86,22 +85,6 @@ func (g *GiteaLocalUploader) MaxBatchInsertSize(tp string) int { return 10 } -func fullURL(opts base.MigrateOptions, remoteAddr string) (string, error) { - var fullRemoteAddr = remoteAddr - if len(opts.AuthToken) > 0 || len(opts.AuthUsername) > 0 { - u, err := url.Parse(remoteAddr) - if err != nil { - return "", err - } - u.User = url.UserPassword(opts.AuthUsername, opts.AuthPassword) - if len(opts.AuthToken) > 0 { - u.User = url.UserPassword("oauth2", opts.AuthToken) - } - fullRemoteAddr = u.String() - } - return fullRemoteAddr, nil -} - // CreateRepo creates a repository func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.MigrateOptions) error { owner, err := models.GetUserByName(g.repoOwner) @@ -109,10 +92,6 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate return err } - remoteAddr, err := fullURL(opts, repo.CloneURL) - if err != nil { - return err - } var r *models.Repository if opts.MigrateToRepoID <= 0 { r, err = repo_module.CreateRepository(g.doer, owner, models.CreateRepoOptions{ @@ -138,7 +117,7 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate OriginalURL: repo.OriginalURL, GitServiceType: opts.GitServiceType, Mirror: repo.IsMirror, - CloneAddr: remoteAddr, + CloneAddr: repo.CloneURL, Private: repo.IsPrivate, Wiki: opts.Wiki, Releases: opts.Releases, // if didn't get releases, then sync them from tags diff --git a/modules/migrations/github.go b/modules/migrations/github.go index 178517ba42..4d832387ba 100644 --- a/modules/migrations/github.go +++ b/modules/migrations/github.go @@ -65,6 +65,7 @@ func (f *GithubDownloaderV3Factory) GitServiceType() structs.GitServiceType { // GithubDownloaderV3 implements a Downloader interface to get repository informations // from github via APIv3 type GithubDownloaderV3 struct { + base.NullDownloader ctx context.Context client *github.Client repoOwner string diff --git a/modules/migrations/gitlab.go b/modules/migrations/gitlab.go index e3fa956758..a697075ff8 100644 --- a/modules/migrations/gitlab.go +++ b/modules/migrations/gitlab.go @@ -63,6 +63,7 @@ func (f *GitlabDownloaderFactory) GitServiceType() structs.GitServiceType { // - issueSeen, working alongside issueCount, is checked in GetComments() to see whether we // need to fetch the Issue or PR comments, as Gitlab stores them separately. type GitlabDownloader struct { + base.NullDownloader ctx context.Context client *gitlab.Client repoID int diff --git a/modules/migrations/gogs.go b/modules/migrations/gogs.go new file mode 100644 index 0000000000..b616907938 --- /dev/null +++ b/modules/migrations/gogs.go @@ -0,0 +1,312 @@ +// Copyright 2019 The Gitea Authors. 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/http" + "net/url" + "strings" + "time" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/migrations/base" + "code.gitea.io/gitea/modules/structs" + + "github.com/gogs/go-gogs-client" +) + +var ( + _ base.Downloader = &GogsDownloader{} + _ base.DownloaderFactory = &GogsDownloaderFactory{} +) + +func init() { + RegisterDownloaderFactory(&GogsDownloaderFactory{}) +} + +// GogsDownloaderFactory defines a gogs downloader factory +type GogsDownloaderFactory struct { +} + +// New returns a Downloader related to this factory according MigrateOptions +func (f *GogsDownloaderFactory) New(ctx context.Context, opts base.MigrateOptions) (base.Downloader, error) { + u, err := url.Parse(opts.CloneAddr) + if err != nil { + return nil, err + } + + baseURL := u.Scheme + "://" + u.Host + repoNameSpace := strings.TrimSuffix(u.Path, ".git") + repoNameSpace = strings.Trim(repoNameSpace, "/") + + fields := strings.Split(repoNameSpace, "/") + if len(fields) < 2 { + return nil, fmt.Errorf("invalid path: %s", repoNameSpace) + } + + log.Trace("Create gogs downloader. BaseURL: %s RepoOwner: %s RepoName: %s", baseURL, fields[0], fields[1]) + return NewGogsDownloader(ctx, baseURL, opts.AuthUsername, opts.AuthPassword, opts.AuthToken, fields[0], fields[1]), nil +} + +// GitServiceType returns the type of git service +func (f *GogsDownloaderFactory) GitServiceType() structs.GitServiceType { + return structs.GogsService +} + +// GogsDownloader implements a Downloader interface to get repository informations +// from gogs via API +type GogsDownloader struct { + base.NullDownloader + ctx context.Context + client *gogs.Client + baseURL string + repoOwner string + repoName string + userName string + password string + openIssuesFinished bool + openIssuesPages int + transport http.RoundTripper +} + +// SetContext set context +func (g *GogsDownloader) SetContext(ctx context.Context) { + g.ctx = ctx +} + +// NewGogsDownloader creates a gogs Downloader via gogs API +func NewGogsDownloader(ctx context.Context, baseURL, userName, password, token, repoOwner, repoName string) *GogsDownloader { + var downloader = GogsDownloader{ + ctx: ctx, + baseURL: baseURL, + userName: userName, + password: password, + repoOwner: repoOwner, + repoName: repoName, + } + + var client *gogs.Client + if len(token) != 0 { + client = gogs.NewClient(baseURL, token) + downloader.userName = token + } else { + downloader.transport = &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + req.SetBasicAuth(userName, password) + return nil, nil + }, + } + + client = gogs.NewClient(baseURL, "") + client.SetHTTPClient(&http.Client{ + Transport: &downloader, + }) + } + + downloader.client = client + return &downloader +} + +// RoundTrip wraps the provided request within this downloader's context and passes it to our internal http.Transport. +// This implements http.RoundTripper and makes the gogs client requests cancellable even though it is not cancellable itself +func (g *GogsDownloader) RoundTrip(req *http.Request) (*http.Response, error) { + return g.transport.RoundTrip(req.WithContext(g.ctx)) +} + +// GetRepoInfo returns a repository information +func (g *GogsDownloader) GetRepoInfo() (*base.Repository, error) { + gr, err := g.client.GetRepo(g.repoOwner, g.repoName) + if err != nil { + return nil, err + } + + // convert gogs repo to stand Repo + return &base.Repository{ + Owner: g.repoOwner, + Name: g.repoName, + IsPrivate: gr.Private, + Description: gr.Description, + CloneURL: gr.CloneURL, + OriginalURL: gr.HTMLURL, + DefaultBranch: gr.DefaultBranch, + }, nil +} + +// GetMilestones returns milestones +func (g *GogsDownloader) GetMilestones() ([]*base.Milestone, error) { + var perPage = 100 + var milestones = make([]*base.Milestone, 0, perPage) + + ms, err := g.client.ListRepoMilestones(g.repoOwner, g.repoName) + if err != nil { + return nil, err + } + + t := time.Now() + + for _, m := range ms { + milestones = append(milestones, &base.Milestone{ + Title: m.Title, + Description: m.Description, + Deadline: m.Deadline, + State: string(m.State), + Created: t, + Updated: &t, + Closed: m.Closed, + }) + } + + return milestones, nil +} + +// GetLabels returns labels +func (g *GogsDownloader) GetLabels() ([]*base.Label, error) { + var perPage = 100 + var labels = make([]*base.Label, 0, perPage) + ls, err := g.client.ListRepoLabels(g.repoOwner, g.repoName) + if err != nil { + return nil, err + } + + for _, label := range ls { + labels = append(labels, convertGogsLabel(label)) + } + + return labels, nil +} + +// GetIssues returns issues according start and limit, perPage is not supported +func (g *GogsDownloader) GetIssues(page, _ int) ([]*base.Issue, bool, error) { + var state string + if g.openIssuesFinished { + state = string(gogs.STATE_CLOSED) + page -= g.openIssuesPages + } else { + state = string(gogs.STATE_OPEN) + g.openIssuesPages = page + } + + issues, isEnd, err := g.getIssues(page, state) + if err != nil { + return nil, false, err + } + + if isEnd { + if g.openIssuesFinished { + return issues, true, nil + } + g.openIssuesFinished = true + } + + return issues, false, nil +} + +func (g *GogsDownloader) getIssues(page int, state string) ([]*base.Issue, bool, error) { + var allIssues = make([]*base.Issue, 0, 10) + + issues, err := g.client.ListRepoIssues(g.repoOwner, g.repoName, gogs.ListIssueOption{ + Page: page, + State: state, + }) + if err != nil { + return nil, false, fmt.Errorf("error while listing repos: %v", err) + } + + for _, issue := range issues { + if issue.PullRequest != nil { + continue + } + allIssues = append(allIssues, convertGogsIssue(issue)) + } + + return allIssues, len(issues) == 0, nil +} + +// GetComments returns comments according issueNumber +func (g *GogsDownloader) GetComments(issueNumber int64) ([]*base.Comment, error) { + var allComments = make([]*base.Comment, 0, 100) + + comments, err := g.client.ListIssueComments(g.repoOwner, g.repoName, issueNumber) + if err != nil { + return nil, fmt.Errorf("error while listing repos: %v", err) + } + for _, comment := range comments { + if len(comment.Body) == 0 || comment.Poster == nil { + continue + } + allComments = append(allComments, &base.Comment{ + IssueIndex: issueNumber, + PosterID: comment.Poster.ID, + PosterName: comment.Poster.Login, + PosterEmail: comment.Poster.Email, + Content: comment.Body, + Created: comment.Created, + Updated: comment.Updated, + }) + } + + return allComments, nil +} + +// GetTopics return repository topics +func (g *GogsDownloader) GetTopics() ([]string, error) { + return []string{}, nil +} + +// FormatCloneURL add authentification into remote URLs +func (g *GogsDownloader) FormatCloneURL(opts MigrateOptions, remoteAddr string) (string, error) { + if len(opts.AuthToken) > 0 || len(opts.AuthUsername) > 0 { + u, err := url.Parse(remoteAddr) + if err != nil { + return "", err + } + if len(opts.AuthToken) != 0 { + u.User = url.UserPassword(opts.AuthToken, "") + } else { + u.User = url.UserPassword(opts.AuthUsername, opts.AuthPassword) + } + return u.String(), nil + } + return remoteAddr, nil +} + +func convertGogsIssue(issue *gogs.Issue) *base.Issue { + var milestone string + if issue.Milestone != nil { + milestone = issue.Milestone.Title + } + var labels = make([]*base.Label, 0, len(issue.Labels)) + for _, l := range issue.Labels { + labels = append(labels, convertGogsLabel(l)) + } + + var closed *time.Time + if issue.State == gogs.STATE_CLOSED { + // gogs client haven't provide closed, so we use updated instead + closed = &issue.Updated + } + + return &base.Issue{ + Title: issue.Title, + Number: issue.Index, + PosterName: issue.Poster.Login, + PosterEmail: issue.Poster.Email, + Content: issue.Body, + Milestone: milestone, + State: string(issue.State), + Created: issue.Created, + Labels: labels, + Closed: closed, + } +} + +func convertGogsLabel(label *gogs.Label) *base.Label { + return &base.Label{ + Name: label.Name, + Color: label.Color, + } +} diff --git a/modules/migrations/gogs_test.go b/modules/migrations/gogs_test.go new file mode 100644 index 0000000000..c240ae6432 --- /dev/null +++ b/modules/migrations/gogs_test.go @@ -0,0 +1,122 @@ +// Copyright 2019 The Gitea Authors. 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" + "net/http" + "os" + "testing" + "time" + + "code.gitea.io/gitea/modules/migrations/base" + + "github.com/stretchr/testify/assert" +) + +func TestGogsDownloadRepo(t *testing.T) { + // Skip tests if Gogs token is not found + gogsPersonalAccessToken := os.Getenv("GOGS_READ_TOKEN") + if len(gogsPersonalAccessToken) == 0 { + t.Skip("skipped test because GOGS_READ_TOKEN was not in the environment") + } + + resp, err := http.Get("https://try.gogs.io/lunnytest/TESTREPO") + if err != nil || resp.StatusCode/100 != 2 { + // skip and don't run test + t.Skipf("visit test repo failed, ignored") + return + } + + downloader := NewGogsDownloader(context.Background(), "https://try.gogs.io", "", "", gogsPersonalAccessToken, "lunnytest", "TESTREPO") + repo, err := downloader.GetRepoInfo() + assert.NoError(t, err) + + assert.EqualValues(t, &base.Repository{ + Name: "TESTREPO", + Owner: "lunnytest", + Description: "", + CloneURL: "https://try.gogs.io/lunnytest/TESTREPO.git", + }, repo) + + milestones, err := downloader.GetMilestones() + assert.NoError(t, err) + assert.True(t, len(milestones) == 1) + + for _, milestone := range milestones { + switch milestone.Title { + case "1.0": + assert.EqualValues(t, "open", milestone.State) + } + } + + labels, err := downloader.GetLabels() + assert.NoError(t, err) + assert.Len(t, labels, 7) + for _, l := range labels { + switch l.Name { + case "bug": + assertLabelEqual(t, "bug", "ee0701", "", l) + case "duplicated": + assertLabelEqual(t, "duplicated", "cccccc", "", l) + case "enhancement": + assertLabelEqual(t, "enhancement", "84b6eb", "", l) + case "help wanted": + assertLabelEqual(t, "help wanted", "128a0c", "", l) + case "invalid": + assertLabelEqual(t, "invalid", "e6e6e6", "", l) + case "question": + assertLabelEqual(t, "question", "cc317c", "", l) + case "wontfix": + assertLabelEqual(t, "wontfix", "ffffff", "", l) + } + } + + _, err = downloader.GetReleases() + assert.Error(t, err) + + // downloader.GetIssues() + issues, isEnd, err := downloader.GetIssues(1, 8) + assert.NoError(t, err) + assert.EqualValues(t, 1, len(issues)) + assert.False(t, isEnd) + + assert.EqualValues(t, []*base.Issue{ + { + Number: 1, + Title: "test", + Content: "test", + Milestone: "", + PosterName: "lunny", + PosterEmail: "xiaolunwen@gmail.com", + State: "open", + Created: time.Date(2019, 06, 11, 8, 16, 44, 0, time.UTC), + Labels: []*base.Label{ + { + Name: "bug", + Color: "ee0701", + }, + }, + }, + }, issues) + + // downloader.GetComments() + comments, err := downloader.GetComments(1) + assert.NoError(t, err) + assert.EqualValues(t, 1, len(comments)) + assert.EqualValues(t, []*base.Comment{ + { + PosterName: "lunny", + PosterEmail: "xiaolunwen@gmail.com", + Created: time.Date(2019, 06, 11, 8, 19, 50, 0, time.UTC), + Updated: time.Date(2019, 06, 11, 8, 19, 50, 0, time.UTC), + Content: `1111`, + }, + }, comments) + + // downloader.GetPullRequests() + _, _, err = downloader.GetPullRequests(1, 3) + assert.Error(t, err) +} diff --git a/modules/migrations/migrate.go b/modules/migrations/migrate.go index 4c15626e57..b9c17478a9 100644 --- a/modules/migrations/migrate.go +++ b/modules/migrations/migrate.go @@ -133,15 +133,22 @@ func newDownloader(ctx context.Context, ownerName string, opts base.MigrateOptio func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts base.MigrateOptions) error { repo, err := downloader.GetRepoInfo() if err != nil { - return err + if !base.IsErrNotSupported(err) { + return err + } + log.Info("migrating repo infos is not supported, ignored") } repo.IsPrivate = opts.Private repo.IsMirror = opts.Mirror if opts.Description != "" { repo.Description = opts.Description } + if repo.CloneURL, err = downloader.FormatCloneURL(opts, repo.CloneURL); err != nil { + return err + } + log.Trace("migrating git data") - if err := uploader.CreateRepo(repo, opts); err != nil { + if err = uploader.CreateRepo(repo, opts); err != nil { return err } defer uploader.Close() @@ -149,10 +156,13 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts log.Trace("migrating topics") topics, err := downloader.GetTopics() if err != nil { - return err + if !base.IsErrNotSupported(err) { + return err + } + log.Warn("migrating topics is not supported, ignored") } - if len(topics) > 0 { - if err := uploader.CreateTopics(topics...); err != nil { + if len(topics) != 0 { + if err = uploader.CreateTopics(topics...); err != nil { return err } } @@ -161,7 +171,10 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts log.Trace("migrating milestones") milestones, err := downloader.GetMilestones() if err != nil { - return err + if !base.IsErrNotSupported(err) { + return err + } + log.Warn("migrating milestones is not supported, ignored") } msBatchSize := uploader.MaxBatchInsertSize("milestone") @@ -181,7 +194,10 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts log.Trace("migrating labels") labels, err := downloader.GetLabels() if err != nil { - return err + if !base.IsErrNotSupported(err) { + return err + } + log.Warn("migrating labels is not supported, ignored") } lbBatchSize := uploader.MaxBatchInsertSize("label") @@ -201,7 +217,10 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts log.Trace("migrating releases") releases, err := downloader.GetReleases() if err != nil { - return err + if !base.IsErrNotSupported(err) { + return err + } + log.Warn("migrating releases is not supported, ignored") } relBatchSize := uploader.MaxBatchInsertSize("release") @@ -210,14 +229,14 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts relBatchSize = len(releases) } - if err := uploader.CreateReleases(releases[:relBatchSize]...); err != nil { + if err = uploader.CreateReleases(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 { + if err = uploader.SyncTags(); err != nil { return err } } @@ -234,7 +253,11 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts for i := 1; ; i++ { issues, isEnd, err := downloader.GetIssues(i, issueBatchSize) if err != nil { - return err + if !base.IsErrNotSupported(err) { + return err + } + log.Warn("migrating issues is not supported, ignored") + break } if err := uploader.CreateIssues(issues...); err != nil { @@ -247,13 +270,16 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts log.Trace("migrating issue %d's comments", issue.Number) comments, err := downloader.GetComments(issue.Number) if err != nil { - return err + if !base.IsErrNotSupported(err) { + return err + } + log.Warn("migrating comments is not supported, ignored") } allComments = append(allComments, comments...) if len(allComments) >= commentBatchSize { - if err := uploader.CreateComments(allComments[:commentBatchSize]...); err != nil { + if err = uploader.CreateComments(allComments[:commentBatchSize]...); err != nil { return err } @@ -262,7 +288,7 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts } if len(allComments) > 0 { - if err := uploader.CreateComments(allComments...); err != nil { + if err = uploader.CreateComments(allComments...); err != nil { return err } } @@ -280,7 +306,11 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts for i := 1; ; i++ { prs, isEnd, err := downloader.GetPullRequests(i, prBatchSize) if err != nil { - return err + if !base.IsErrNotSupported(err) { + return err + } + log.Warn("migrating pull requests is not supported, ignored") + break } if err := uploader.CreatePullRequests(prs...); err != nil { @@ -294,20 +324,23 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts log.Trace("migrating pull request %d's comments", pr.Number) comments, err := downloader.GetComments(pr.Number) if err != nil { - return err + if !base.IsErrNotSupported(err) { + return err + } + log.Warn("migrating comments is not supported, ignored") } allComments = append(allComments, comments...) if len(allComments) >= commentBatchSize { - if err := uploader.CreateComments(allComments[:commentBatchSize]...); err != nil { + if err = uploader.CreateComments(allComments[:commentBatchSize]...); err != nil { return err } allComments = allComments[commentBatchSize:] } } if len(allComments) > 0 { - if err := uploader.CreateComments(allComments...); err != nil { + if err = uploader.CreateComments(allComments...); err != nil { return err } } @@ -323,26 +356,30 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts } reviews, err := downloader.GetReviews(number) + if err != nil { + if !base.IsErrNotSupported(err) { + return err + } + log.Warn("migrating reviews is not supported, ignored") + break + } 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 { + if err = uploader.CreateReviews(allReviews[:reviewBatchSize]...); err != nil { return err } allReviews = allReviews[reviewBatchSize:] } } if len(allReviews) > 0 { - if err := uploader.CreateReviews(allReviews...); err != nil { + if err = uploader.CreateReviews(allReviews...); err != nil { return err } } diff --git a/modules/migrations/restore.go b/modules/migrations/restore.go index 5550aaeb03..e1ab408e41 100644 --- a/modules/migrations/restore.go +++ b/modules/migrations/restore.go @@ -19,6 +19,7 @@ import ( // RepositoryRestorer implements an Downloader from the local directory type RepositoryRestorer struct { + base.NullDownloader ctx context.Context baseDir string repoOwner string |