diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go index 696d3b9a5..b3fead7da 100644 --- a/modules/auth/repo_form.go +++ b/modules/auth/repo_form.go @@ -56,8 +56,10 @@ func (f *CreateRepoForm) Validate(ctx *macaron.Context, errs binding.Errors) bin type MigrateRepoForm struct { // required: true CloneAddr string `json:"clone_addr" binding:"Required"` + Service int `json:"service"` AuthUsername string `json:"auth_username"` AuthPassword string `json:"auth_password"` + AuthToken string `json:"auth_token"` // required: true UID int64 `json:"uid" binding:"Required"` // required: true diff --git a/modules/migrations/base/downloader.go b/modules/migrations/base/downloader.go index c31f3df1d..b692969ba 100644 --- a/modules/migrations/base/downloader.go +++ b/modules/migrations/base/downloader.go @@ -7,13 +7,20 @@ package base import ( "context" + "io" "time" "code.gitea.io/gitea/modules/structs" ) +// AssetDownloader downloads an asset (attachment) for a release +type AssetDownloader interface { + GetAsset(tag string, id int64) (io.ReadCloser, error) +} + // Downloader downloads the site repo informations type Downloader interface { + AssetDownloader SetContext(context.Context) GetRepoInfo() (*Repository, error) GetTopics() ([]string, error) @@ -28,7 +35,6 @@ type Downloader interface { // DownloaderFactory defines an interface to match a downloader implementation and create a downloader type DownloaderFactory interface { - Match(opts MigrateOptions) (bool, error) New(opts MigrateOptions) (Downloader, error) GitServiceType() structs.GitServiceType } diff --git a/modules/migrations/base/release.go b/modules/migrations/base/release.go index b2541f1bf..2a223920c 100644 --- a/modules/migrations/base/release.go +++ b/modules/migrations/base/release.go @@ -8,7 +8,7 @@ import "time" // ReleaseAsset represents a release asset type ReleaseAsset struct { - URL string + ID int64 Name string ContentType *string Size *int diff --git a/modules/migrations/base/uploader.go b/modules/migrations/base/uploader.go index 85ad60fe0..07c2bb0d4 100644 --- a/modules/migrations/base/uploader.go +++ b/modules/migrations/base/uploader.go @@ -11,7 +11,7 @@ type Uploader interface { CreateRepo(repo *Repository, opts MigrateOptions) error CreateTopics(topic ...string) error CreateMilestones(milestones ...*Milestone) error - CreateReleases(releases ...*Release) error + CreateReleases(downloader Downloader, releases ...*Release) error SyncTags() error CreateLabels(labels ...*Label) error CreateIssues(issues ...*Issue) error diff --git a/modules/migrations/git.go b/modules/migrations/git.go index af345808b..5c9acb253 100644 --- a/modules/migrations/git.go +++ b/modules/migrations/git.go @@ -6,6 +6,7 @@ package migrations import ( "context" + "io" "code.gitea.io/gitea/modules/migrations/base" ) @@ -64,6 +65,11 @@ func (g *PlainGitDownloader) GetReleases() ([]*base.Release, error) { return nil, ErrNotSupported } +// GetAsset returns an asset +func (g *PlainGitDownloader) GetAsset(_ string, _ int64) (io.ReadCloser, 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 diff --git a/modules/migrations/gitea.go b/modules/migrations/gitea.go index 8c097e143..082ddcd5f 100644 --- a/modules/migrations/gitea.go +++ b/modules/migrations/gitea.go @@ -93,12 +93,15 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate } var remoteAddr = repo.CloneURL - if len(opts.AuthUsername) > 0 { + if len(opts.AuthToken) > 0 || len(opts.AuthUsername) > 0 { u, err := url.Parse(repo.CloneURL) 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) + } remoteAddr = u.String() } @@ -210,7 +213,7 @@ func (g *GiteaLocalUploader) CreateLabels(labels ...*base.Label) error { } // CreateReleases creates releases -func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error { +func (g *GiteaLocalUploader) CreateReleases(downloader base.Downloader, releases ...*base.Release) error { var rels = make([]*models.Release, 0, len(releases)) for _, release := range releases { var rel = models.Release{ @@ -269,13 +272,11 @@ func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error { // download attachment err = func() error { - resp, err := http.Get(asset.URL) + rc, err := downloader.GetAsset(rel.TagName, asset.ID) if err != nil { return err } - defer resp.Body.Close() - - _, err = storage.Attachments.Save(attach.RelativePath(), resp.Body) + _, err = storage.Attachments.Save(attach.RelativePath(), rc) return err }() if err != nil { diff --git a/modules/migrations/gitea_test.go b/modules/migrations/gitea_test.go index c0d2dcd18..02b2f0a5c 100644 --- a/modules/migrations/gitea_test.go +++ b/modules/migrations/gitea_test.go @@ -26,7 +26,7 @@ func TestGiteaUploadRepo(t *testing.T) { user := models.AssertExistsAndLoadBean(t, &models.User{ID: 1}).(*models.User) var ( - downloader = NewGithubDownloaderV3("", "", "go-xorm", "builder") + downloader = NewGithubDownloaderV3("", "", "", "go-xorm", "builder") repoName = "builder-" + time.Now().Format("2006-01-02-15-04-05") uploader = NewGiteaLocalUploader(graceful.GetManager().HammerContext(), user, user.Name, repoName) ) diff --git a/modules/migrations/github.go b/modules/migrations/github.go index 97d62b994..eb73a7e0d 100644 --- a/modules/migrations/github.go +++ b/modules/migrations/github.go @@ -6,8 +6,11 @@ package migrations import ( + "bytes" "context" "fmt" + "io" + "io/ioutil" "net/http" "net/url" "strings" @@ -37,16 +40,6 @@ func init() { type GithubDownloaderV3Factory struct { } -// Match returns ture if the migration remote URL matched this downloader factory -func (f *GithubDownloaderV3Factory) Match(opts base.MigrateOptions) (bool, error) { - u, err := url.Parse(opts.CloneAddr) - if err != nil { - return false, err - } - - return strings.EqualFold(u.Host, "github.com") && opts.AuthUsername != "", nil -} - // New returns a Downloader related to this factory according MigrateOptions func (f *GithubDownloaderV3Factory) New(opts base.MigrateOptions) (base.Downloader, error) { u, err := url.Parse(opts.CloneAddr) @@ -60,7 +53,7 @@ func (f *GithubDownloaderV3Factory) New(opts base.MigrateOptions) (base.Download log.Trace("Create github downloader: %s/%s", oldOwner, oldName) - return NewGithubDownloaderV3(opts.AuthUsername, opts.AuthPassword, oldOwner, oldName), nil + return NewGithubDownloaderV3(opts.AuthUsername, opts.AuthPassword, opts.AuthToken, oldOwner, oldName), nil } // GitServiceType returns the type of git service @@ -81,7 +74,7 @@ type GithubDownloaderV3 struct { } // NewGithubDownloaderV3 creates a github Downloader via github v3 API -func NewGithubDownloaderV3(userName, password, repoOwner, repoName string) *GithubDownloaderV3 { +func NewGithubDownloaderV3(userName, password, token, repoOwner, repoName string) *GithubDownloaderV3 { var downloader = GithubDownloaderV3{ userName: userName, password: password, @@ -90,23 +83,19 @@ func NewGithubDownloaderV3(userName, password, repoOwner, repoName string) *Gith repoName: repoName, } - var client *http.Client - if userName != "" { - if password == "" { - ts := oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: userName}, - ) - client = oauth2.NewClient(downloader.ctx, ts) - } else { - client = &http.Client{ - Transport: &http.Transport{ - Proxy: func(req *http.Request) (*url.URL, error) { - req.SetBasicAuth(userName, password) - return nil, nil - }, - }, - } - } + client := &http.Client{ + Transport: &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + req.SetBasicAuth(userName, password) + return nil, nil + }, + }, + } + if token != "" { + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: token}, + ) + client = oauth2.NewClient(downloader.ctx, ts) } downloader.client = github.NewClient(client) return &downloader @@ -290,10 +279,8 @@ func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease) } for _, asset := range rel.Assets { - u, _ := url.Parse(*asset.BrowserDownloadURL) - u.User = url.UserPassword(g.userName, g.password) r.Assets = append(r.Assets, base.ReleaseAsset{ - URL: u.String(), + ID: *asset.ID, Name: *asset.Name, ContentType: asset.ContentType, Size: asset.Size, @@ -331,6 +318,18 @@ func (g *GithubDownloaderV3) GetReleases() ([]*base.Release, error) { return releases, nil } +// GetAsset returns an asset +func (g *GithubDownloaderV3) GetAsset(_ string, id int64) (io.ReadCloser, error) { + asset, redir, err := g.client.Repositories.DownloadReleaseAsset(g.ctx, g.repoOwner, g.repoName, id, http.DefaultClient) + if err != nil { + return nil, err + } + if asset == nil { + return ioutil.NopCloser(bytes.NewBufferString(redir)), nil + } + return asset, nil +} + // GetIssues returns issues according start and limit func (g *GithubDownloaderV3) GetIssues(page, perPage int) ([]*base.Issue, bool, error) { opt := &github.IssueListByRepoOptions{ diff --git a/modules/migrations/github_test.go b/modules/migrations/github_test.go index 814c771e8..0b8c559d3 100644 --- a/modules/migrations/github_test.go +++ b/modules/migrations/github_test.go @@ -64,7 +64,7 @@ func assertLabelEqual(t *testing.T, name, color, description string, label *base func TestGitHubDownloadRepo(t *testing.T) { GithubLimitRateRemaining = 3 //Wait at 3 remaining since we could have 3 CI in // - downloader := NewGithubDownloaderV3(os.Getenv("GITHUB_READ_TOKEN"), "", "go-gitea", "test_repo") + downloader := NewGithubDownloaderV3("", "", os.Getenv("GITHUB_READ_TOKEN"), "go-gitea", "test_repo") err := downloader.RefreshRate() assert.NoError(t, err) diff --git a/modules/migrations/gitlab.go b/modules/migrations/gitlab.go index 4f218c95f..eec16d243 100644 --- a/modules/migrations/gitlab.go +++ b/modules/migrations/gitlab.go @@ -8,6 +8,8 @@ import ( "context" "errors" "fmt" + "io" + "net/http" "net/url" "strings" "time" @@ -32,21 +34,6 @@ func init() { type GitlabDownloaderFactory struct { } -// Match returns true if the migration remote URL matched this downloader factory -func (f *GitlabDownloaderFactory) Match(opts base.MigrateOptions) (bool, error) { - var matched bool - - u, err := url.Parse(opts.CloneAddr) - if err != nil { - return false, err - } - if strings.EqualFold(u.Host, "gitlab.com") && opts.AuthUsername != "" { - matched = true - } - - return matched, nil -} - // New returns a Downloader related to this factory according MigrateOptions func (f *GitlabDownloaderFactory) New(opts base.MigrateOptions) (base.Downloader, error) { u, err := url.Parse(opts.CloneAddr) @@ -56,10 +43,11 @@ func (f *GitlabDownloaderFactory) New(opts base.MigrateOptions) (base.Downloader baseURL := u.Scheme + "://" + u.Host repoNameSpace := strings.TrimPrefix(u.Path, "/") + repoNameSpace = strings.TrimSuffix(repoNameSpace, ".git") log.Trace("Create gitlab downloader. BaseURL: %s RepoName: %s", baseURL, repoNameSpace) - return NewGitlabDownloader(baseURL, repoNameSpace, opts.AuthUsername, opts.AuthPassword), nil + return NewGitlabDownloader(baseURL, repoNameSpace, opts.AuthUsername, opts.AuthPassword, opts.AuthToken), nil } // GitServiceType returns the type of git service @@ -85,15 +73,13 @@ type GitlabDownloader struct { // NewGitlabDownloader creates a gitlab Downloader via gitlab API // Use either a username/password, personal token entered into the username field, or anonymous/public access // Note: Public access only allows very basic access -func NewGitlabDownloader(baseURL, repoPath, username, password string) *GitlabDownloader { +func NewGitlabDownloader(baseURL, repoPath, username, password, token string) *GitlabDownloader { var gitlabClient *gitlab.Client var err error - if username != "" { - if password == "" { - gitlabClient, err = gitlab.NewClient(username, gitlab.WithBaseURL(baseURL)) - } else { - gitlabClient, err = gitlab.NewBasicAuthClient(username, password, gitlab.WithBaseURL(baseURL)) - } + if token != "" { + gitlabClient, err = gitlab.NewClient(token, gitlab.WithBaseURL(baseURL)) + } else { + gitlabClient, err = gitlab.NewBasicAuthClient(username, password, gitlab.WithBaseURL(baseURL)) } if err != nil { @@ -271,7 +257,7 @@ func (g *GitlabDownloader) GetLabels() ([]*base.Label, error) { } func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Release { - + var zero int r := &base.Release{ TagName: rel.TagName, TargetCommitish: rel.Commit.ID, @@ -284,9 +270,11 @@ func (g *GitlabDownloader) convertGitlabRelease(rel *gitlab.Release) *base.Relea for k, asset := range rel.Assets.Links { r.Assets = append(r.Assets, base.ReleaseAsset{ - URL: asset.URL, - Name: asset.Name, - ContentType: &rel.Assets.Sources[k].Format, + ID: int64(asset.ID), + Name: asset.Name, + ContentType: &rel.Assets.Sources[k].Format, + Size: &zero, + DownloadCount: &zero, }) } return r @@ -315,6 +303,21 @@ func (g *GitlabDownloader) GetReleases() ([]*base.Release, error) { return releases, nil } +// GetAsset returns an asset +func (g *GitlabDownloader) GetAsset(tag string, id int64) (io.ReadCloser, error) { + link, _, err := g.client.ReleaseLinks.GetReleaseLink(g.repoID, tag, int(id)) + if err != nil { + return nil, err + } + resp, err := http.Get(link.URL) + if err != nil { + return nil, err + } + + // resp.Body is closed by the uploader + return resp.Body, nil +} + // GetIssues returns issues according start and limit // Note: issue label description and colors are not supported by the go-gitlab library at this time // TODO: figure out how to transfer issue reactions diff --git a/modules/migrations/gitlab_test.go b/modules/migrations/gitlab_test.go index 003da5bbd..daf05f8e3 100644 --- a/modules/migrations/gitlab_test.go +++ b/modules/migrations/gitlab_test.go @@ -27,7 +27,7 @@ func TestGitlabDownloadRepo(t *testing.T) { t.Skipf("Can't access test repo, skipping %s", t.Name()) } - downloader := NewGitlabDownloader("https://gitlab.com", "gitea/test_repo", gitlabPersonalAccessToken, "") + downloader := NewGitlabDownloader("https://gitlab.com", "gitea/test_repo", "", "", gitlabPersonalAccessToken) if downloader == nil { t.Fatal("NewGitlabDownloader is nil") } diff --git a/modules/migrations/migrate.go b/modules/migrations/migrate.go index c970ba692..7858dfc68 100644 --- a/modules/migrations/migrate.go +++ b/modules/migrations/migrate.go @@ -13,7 +13,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/migrations/base" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/structs" ) // MigrateOptions is equal to base.MigrateOptions @@ -33,18 +32,15 @@ func MigrateRepository(ctx context.Context, doer *models.User, ownerName string, var ( downloader base.Downloader uploader = NewGiteaLocalUploader(ctx, doer, ownerName, opts.RepoName) - theFactory base.DownloaderFactory + err error ) for _, factory := range factories { - if match, err := factory.Match(opts); err != nil { - return nil, err - } else if match { + if factory.GitServiceType() == opts.GitServiceType { downloader, err = factory.New(opts) if err != nil { return nil, err } - theFactory = factory break } } @@ -57,11 +53,8 @@ func MigrateRepository(ctx context.Context, doer *models.User, ownerName string, opts.Comments = false opts.Issues = false opts.PullRequests = false - opts.GitServiceType = structs.PlainGitService downloader = NewPlainGitDownloader(ownerName, opts.RepoName, opts.CloneAddr) log.Trace("Will migrate from git: %s", opts.OriginalURL) - } else if opts.GitServiceType == structs.NotMigrated { - opts.GitServiceType = theFactory.GitServiceType() } uploader.gitServiceType = opts.GitServiceType @@ -169,7 +162,7 @@ func migrateRepository(downloader base.Downloader, uploader base.Uploader, opts relBatchSize = len(releases) } - if err := uploader.CreateReleases(releases[:relBatchSize]...); err != nil { + if err := uploader.CreateReleases(downloader, releases[:relBatchSize]...); err != nil { return err } releases = releases[relBatchSize:] diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 217a6f74a..808d2ffbc 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -218,6 +218,32 @@ func (gt GitServiceType) Name() string { return "" } +// Title represents the service type's proper title +func (gt GitServiceType) Title() string { + switch gt { + case GithubService: + return "GitHub" + case GiteaService: + return "Gitea" + case GitlabService: + return "GitLab" + case GogsService: + return "Gogs" + case PlainGitService: + return "Git" + } + return "" +} + +// TokenAuth represents whether a service type supports token-based auth +func (gt GitServiceType) TokenAuth() bool { + switch gt { + case GithubService, GiteaService, GitlabService: + return true + } + return false +} + var ( // SupportedFullGitService represents all git services supported to migrate issues/labels/prs and etc. // TODO: add to this list after new git service added @@ -233,6 +259,7 @@ type MigrateRepoOption struct { CloneAddr string `json:"clone_addr" binding:"Required"` AuthUsername string `json:"auth_username"` AuthPassword string `json:"auth_password"` + AuthToken string `json:"auth_token"` // required: true UID int `json:"uid" binding:"Required"` // required: true diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 388348e95..5aeffc758 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -26,6 +26,7 @@ return_to_gitea = Return to Gitea username = Username email = Email Address password = Password +access_token = Access Token re_type = Re-Type Password captcha = CAPTCHA twofa = Two-Factor Authentication @@ -707,9 +708,10 @@ form.name_reserved = The repository name '%s' is reserved. form.name_pattern_not_allowed = The pattern '%s' is not allowed in a repository name. need_auth = Clone Authorization -migrate_type = Migration Type -migrate_type_helper = This repository will be a mirror -migrate_type_helper_disabled = Your site administrator has disabled new mirrors. +migrate_options = Migration Options +migrate_service = Migration Service +migrate_options_mirror_helper = This repository will be a mirror +migrate_options_mirror_disabled = Your site administrator has disabled new mirrors. migrate_items = Migration Items migrate_items_wiki = Wiki migrate_items_milestones = Milestones @@ -725,7 +727,7 @@ migrate.permission_denied = You are not allowed to import local repositories. migrate.invalid_local_path = "The local path is invalid. It does not exist or is not a directory." migrate.failed = Migration failed: %v migrate.lfs_mirror_unsupported = Mirroring LFS objects is not supported - use 'git lfs fetch --all' and 'git lfs push --all' instead. -migrate.migrate_items_options = When migrating from github, input a username and migration options will be displayed. +migrate.migrate_items_options = Authentication is needed to migrate items from a service that supports them. migrated_from = Migrated from %[2]s migrated_from_fake = Migrated From %[1]s migrate.migrating = Migrating from %s ... diff --git a/routers/repo/repo.go b/routers/repo/repo.go index 27c8ff1e0..71df2d0cb 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -7,7 +7,6 @@ package repo import ( "fmt" - "net/url" "os" "path" "strings" @@ -269,6 +268,9 @@ func Migrate(ctx *context.Context) { ctx.Data["pull_requests"] = ctx.Query("pull_requests") == "1" ctx.Data["releases"] = ctx.Query("releases") == "1" ctx.Data["LFSActive"] = setting.LFS.StartServer + // Plain git should be first + ctx.Data["service"] = structs.PlainGitService + ctx.Data["Services"] = append([]structs.GitServiceType{structs.PlainGitService}, structs.SupportedFullGitService...) ctxUser := checkContextUser(ctx, ctx.QueryInt64("org")) if ctx.Written() { @@ -316,6 +318,9 @@ func handleMigrateError(ctx *context.Context, owner *models.User, err error, nam // MigratePost response for migrating from external git repository func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { ctx.Data["Title"] = ctx.Tr("new_migrate") + // Plain git should be first + ctx.Data["service"] = structs.PlainGitService + ctx.Data["Services"] = append([]structs.GitServiceType{structs.PlainGitService}, structs.SupportedFullGitService...) ctxUser := checkContextUser(ctx, form.UID) if ctx.Written() { @@ -349,15 +354,9 @@ func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { return } - var gitServiceType = structs.PlainGitService - u, err := url.Parse(form.CloneAddr) - if err == nil && strings.EqualFold(u.Host, "github.com") { - gitServiceType = structs.GithubService - } - var opts = migrations.MigrateOptions{ OriginalURL: form.CloneAddr, - GitServiceType: gitServiceType, + GitServiceType: structs.GitServiceType(form.Service), CloneAddr: remoteAddr, RepoName: form.RepoName, Description: form.Description, @@ -365,6 +364,7 @@ func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { Mirror: form.Mirror && !setting.Repository.DisableMirrors, AuthUsername: form.AuthUsername, AuthPassword: form.AuthPassword, + AuthToken: form.AuthToken, Wiki: form.Wiki, Issues: form.Issues, Milestones: form.Milestones, diff --git a/templates/repo/migrate.tmpl b/templates/repo/migrate.tmpl index 60b432bea..d5a31a680 100644 --- a/templates/repo/migrate.tmpl +++ b/templates/repo/migrate.tmpl @@ -14,83 +14,52 @@ {{.i18n.Tr "repo.migrate.clone_address_desc"}}{{if .ContextUser.CanImportLocal}} {{.i18n.Tr "repo.migrate.clone_local_path"}}{{end}} -
{{.i18n.Tr "repo.migrate.migrate_items_options"}} {{if .LFSActive}}
{{.i18n.Tr "repo.migrate.lfs_mirror_unsupported"}}{{end}}
-
-
- - {{.i18n.Tr "repo.need_auth"}} -
-
-
- - -
- -
- - -
-
-
-
- -
- -